#include "window.hpp"
#include "init/splash_window.hpp"

#include <hex/api/imhex_api.hpp>
#include <hex/api/event_manager.hpp>

#include <hex/helpers/utils.hpp>
#include <hex/helpers/fmt.hpp>
#include <hex/helpers/logger.hpp>

#include <romfs/romfs.hpp>

#include <imgui.h>
#include <imgui_internal.h>
#include <hex/ui/imgui_imhex_extensions.h>

#include <imgui_impl_glfw.h>
#include <imgui_impl_opengl3.h>
#include <GLFW/glfw3.h>
#include <opengl_support.h>

#include <wolv/utils/guards.hpp>

#include <future>
#include <numeric>
#include <random>


using namespace std::literals::chrono_literals;

namespace hex::init {

    struct GlfwError {
        int errorCode = 0;
        std::string desc;
    };

    GlfwError lastGlfwError;

    WindowSplash::WindowSplash() : m_window(nullptr) {
        this->initGLFW();
        this->initImGui();
        this->initMyself();

        ImHexApi::System::impl::setGPUVendor(reinterpret_cast<const char *>(glGetString(GL_VENDOR)));

        EventManager::subscribe<RequestAddInitTask>([this](const std::string& name, bool async, const TaskFunction &function){
            this->m_tasks.push_back(Task{ name, function, async });
        });
    }

    WindowSplash::~WindowSplash() {
        this->exitImGui();
        this->exitGLFW();
    }


    static void centerWindow(GLFWwindow *window) {
        // Get the primary monitor
        GLFWmonitor *monitor = glfwGetPrimaryMonitor();
        if (!monitor)
            return;

        // Get information about the monitor
        const GLFWvidmode *mode = glfwGetVideoMode(monitor);
        if (!mode)
            return;

        // Get the position of the monitor's viewport on the virtual screen
        int monitorX, monitorY;
        glfwGetMonitorPos(monitor, &monitorX, &monitorY);

        // Get the window size
        int windowWidth, windowHeight;
        glfwGetWindowSize(window, &windowWidth, &windowHeight);

        // Center the splash screen on the monitor
        glfwSetWindowPos(window, monitorX + (mode->width - windowWidth) / 2, monitorY + (mode->height - windowHeight) / 2);
    }

    void WindowSplash::createTask(const Task& task) {
        auto runTask = [&, task] {
            try {
                // Save an iterator to the current task name
                decltype(this->m_currTaskNames)::iterator taskNameIter;
                {
                    std::lock_guard guard(this->m_progressMutex);
                    this->m_currTaskNames.push_back(task.name + "...");
                    taskNameIter = std::prev(this->m_currTaskNames.end());
                }

                // When the task finished, increment the progress bar
                ON_SCOPE_EXIT {
                    this->m_completedTaskCount += 1;
                    this->m_progress = float(this->m_completedTaskCount) / float(this->m_totalTaskCount);
                };

                // Execute the actual task and track the amount of time it took to run
                auto startTime = std::chrono::high_resolution_clock::now();
                bool taskStatus = task.callback();
                auto endTime = std::chrono::high_resolution_clock::now();

                auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();

                if (taskStatus)
                    log::info("Task '{}' finished successfully in {} ms", task.name, milliseconds);
                else
                    log::warn("Task '{}' finished unsuccessfully in {} ms", task.name, milliseconds);

                // Track the overall status of the tasks
                this->m_taskStatus = this->m_taskStatus && taskStatus;

                // Erase the task name from the list of running tasks
                {
                    std::lock_guard guard(this->m_progressMutex);
                    this->m_currTaskNames.erase(taskNameIter);
                }
            } catch (const std::exception &e) {
                log::error("Init task '{}' threw an exception: {}", task.name, e.what());
                this->m_taskStatus = false;
            } catch (...) {
                log::error("Init task '{}' threw an unidentifiable exception", task.name);
                this->m_taskStatus = false;
            }
        };

        this->m_totalTaskCount += 1;

        // If the task can be run asynchronously, run it in a separate thread
        // otherwise run it in this thread and wait for it to finish
        if (task.async) {
            std::thread([runTask = std::move(runTask)]{ runTask(); }).detach();
        } else {
            runTask();
        }
    }

