#include "window_manager.h"

#include <GLFW/glfw3.h>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <filesystem>
#include <vector>

#ifdef _WIN32
#define GLFW_EXPOSE_NATIVE_WIN32
#include <GLFW/glfw3native.h>
#include <dwmapi.h>
#include <windows.h>
#include <wincodec.h>
#endif

#ifdef _WIN32
namespace
{
    HICON loadPngIcon(const std::filesystem::path &path, UINT size, bool rounded)
    {
        const HRESULT com_result = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
        const bool uninitialize_com = SUCCEEDED(com_result);

        IWICImagingFactory *factory = nullptr;
        IWICBitmapDecoder *decoder = nullptr;
        IWICBitmapFrameDecode *frame = nullptr;
        IWICBitmapScaler *scaler = nullptr;
        IWICFormatConverter *converter = nullptr;
        HICON icon = nullptr;

        if (FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
                                    IID_PPV_ARGS(&factory))) ||
            FAILED(factory->CreateDecoderFromFilename(path.c_str(), nullptr, GENERIC_READ,
                                                      WICDecodeMetadataCacheOnLoad, &decoder)) ||
            FAILED(decoder->GetFrame(0, &frame)) ||
            FAILED(factory->CreateBitmapScaler(&scaler)) ||
            FAILED(scaler->Initialize(frame, size, size, WICBitmapInterpolationModeFant)) ||
            FAILED(factory->CreateFormatConverter(&converter)) ||
            FAILED(converter->Initialize(scaler, GUID_WICPixelFormat32bppBGRA,
                                         WICBitmapDitherTypeNone, nullptr, 0.0,
                                         WICBitmapPaletteTypeCustom)))
        {
            goto cleanup;
        }

        {
            std::vector<BYTE> pixels(static_cast<size_t>(size) * size * 4);
            if (FAILED(converter->CopyPixels(nullptr, size * 4,
                                             static_cast<UINT>(pixels.size()), pixels.data())))
            {
                goto cleanup;
            }

            if (rounded)
            {
                const float radius = static_cast<float>(size) * 0.22f;
                const float maximum = static_cast<float>(size) - radius - 0.5f;
                const float minimum = radius - 0.5f;
                for (UINT y = 0; y < size; ++y)
                {
                    for (UINT x = 0; x < size; ++x)
                    {
                        const float nearest_x = std::clamp(static_cast<float>(x), minimum, maximum);
                        const float nearest_y = std::clamp(static_cast<float>(y), minimum, maximum);
                        const float dx = static_cast<float>(x) - nearest_x;
                        const float dy = static_cast<float>(y) - nearest_y;
                        const float coverage = std::clamp(radius + 0.5f - std::sqrt(dx * dx + dy * dy),
                                                          0.0f, 1.0f);
                        pixels[(static_cast<size_t>(y) * size + x) * 4 + 3] =
                            static_cast<BYTE>(coverage * 255.0f);
                    }
                }
            }

            BITMAPV5HEADER header{};
            header.bV5Size = sizeof(header);
            header.bV5Width = static_cast<LONG>(size);
            header.bV5Height = -static_cast<LONG>(size);
            header.bV5Planes = 1;
            header.bV5BitCount = 32;
            header.bV5Compression = BI_BITFIELDS;
            header.bV5RedMask = 0x00ff0000;
            header.bV5GreenMask = 0x0000ff00;
            header.bV5BlueMask = 0x000000ff;
            header.bV5AlphaMask = 0xff000000;

            void *bitmap_bits = nullptr;
            const HDC screen = GetDC(nullptr);
            const HBITMAP color = CreateDIBSection(screen, reinterpret_cast<BITMAPINFO *>(&header),
                                                   DIB_RGB_COLORS, &bitmap_bits, nullptr, 0);
            ReleaseDC(nullptr, screen);
            const HBITMAP mask = CreateBitmap(size, size, 1, 1, nullptr);
            if (color && mask && bitmap_bits)
            {
                std::memcpy(bitmap_bits, pixels.data(), pixels.size());
                ICONINFO info{};
                info.fIcon = TRUE;
                info.hbmColor = color;
                info.hbmMask = mask;
                icon = CreateIconIndirect(&info);
            }
            if (color)
                DeleteObject(color);
            if (mask)
                DeleteObject(mask);
        }

    cleanup:
        if (converter)
            converter->Release();
        if (scaler)
            scaler->Release();
        if (frame)
            frame->Release();
        if (decoder)
            decoder->Release();
        if (factory)
            factory->Release();
        if (uninitialize_com)
            CoUninitialize();
        return icon;
    }
} // namespace
#endif

WindowManager::~WindowManager()
{
    destroy();
}

