#include "avatar_cache.h"

#include <GLFW/glfw3.h>
#include <algorithm>
#include <array>
#include <chrono>
#include <cctype>
#include <iomanip>
#include <sstream>
#include <vector>

#ifdef _WIN32
#include <windows.h>
#include <bcrypt.h>
#include <objbase.h>
#include <urlmon.h>
#include <wincodec.h>
#endif

AvatarCache::AvatarCache(const std::filesystem::path &user_data_directory)
    : directory_(user_data_directory / "avatars")
{
    std::filesystem::create_directories(directory_);
}

AvatarCache::~AvatarCache()
{
    shutdown();
}

std::string AvatarCache::hashEmail(const std::string &email) const
{
    std::string normalized = email;
    normalized.erase(normalized.begin(), std::find_if(normalized.begin(), normalized.end(),
                                                      [](unsigned char value)
                                                      { return !std::isspace(value); }));
    normalized.erase(std::find_if(normalized.rbegin(), normalized.rend(),
                                  [](unsigned char value)
                                  { return !std::isspace(value); })
                         .base(),
                     normalized.end());
    std::transform(normalized.begin(), normalized.end(), normalized.begin(),
                   [](unsigned char value)
                   { return static_cast<char>(std::tolower(value)); });

#ifdef _WIN32
    BCRYPT_ALG_HANDLE algorithm = nullptr;
    BCRYPT_HASH_HANDLE hash = nullptr;
    std::array<unsigned char, 16> digest{};
    DWORD object_size = 0;
    DWORD written = 0;
    if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_MD5_ALGORITHM, nullptr, 0) < 0 ||
        BCryptGetProperty(algorithm, BCRYPT_OBJECT_LENGTH, reinterpret_cast<PUCHAR>(&object_size),
                          sizeof(object_size), &written, 0) < 0)
    {
        if (algorithm)
            BCryptCloseAlgorithmProvider(algorithm, 0);
        return {};
    }
    std::vector<unsigned char> object(object_size);
    if (BCryptCreateHash(algorithm, &hash, object.data(), object_size, nullptr, 0, 0) < 0 ||
        BCryptHashData(hash, reinterpret_cast<PUCHAR>(normalized.data()),
                       static_cast<ULONG>(normalized.size()), 0) < 0 ||
        BCryptFinishHash(hash, digest.data(), static_cast<ULONG>(digest.size()), 0) < 0)
    {
        if (hash)
            BCryptDestroyHash(hash);
        BCryptCloseAlgorithmProvider(algorithm, 0);
        return {};
    }
    BCryptDestroyHash(hash);
    BCryptCloseAlgorithmProvider(algorithm, 0);
    std::ostringstream output;
    for (unsigned char value : digest)
        output << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(value);
    return output.str();
#else
    return std::to_string(std::hash<std::string>{}(normalized));
#endif
}

unsigned int AvatarCache::textureFor(const std::string &email)
{
    if (email.empty())
        return 0;
    const std::string hash = hashEmail(email);
    if (hash.empty())
        return 0;
    Entry &entry = entries_[hash];
    if (entry.texture)
        return entry.texture;
    if (entry.requested)
        return 0;

    entry.requested = true;
    entry.file = directory_ / (hash + ".img");
    if (std::filesystem::exists(entry.file))
    {
        entry.texture = loadTexture(entry.file);
        return entry.texture;
    }
#ifdef _WIN32
    const std::filesystem::path file = entry.file;
    const std::wstring url = L"https://www.gravatar.com/avatar/" +
                             std::wstring(hash.begin(), hash.end()) + L"?s=64&d=identicon&r=g";
    entry.download = std::async(std::launch::async, [file, url]
                                { return SUCCEEDED(URLDownloadToFileW(nullptr, url.c_str(), file.wstring().c_str(), 0, nullptr)); });
#endif
    return 0;
}

void AvatarCache::update()
{
    for (auto &[hash, entry] : entries_)
    {
        (void)hash;
        if (entry.texture || !entry.download.valid())
            continue;
        if (entry.download.wait_for(std::chrono::seconds(0)) != std::future_status::ready)
            continue;
        if (entry.download.get())
            entry.texture = loadTexture(entry.file);
    }
}

unsigned int AvatarCache::loadTexture(const std::filesystem::path &file) const
{
#ifdef _WIN32
    CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
    IWICImagingFactory *factory = nullptr;
    IWICBitmapDecoder *decoder = nullptr;
    IWICBitmapFrameDecode *frame = nullptr;
    IWICFormatConverter *converter = nullptr;
    UINT width = 0;
    UINT height = 0;
    std::vector<unsigned char> pixels;
    unsigned int texture = 0;
    if (SUCCEEDED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
                                   IID_PPV_ARGS(&factory))) &&
        SUCCEEDED(factory->CreateDecoderFromFilename(file.wstring().c_str(), nullptr, GENERIC_READ,
                                                     WICDecodeMetadataCacheOnLoad, &decoder)) &&
        SUCCEEDED(decoder->GetFrame(0, &frame)) &&
        SUCCEEDED(factory->CreateFormatConverter(&converter)) &&
        SUCCEEDED(converter->Initialize(frame, GUID_WICPixelFormat32bppRGBA,
                                        WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom)) &&
        SUCCEEDED(converter->GetSize(&width, &height)))
    {
        pixels.resize(static_cast<size_t>(width) * height * 4);
        if (SUCCEEDED(converter->CopyPixels(nullptr, width * 4,
                                            static_cast<UINT>(pixels.size()), pixels.data())))
        {
            glGenTextures(1, &texture);
            glBindTexture(GL_TEXTURE_2D, texture);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, static_cast<int>(width), static_cast<int>(height),
                         0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
            glBindTexture(GL_TEXTURE_2D, 0);
        }
    }
    if (converter)
        converter->Release();
    if (frame)
        frame->Release();
    if (decoder)
        decoder->Release();
    if (factory)
        factory->Release();
    CoUninitialize();
    return texture;
#else
    (void)file;
    return 0;
#endif
}

void AvatarCache::shutdown()
{
    for (auto &[hash, entry] : entries_)
    {
        (void)hash;
        if (entry.download.valid())
            entry.download.wait();
        if (entry.texture)
            glDeleteTextures(1, &entry.texture);
        entry.texture = 0;
    }
    entries_.clear();
}