    std::future<bool> WindowSplash::processTasksAsync() {
        return std::async(std::launch::async, [this] {
            auto startTime = std::chrono::high_resolution_clock::now();

            // Loop over all registered init tasks
            for (auto it = this->m_tasks.begin(); it != this->m_tasks.end(); ++it) {
                // Construct a new task callback
                this->createTask(*it);
            }

            // Check every 100ms if all tasks have run
            while (true) {
                {
                    std::scoped_lock lock(this->m_tasksMutex);
                    if (this->m_completedTaskCount >= this->m_totalTaskCount)
                        break;
                }

                std::this_thread::sleep_for(100ms);
            }

            auto endTime = std::chrono::high_resolution_clock::now();

            auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();
            log::info("ImHex fully started in {}ms", milliseconds);

            // Small extra delay so the last progress step is visible
            std::this_thread::sleep_for(100ms);

            return this->m_taskStatus.load();
        });
    }


    FrameResult WindowSplash::fullFrame() {
        glfwSetWindowSize(this->m_window, 640_scaled, 400_scaled);
        centerWindow(this->m_window);

        glfwPollEvents();

        // Start a new ImGui frame
        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplGlfw_NewFrame();
        ImGui::NewFrame();

        auto scale = ImHexApi::System::getGlobalScale();

        // Draw the splash screen background
        auto drawList = ImGui::GetBackgroundDrawList();
        {

            // Draw the splash screen background
            drawList->AddImage(this->splashBackgroundTexture, ImVec2(0, 0), this->splashBackgroundTexture.getSize() * scale);

            {

                // Function to highlight a given number of bytes at a position in the splash screen
                const auto highlightBytes = [&](ImVec2 start, size_t count, ImColor color, float opacity) {
                    // Dimensions and number of bytes that are drawn. Taken from the splash screen image
                    const auto hexSize = ImVec2(29, 18) * scale;
                    const auto hexSpacing = ImVec2(17.4, 15) * scale;
                    const auto hexStart = ImVec2(27, 127) * scale;

                    constexpr auto HexCount = ImVec2(13, 7);

                    bool isStart = true;

                    color.Value.w *= opacity;

                    // Loop over all the bytes on the splash screen
                    for (u32 y = u32(start.y); y < u32(HexCount.y); y += 1) {
                        for (u32 x = u32(start.x); x < u32(HexCount.x); x += 1) {
                            if (count-- == 0)
                                return;

                            // Find the start position of the byte to draw
                            auto pos = hexStart + ImVec2(float(x), float(y)) * (hexSize + hexSpacing);

                            // Fill the rectangle in the byte with the given color
                            drawList->AddRectFilled(pos + ImVec2(0, -hexSpacing.y / 2), pos + hexSize + ImVec2(0, hexSpacing.y / 2), color);

                            // Add some extra color on the right if the current byte isn't the last byte, and we didn't reach the right side of the image
                            if (count > 0 && x != u32(HexCount.x) - 1)
                                drawList->AddRectFilled(pos + ImVec2(hexSize.x, -hexSpacing.y / 2), pos + hexSize + ImVec2(hexSpacing.x, hexSpacing.y / 2), color);

                            // Add some extra color on the left if this is the first byte we're highlighting
                            if (isStart) {
                                isStart = false;
                                drawList->AddRectFilled(pos - hexSpacing / 2, pos + ImVec2(0, hexSize.y + hexSpacing.y / 2), color);
                            }

                            // Add some extra color on the right if this is the last byte
                            if (count == 0) {
                                drawList->AddRectFilled(pos + ImVec2(hexSize.x, -hexSpacing.y / 2), pos + hexSize + hexSpacing / 2, color);
                            }
                        }

                        start.x = 0;
                    }
                };

                // Draw all highlights, slowly fading them in as the init tasks progress
                for (const auto &highlight : this->highlights)
                    highlightBytes(highlight.start, highlight.count, highlight.color, this->progressLerp);
            }

            this->progressLerp += (this->m_progress - this->progressLerp) * 0.1F;

            // Draw the splash screen foreground
            drawList->AddImage(this->splashTextTexture, ImVec2(0, 0), this->splashTextTexture.getSize() * scale);

            // Draw the "copyright" notice
            drawList->AddText(ImVec2(35, 85) * scale, ImColor(0xFF, 0xFF, 0xFF, 0xFF), hex::format("WerWolv\n2020 - {0}", &__DATE__[7]).c_str());

            // Draw version information
            // In debug builds, also display the current commit hash and branch
            #if defined(DEBUG)
                const static auto VersionInfo = hex::format("{0} : {1}@{2}", ImHexApi::System::getImHexVersion(), ImHexApi::System::getCommitBranch(), ImHexApi::System::getCommitHash());
            #else
                const static auto VersionInfo = hex::format("{0}", ImHexApi::System::getImHexVersion());
            #endif

            drawList->AddText(ImVec2((this->splashBackgroundTexture.getSize().x * scale - ImGui::CalcTextSize(VersionInfo.c_str()).x) / 2, 105 * scale), ImColor(0xFF, 0xFF, 0xFF, 0xFF), VersionInfo.c_str());
        }

        // Draw the task progress bar
        {
            std::lock_guard guard(this->m_progressMutex);

            const auto progressBackgroundStart = ImVec2(99, 357) * scale;
            const auto progressBackgroundSize = ImVec2(442, 30) * scale;

            const auto progressStart = progressBackgroundStart + ImVec2(0, 20) * scale;
            const auto progressSize = ImVec2(progressBackgroundSize.x * this->m_progress, 10 * scale);

            // Draw progress bar
            drawList->AddRectFilled(progressStart, progressStart + progressSize, 0xD0FFFFFF);

            // Draw task names separated by | characters
            if (!this->m_currTaskNames.empty()) {
                drawList->PushClipRect(progressBackgroundStart, progressBackgroundStart + progressBackgroundSize, true);
                drawList->AddText(progressStart + ImVec2(5, -20) * scale, ImColor(0xFF, 0xFF, 0xFF, 0xFF), hex::format("{}", fmt::join(this->m_currTaskNames, " | ")).c_str());
                drawList->PopClipRect();
            }
        }

        // Render the frame
        ImGui::Render();
        int displayWidth, displayHeight;
        glfwGetFramebufferSize(this->m_window, &displayWidth, &displayHeight);
        glViewport(0, 0, displayWidth, displayHeight);
        glClearColor(0.00F, 0.00F, 0.00F, 0.00F);
        glClear(GL_COLOR_BUFFER_BIT);
        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

        glfwSwapBuffers(this->m_window);

        // Check if all background tasks have finished so the splash screen can be closed
        if (this->tasksSucceeded.wait_for(0s) == std::future_status::ready) {
            if (this->tasksSucceeded.get()) {
                log::debug("All tasks finished with success !");
                return FrameResult::success;
            } else {
                log::warn("All tasks finished, but some failed");
                return FrameResult::failure;
            }
        }

        return FrameResult::wait;
    }

