#include "imgui_helper.h"
#include "ComKit.h"
#include "imgui.h"
#include "spdlog/spdlog.h"
#include <dbt.h>
#include <format>
#ifdef _WIN32
bool ImGuiHelper::CreateDeviceD3d(HWND hWnd) {
    DXGI_SWAP_CHAIN_DESC sd = {0};
    sd.BufferCount = 2;
    sd.BufferDesc.Width = 0;
    sd.BufferDesc.Height = 0;
    sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    sd.BufferDesc.RefreshRate.Numerator = 60;
    sd.BufferDesc.RefreshRate.Denominator = 1;
    sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    sd.OutputWindow = hWnd;
    sd.SampleDesc.Count = 1;
    sd.SampleDesc.Quality = 0;
    sd.Windowed = true;
    sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;

    UINT createDeviceFlags = 0;
    D3D_FEATURE_LEVEL featureLevel;
    const D3D_FEATURE_LEVEL featureLevelArray[2] = {D3D_FEATURE_LEVEL_11_0,
                                                    D3D_FEATURE_LEVEL_10_0};
    HRESULT res = D3D11CreateDeviceAndSwapChain(
        nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, createDeviceFlags,
        featureLevelArray, 2, D3D11_SDK_VERSION, &sd, &g_pSwapChain,
        &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext);
    if (res == DXGI_ERROR_UNSUPPORTED) {
        res = D3D11CreateDeviceAndSwapChain(
            nullptr, D3D_DRIVER_TYPE_WARP, nullptr, createDeviceFlags,
            featureLevelArray, 2, D3D11_SDK_VERSION, &sd, &g_pSwapChain,
            &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext);
    }
    if (res != S_OK)
        return false;
    CreateRenderTarget();
    return true;
}

void ImGuiHelper::CleanupDeviceD3d() {
    CleanupRenderTarget();
    if (g_pSwapChain) {
        g_pSwapChain->Release();
        g_pSwapChain = nullptr;
    }
    if (g_pd3dDeviceContext) {
        g_pd3dDeviceContext->Release();
        g_pSwapChain = nullptr;
    }
    if (g_pd3dDevice) {
        g_pd3dDevice->Release();
        g_pd3dDevice = nullptr;
    }
}

void ImGuiHelper::CleanupRenderTarget() {
    if (g_pMainRenderTargetView) {
        g_pMainRenderTargetView->Release();
        g_pMainRenderTargetView = nullptr;
    }
}

void ImGuiHelper::CreateRenderTarget() {
    ID3D11Texture2D *pBackBuffer;
    g_pSwapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer));
    g_pd3dDevice->CreateRenderTargetView(pBackBuffer, nullptr,
                                         &g_pMainRenderTargetView);
    pBackBuffer->Release();
}

LRESULT ImGuiHelper::WndProc(HWND hWnd, UINT msg, WPARAM wParam,
                             LPARAM lParam) {
    if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam)) {
        return true;
    }
    switch (msg) {
    case WM_SYSCOMMAND:
        if ((wParam & 0xfff0) == SC_KEYMENU) {
            return 0;
        }
        break;
    case WM_DESTROY:
        ::PostQuitMessage(0);
        return 0;
    case WM_DEVICECHANGE:
        PDEV_BROADCAST_HDR pHdr = reinterpret_cast<PDEV_BROADCAST_HDR>(lParam);
        switch (wParam) {
        case DBT_DEVICEARRIVAL:
            if (pHdr->dbch_devicetype == DBT_DEVTYP_PORT) {
                PDEV_BROADCAST_PORT pPort =
                    reinterpret_cast<PDEV_BROADCAST_PORT>(pHdr);
                spdlog::log(spdlog::level::debug,
                            std::format("port: [{}] add", pPort->dbcp_name));
                ComKit::FlushLog();
                ComKit::GetAllComPortNames();
                ComKit::Work();
            } else {
                spdlog::log(spdlog::level::debug,
                            std::format("port: other device [{}] add",
                                        pHdr->dbch_devicetype));
            }
            break;
        case DBT_DEVICEREMOVECOMPLETE:
            if (pHdr->dbch_devicetype == DBT_DEVTYP_PORT) {
                PDEV_BROADCAST_PORT pPort =
                    reinterpret_cast<PDEV_BROADCAST_PORT>(pHdr);
                spdlog::log(
                    spdlog::level::debug,
                    std::format("port: [{}] removed", pPort->dbcp_name));
                ComKit::FlushLog();
                ComKit::GetAllComPortNames();
            } else {
                spdlog::log(spdlog::level::debug,
                            std::format("port: other device [{}] add",
                                        pHdr->dbch_devicetype));
            }
            break;
        }
    }
    return ::DefWindowProc(hWnd, msg, wParam, lParam);
}