bool WindowManager::create(const char *title, int width, int height)
{
#ifdef _WIN32
    using SetDpiAwarenessContext = BOOL(WINAPI *)(HANDLE);
    const HMODULE user32 = GetModuleHandleW(L"user32.dll");
    const FARPROC dpi_address = GetProcAddress(user32, "SetProcessDpiAwarenessContext");
    SetDpiAwarenessContext set_dpi_awareness = nullptr;
    static_assert(sizeof(set_dpi_awareness) == sizeof(dpi_address));
    std::memcpy(&set_dpi_awareness, &dpi_address, sizeof(set_dpi_awareness));
    if (set_dpi_awareness)
        set_dpi_awareness(reinterpret_cast<HANDLE>(-4)); // Per-monitor v2
#endif
    if (!glfwInit())
        return false;
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE);
    window_ = glfwCreateWindow(width, height, title, nullptr, nullptr);
    if (!window_)
    {
        glfwTerminate();
        return false;
    }

    glfwMakeContextCurrent(window_);
    glfwSwapInterval(1);
    glfwSetWindowUserPointer(window_, this);
    glfwSetWindowContentScaleCallback(window_, contentScaleCallback);
    float x_scale = 1.0f;
    float y_scale = 1.0f;
    glfwGetWindowContentScale(window_, &x_scale, &y_scale);
    dpi_scale_ = std::max(x_scale, y_scale);
    applyNativeCaption();
    applyNativeIcons();
    return true;
}

void WindowManager::destroy()
{
    if (!window_)
        return;
    glfwDestroyWindow(window_);
    window_ = nullptr;
    destroyNativeIcons();
    glfwTerminate();
}

void WindowManager::pollEvents() { glfwPollEvents(); }
void WindowManager::swapBuffers() { glfwSwapBuffers(window_); }
void WindowManager::requestClose() { glfwSetWindowShouldClose(window_, GLFW_TRUE); }
bool WindowManager::shouldClose() const { return !window_ || glfwWindowShouldClose(window_); }

bool WindowManager::consumeDpiChange()
{
    const bool changed = dpi_changed_;
    dpi_changed_ = false;
    return changed;
}

void WindowManager::contentScaleCallback(GLFWwindow *window, float x_scale, float y_scale)
{
    auto *manager = static_cast<WindowManager *>(glfwGetWindowUserPointer(window));
    if (manager)
        manager->updateDpi(std::max(x_scale, y_scale));
}

void WindowManager::updateDpi(float scale)
{
    scale = std::clamp(scale, 1.0f, 4.0f);
    if (scale == dpi_scale_)
        return;
    dpi_scale_ = scale;
    dpi_changed_ = true;
    applyNativeCaption();
}

void WindowManager::applyNativeCaption() const
{
#ifdef _WIN32
    if (!window_)
        return;

    const HWND hwnd = glfwGetWin32Window(window_);
    if (!hwnd)
        return;

    const BOOL dark = TRUE;

    // DWMWA_USE_IMMERSIVE_DARK_MODE
    DwmSetWindowAttribute(hwnd, 20, &dark, sizeof(dark));

    DWORD corner_pref = 0;

    switch (corner_mode_)
    {
    case WindowCornerMode::Default:
        corner_pref = 0; // DWMWCP_DEFAULT
        break;

    case WindowCornerMode::DoNotRound:
        corner_pref = 1; // DWMWCP_DONOTROUND
        break;

    case WindowCornerMode::Round:
        corner_pref = 2; // DWMWCP_ROUND
        break;

    case WindowCornerMode::RoundSmall:
        corner_pref = 3; // DWMWCP_ROUNDSMALL
        break;
    }

    // DWMWA_WINDOW_CORNER_PREFERENCE
    DwmSetWindowAttribute(hwnd, 33, &corner_pref, sizeof(corner_pref));

    // Windows 11 caption customization. Older versions safely ignore these.
    const COLORREF caption = static_cast<COLORREF>(caption_color_);
    const COLORREF border = RGB(51, 55, 63);
    const COLORREF text = RGB(199, 203, 209);

    // DWMWA_BORDER_COLOR
    DwmSetWindowAttribute(hwnd, 34, &border, sizeof(border));

    // DWMWA_CAPTION_COLOR
    DwmSetWindowAttribute(hwnd, 35, &caption, sizeof(caption));

    // DWMWA_TEXT_COLOR
    DwmSetWindowAttribute(hwnd, 36, &text, sizeof(text));
#endif
}

void WindowManager::applyNativeIcons()
{
#ifdef _WIN32
    destroyNativeIcons();
    const HWND hwnd = glfwGetWin32Window(window_);
    const auto asset_dir = std::filesystem::path(GITREE_IMAGE_ASSET_DIR);
    window_icon_ = loadPngIcon(asset_dir / L"gitree_logo.png", 32, true);
    taskbar_icon_ = loadPngIcon(asset_dir / L"gitree_logo.png", 64, true);

    if (window_icon_)
    {
        SendMessageW(hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(window_icon_));
        SendMessageW(hwnd, WM_SETICON, ICON_SMALL2, reinterpret_cast<LPARAM>(window_icon_));
    }
    if (taskbar_icon_)
    {
        SendMessageW(hwnd, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(taskbar_icon_));
    }
#endif
}

void WindowManager::destroyNativeIcons()
{
#ifdef _WIN32
    if (window_icon_)
        DestroyIcon(static_cast<HICON>(window_icon_));
    if (taskbar_icon_)
        DestroyIcon(static_cast<HICON>(taskbar_icon_));
#endif
    window_icon_ = nullptr;
    taskbar_icon_ = nullptr;
}

void WindowManager::setCornerMode(WindowCornerMode mode)
{
    corner_mode_ = mode;
    applyNativeCaption();
}

void WindowManager::setCaptionColor(std::uint32_t color)
{
    caption_color_ = color;
    applyNativeCaption();
}