    bool WindowSplash::loop() {
        // Splash window rendering loop
        while (true) {
            auto res = this->fullFrame();
            if (res == FrameResult::success) return true;
            else if (res == FrameResult::failure) return false;
        }
    }

    void WindowSplash::initGLFW() {
        glfwSetErrorCallback([](int errorCode, const char *desc) {
            lastGlfwError.errorCode = errorCode;
            lastGlfwError.desc = std::string(desc);
            log::error("GLFW Error [{}] : {}", errorCode, desc);
        });

        if (!glfwInit()) {
            log::fatal("Failed to initialize GLFW!");
            std::exit(EXIT_FAILURE);
        }

        // Configure used OpenGL version
        #if defined(OS_MACOS)
            glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
            glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
            glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
            glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_FALSE);
            glfwWindowHint(GLFW_COCOA_GRAPHICS_SWITCHING, GLFW_TRUE);
        #else
            glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
            glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
        #endif

        // Make splash screen non-resizable, undecorated and transparent
        glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
        glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
        glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE);
        glfwWindowHint(GLFW_DECORATED, GLFW_FALSE);
        glfwWindowHint(GLFW_FLOATING, GLFW_FALSE);
        glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);

        // Create the splash screen window
        this->m_window = glfwCreateWindow(1, 1, "Starting ImHex...", nullptr, nullptr);
        if (this->m_window == nullptr) {
            hex::nativeErrorMessage(hex::format(
                "Failed to create GLFW window: [{}] {}.\n"
                "You may not have a renderer available.\n"
                "The most common cause of this is using a virtual machine\n"
                "You may want to try a release artifact ending with 'NoGPU'"
                , lastGlfwError.errorCode, lastGlfwError.desc));
            std::exit(EXIT_FAILURE);
        }

        // Force window to be fully opaque by default
        glfwSetWindowOpacity(this->m_window, 1.0F);

        // Calculate native scale factor for hidpi displays
        {
            float xScale = 0, yScale = 0;
            glfwGetWindowContentScale(this->m_window, &xScale, &yScale);

            auto meanScale = std::midpoint(xScale, yScale);
            if (meanScale <= 0.0F)
                meanScale = 1.0F;

            #if defined(OS_MACOS)
                meanScale /= getBackingScaleFactor();
            #elif defined(OS_WEB)
                meanScale = 1.0F;
            #endif

            ImHexApi::System::impl::setGlobalScale(meanScale);
            ImHexApi::System::impl::setNativeScale(meanScale);

            log::info("Native scaling set to: {:.1f}", meanScale);
        }

        glfwMakeContextCurrent(this->m_window);
        glfwSwapInterval(1);
    }

    void WindowSplash::initImGui() {
        // Initialize ImGui
        IMGUI_CHECKVERSION();
        GImGui = ImGui::CreateContext();
        ImGui::StyleColorsDark();

        ImGui_ImplGlfw_InitForOpenGL(this->m_window, true);

        #if defined(OS_MACOS)
            ImGui_ImplOpenGL3_Init("#version 150");
        #elif defined(OS_WEB)
            ImGui_ImplOpenGL3_Init();
        #else
            ImGui_ImplOpenGL3_Init("#version 130");
        #endif

        auto &io = ImGui::GetIO();

        ImGui::GetStyle().ScaleAllSizes(ImHexApi::System::getGlobalScale());

        // Load fonts necessary for the splash screen
        {
            io.Fonts->Clear();

            ImFontConfig cfg;
            cfg.OversampleH = cfg.OversampleV = 1, cfg.PixelSnapH = true;
            cfg.SizePixels = 13.0_scaled;
            io.Fonts->AddFontDefault(&cfg);

            std::uint8_t *px;
            int w, h;
            io.Fonts->GetTexDataAsAlpha8(&px, &w, &h);

            // Create new font atlas
            GLuint tex;
            glGenTextures(1, &tex);
            glBindTexture(GL_TEXTURE_2D, tex);
            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_R8, w, h, 0, GL_ALPHA, GL_UNSIGNED_BYTE, px);
            io.Fonts->SetTexID(reinterpret_cast<ImTextureID>(tex));
        }

        // Don't save window settings for the splash screen
        io.IniFilename = nullptr;
    }

    /**
     * @brief Initialize resources for the splash window
     */
    void WindowSplash::initMyself() {

        // Load splash screen image from romfs
        this->splashBackgroundTexture = ImGuiExt::Texture(romfs::get("splash_background.png").span());
        this->splashTextTexture = ImGuiExt::Texture(romfs::get("splash_text.png").span());

        // If the image couldn't be loaded correctly, something went wrong during the build process
        // Close the application since this would lead to errors later on anyway.
        if (!this->splashBackgroundTexture.isValid() || !this->splashTextTexture.isValid()) {
            log::fatal("Could not load splash screen image!");
            std::exit(EXIT_FAILURE);
        }



        std::mt19937 rng(std::random_device{}());

        u32 lastPos = 0;
        u32 lastCount = 0;
        for (auto &highlight : this->highlights) {
            auto newPos = lastPos + lastCount + (rng() % 40);
            auto newCount = (rng() % 7) + 3;
            highlight.start.x = newPos % 13;
            highlight.start.y = newPos / 13;
            highlight.count = newCount;

            {
                float r, g, b;
                ImGui::ColorConvertHSVtoRGB(
                        (rng() % 360) / 100.0F,
                        (25 + rng() % 70) / 100.0F,
                        (85 + rng() % 10) / 100.0F,
                        r, g, b);
                highlight.color = ImColor(r, g, b, 0x50 / 255.0F);
            }

            lastPos = newPos;
            lastCount = newCount;
        }
    }

    void WindowSplash::startStartupTasks() {
        // Launch init tasks in the background
        this->tasksSucceeded = processTasksAsync();
    }

    void WindowSplash::exitGLFW() {
        glfwDestroyWindow(this->m_window);
        glfwTerminate();
    }

    void WindowSplash::exitImGui() {
        ImGui_ImplOpenGL3_Shutdown();
        ImGui_ImplGlfw_Shutdown();
        ImGui::DestroyContext();
    }

}