ImGuiHelper::ImGuiHelper(int width, int height, const char *windowTitle) {
    this->windowTitle = *windowTitle;
    wc = {sizeof(wc), CS_CLASSDC, WndProc,
          0L,         0L,         GetModuleHandle(nullptr),
          nullptr,    nullptr,    nullptr,
          nullptr,    "Windows",  nullptr};
    ::RegisterClassEx(&wc);
    hWnd = ::CreateWindow(wc.lpszClassName, windowTitle, WS_OVERLAPPEDWINDOW,
                          100, 100, width, height, nullptr, nullptr,
                          wc.hInstance, nullptr);
    if (!CreateDeviceD3d(hWnd)) {
        CleanupDeviceD3d();
        ::UnregisterClass(wc.lpszClassName, wc.hInstance);
        return;
    }
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO &io = ImGui::GetIO();
    (void)io;
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
    io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
    io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
    io.ConfigViewportsNoAutoMerge = true;
    io.ConfigViewportsNoTaskBarIcon = false;

    ImGui::StyleColorsLight();
    ImGui_ImplWin32_Init(hWnd);
    ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext);
}

void ImGuiHelper::Render(const std::function<void()> &context,
                         bool &isRunning) {
    ImGuiIO &io = ImGui::GetIO();
    ::ShowWindow(hWnd, SW_HIDE);
    UpdateWindow(hWnd);
    MSG msg;
    while (isRunning) {
        while (::PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
            if (msg.message == WM_QUIT) {
                isRunning = false;
            }
        }
        ImGui_ImplDX11_NewFrame();
        ImGui_ImplWin32_NewFrame();
        ImGui::NewFrame();
        context();
        ImGui::Render();
        if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
            ImGui::UpdatePlatformWindows();
            ImGui::RenderPlatformWindowsDefault();
        }
        const float clear_color_with_alpha[4] = {
            clear_color.x * clear_color.w, clear_color.y * clear_color.w,
            clear_color.z * clear_color.w, clear_color.w};
        g_pd3dDeviceContext->OMSetRenderTargets(1, &g_pMainRenderTargetView,
                                                nullptr);
        g_pd3dDeviceContext->ClearRenderTargetView(g_pMainRenderTargetView,
                                                   clear_color_with_alpha);
        ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
        g_pSwapChain->Present(1, 0);
    }
}

ImGuiHelper::~ImGuiHelper() {
    ImGui_ImplDX11_Shutdown();
    ImGui_ImplWin32_Shutdown();
    ImGui::DestroyContext();
    ::DestroyWindow(hWnd);
    ::UnregisterClass(wc.lpszClassName, wc.hInstance);
}
#elif __linux__
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include <GLFW/glfw3.h>

static void glfw_error_callback(int error, const char *description) {
    fprintf(stderr, "GLFW ERROR %d: %s\n", error, description);
}

const char *ImGuiHelper::glsl_version = "#version 150";

ImGuiHelper::ImGuiHelper(int width, int height, const std::string &windowTitle)
    : width(width), height(height), windowTitle(windowTitle) {
    glfwSetErrorCallback(glfw_error_callback);
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
    window =
        glfwCreateWindow(width, height, windowTitle.c_str(), nullptr, nullptr);
    glfwMakeContextCurrent(window);
    glfwSwapInterval(1);

    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    io = &ImGui::GetIO();
    (void)io;
    io->ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
    io->ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
    io->ConfigFlags |= ImGuiConfigFlags_DockingEnable;
    io->ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
    io->ConfigViewportsNoAutoMerge = true;
    ImGuiStyle &style = ImGui::GetStyle();
    if (io->ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
        style.WindowRounding = .0f;
        style.Colors[ImGuiCol_WindowBg].w = 1.f;
    }
    ImGui_ImplGlfw_InitForOpenGL(window, true);
    ImGui_ImplOpenGL3_Init(ImGuiHelper::glsl_version);
}

void ImGuiHelper::Render(const std::function<void()> &context,
                         bool &isRunning) {
    ImVec4 clear_color = ImVec4{.45f, .55f, .60f, 1.f};
    while (isRunning) {
        glfwPollEvents();
        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplGlfw_NewFrame();
        ImGui::NewFrame();

        context();

        // Rending
        ImGui::Render();
        int display_w, display_h;
        glfwGetFramebufferSize(window, &display_w, &display_h);
        glViewport(0, 0, display_w, display_h);
        glClearColor(clear_color.x * clear_color.w,
                     clear_color.y * clear_color.w,
                     clear_color.z * clear_color.w, clear_color.w);
        glClear(GL_COLOR_BUFFER_BIT);
        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

        if (io->ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
            GLFWwindow *backup_current_context = glfwGetCurrentContext();
            ImGui::UpdatePlatformWindows();
            ImGui::RenderPlatformWindowsDefault();
            glfwMakeContextCurrent(backup_current_context);
        }

        glfwSwapBuffers(window);
    }
    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    ImGui::DestroyContext();

    glfwDestroyWindow(window);
    glfwTerminate();
}

ImGuiHelper::~ImGuiHelper() {
    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    ImGui::DestroyContext();

    glfwDestroyWindow(window);
    glfwTerminate();
}
#endif
