/*
 * Created by Brett on 28/11/23.
 * Licensed under GNU General Public License V3.0
 * See LICENSE file for license detail
 */
#include <blt/gfx/window.h>
#include <blt/std/assert.h>
#include <blt/std/logging.h>
#include <blt/std/time.h>
#include <blt/gfx/input.h>
#include <queue>
#include <imgui.h>
#include "backends/imgui_impl_opengl3.h"
#include "backends/imgui_impl_glfw.h"
#include <blt/gfx/imgui/ImGuiUtils.h>

void error_callback(int error, const char* description)
{
    BLT_ERROR("GLFW Error (%d): %s", error, description);
    std::abort();
}

namespace blt::gfx
{
    // because we aren't meant to have multiple GLFW windows (especially with GLAD) we will keep the window as global state.1
    struct
    {
        /* GLFW Window Object */
        GLFWwindow* window = nullptr;
        /* BLT internal input state manager, handles keyboard/mouse allocations + states */
        input_manager inputManager;
        /* stores any drag and dropped paths for processing */
        std::queue<std::string> pendingPaths;
        /* current width and height of the window */
        std::int32_t width = 0;
        std::int32_t height = 0;
        std::int64_t lastTime = blt::system::nanoTime();
        std::int64_t deltaTime = 0;
        double nanoDelta = 0;
        double millisDelta = 0;
    } window_state;
    
    void create_callbacks()
    {
        /* Setup keyboard callback */
        glfwSetKeyCallback(window_state.window, [](GLFWwindow* window, int key, int scancode, int action, int mods) {
            if (key < 0 || key == GLFW_KEY_UNKNOWN)
                return;
            KEY_STATE state;
            switch (action)
            {
                case GLFW_PRESS:
                    state = KEY_STATE::PRESS;
                    break;
                case GLFW_REPEAT:
                    state = KEY_STATE::REPEAT;
                    break;
                default:
                    state = KEY_STATE::RELEASE;
            }
            window_state.inputManager.key(key) = state;
        });
        
        /* Setup mouse button callback */
        glfwSetMouseButtonCallback(window_state.window, [](GLFWwindow* window, int button, int action, int mods) {
            if (button < 0)
                return;
            MOUSE_STATE state;
            switch (action)
            {
                case GLFW_PRESS:
                    state = MOUSE_STATE::PRESS;
                    break;
                default:
                    state = MOUSE_STATE::RELEASE;
                    break;
            }
            window_state.inputManager.mouse(button) = state;
        });
        
        /* Setup mouse cursor callback */
        glfwSetCursorPosCallback(window_state.window, [](GLFWwindow* window, double x, double y) {
            window_state.inputManager.updateMousePos(x, y);
        });
        
        /* Setup mouse scroll callback */
        glfwSetScrollCallback(window_state.window, [](GLFWwindow* window, double x, double s) {
            window_state.inputManager.updateScroll(s);
        });
        
        /* Setup drop input callback */
        glfwSetDropCallback(window_state.window, [](GLFWwindow* window, int count, const char** paths) {
            for (int i = 0; i < count; i++)
                window_state.pendingPaths.emplace(paths[i]);
        });
    }
    
    void setup_ImGUI()
    {
        const char* glsl_version = "#version 100";
        
        // Setup Dear ImGui context
        IMGUI_CHECKVERSION();
        ImGui::CreateContext();
        ImGuiIO& io = ImGui::GetIO();
        io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;     // Enable Keyboard Controls
        io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;      // Enable Gamepad Controls
        
        // Setup Dear ImGui style
        ImGui::StyleColorsDark();
        //ImGui::Spectrum::StyleColorsSpectrum();
        ImGui::Spectrum::LoadFont();
        ImGui::SetupImGuiStyle(true, 1.0);
        
        // Setup FA
        ImFontConfig config;
        config.MergeMode = true;
        config.GlyphMinAdvanceX = 13.0f; // Use if you want to make the icon monospaced
        static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0};
        io.Fonts->AddFontFromMemoryCompressedBase85TTF(fontAwesomeRegular_compressed_data_base85, 13.0f, &config, icon_ranges);
        io.Fonts->AddFontFromMemoryCompressedTTF(fontAwesomeSolid_compressed_data, static_cast<int>(fontAwesomeSolid_compressed_size), 13.0, &config,
                                                 icon_ranges);
        io.Fonts->AddFontFromMemoryCompressedTTF(fontAwesomeBrands_compressed_data, static_cast<int>(fontAwesomeBrands_compressed_size), 13.0, &config,
                                                 icon_ranges);
        
        //ImGui::StyleColorsLight();
        
        // Setup Platform/Renderer backends
        ImGui_ImplGlfw_InitForOpenGL(window_state.window, true);
#ifdef __EMSCRIPTEN__
        ImGui_ImplGlfw_InstallEmscriptenCanvasResizeCallback("#canvas");
#endif
        ImGui_ImplOpenGL3_Init(glsl_version);

#ifdef __EMSCRIPTEN__
        io.IniFilename = nullptr;
#endif
    }
    
    void loop(void* arg)
    {
        const auto& data = *((const window_data*) arg);
        /* -- Get the current framebuffer size, update the global width/height state, along with OpenGL viewport -- */
        glfwGetFramebufferSize(window_state.window, &window_state.width, &window_state.height);
        glViewport(0, 0, window_state.width, window_state.height);
        // TODO: user option for this
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        
        /* -- Begin the next ImGUI frame -- */
        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplGlfw_NewFrame();
        ImGui::NewFrame();
        
        /* -- Call user update function -- */
        data.update(window_state.width, window_state.height);
        
        /* -- Render the ImGUI frame -- */
        ImGui::Render();
        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
        
        /* -- Update GLFW state -- */
        glfwSwapBuffers(window_state.window);
        glfwPollEvents();
        
        /* -- Update Frame Timing Information -- */
        auto current_time = blt::system::nanoTime();
        window_state.deltaTime = current_time - window_state.lastTime;
        window_state.lastTime = current_time;
        window_state.nanoDelta = static_cast<double>(window_state.deltaTime) / 1e9f;
        window_state.millisDelta = static_cast<double>(window_state.deltaTime) / 1e6f;
    }
    
    void init(const window_data& data)
    {
        /* -- Set up Error Callback -- */
        glfwSetErrorCallback(error_callback);
        BLT_ASSERT(glfwInit() && "Unable to init GLFW. Aborting.");
        
        /* -- Set up Window Context -- */
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, data.context.GL_MAJOR);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, data.context.GL_MINOR);
        glfwWindowHint(GLFW_DOUBLEBUFFER, data.context.DOUBLE_BUFFER);
        glfwWindowHint(GLFW_OPENGL_PROFILE, data.context.GL_PROFILE);
        
        /* -- Create the Window -- */
        window_state.window = glfwCreateWindow(data.width, data.height, data.title.c_str(), nullptr, nullptr);
        BLT_ASSERT(window_state.window && "Unable to create GLFW window.");
        
        /* -- Set Window Specifics + OpenGL -- */
        glfwMakeContextCurrent(window_state.window);
        glfwSwapInterval(data.sync_interval);
#ifndef __EMSCRIPTEN__
        gladLoadGL(glfwGetProcAddress);
#endif
        
        /* -- Set up our local callbacks, ImGUI will then call these -- */
        create_callbacks();
        
        /* -- Set up ImGUI -- */
        setup_ImGUI();
        
        /* -- Call User Provided post-window-init function  -- */
        data.init();

#ifdef __EMSCRIPTEN__
        /*
     * "setting 0 or a negative value as the fps will use the browser’s requestAnimationFrame mechanism to call the main loop function.
     * This is HIGHLY recommended if you are doing rendering, as the browser’s requestAnimationFrame will
     * make sure you render at a proper smooth rate that lines up properly with the browser and monitor."
     * https://emscripten.org/docs/api_reference/emscripten.h.html
     */
    emscripten_set_main_loop_arg(loop, (void*)&data, 0, true);
#else
        /* -- General Loop -- */
        while (!glfwWindowShouldClose(window_state.window))
            loop((void*) &data);
#endif
    }
    
    void cleanup()
    {
        ImGui_ImplOpenGL3_Shutdown();
        ImGui_ImplGlfw_Shutdown();
        ImGui::DestroyContext();
        
        glfwDestroyWindow(window_state.window);
        glfwTerminate();
    }
    
    double getMouseX()
    {
        return window_state.inputManager.mouseX;
    }
    
    double getMouseY()
    {
        return window_state.inputManager.mouseY;
    }
    
    double getMouseDX()
    {
        return window_state.inputManager.deltaX;
    }
    
    double getMouseDY()
    {
        return window_state.inputManager.deltaY;
    }
    
    void lockCursor()
    {
        glfwSetInputMode(window_state.window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
    }
    
    void unlockCursor()
    {
        glfwSetInputMode(window_state.window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
    }
    
    bool isCursorLocked()
    {
        return glfwGetInputMode(window_state.window, GLFW_CURSOR) == GLFW_CURSOR_DISABLED;
    }
    
    bool isCursorInWindow()
    {
#ifdef __EMSCRIPTEN__
        // TODO:
        return true;
#else
        return glfwGetWindowAttrib(window_state.window, GLFW_HOVERED);
#endif
    }
    
    void setRawInput(bool state)
    {
#ifdef __EMSCRIPTEN__
        // TODO?
#else
        if (glfwRawMouseMotionSupported())
            glfwSetInputMode(window_state.window, GLFW_RAW_MOUSE_MOTION, state ? GLFW_TRUE : GLFW_FALSE);
#endif
    }
    
    bool isRawInput()
    {
#ifdef __EMSCRIPTEN__
        return false;
#else
        return glfwGetInputMode(window_state.window, GLFW_RAW_MOUSE_MOTION);
#endif
    }
    
    void setClipboard(const std::string& str)
    {
        glfwSetClipboardString(window_state.window, str.c_str());
    }
    
    std::string getClipboard()
    {
        return glfwGetClipboardString(window_state.window);
    }
    
    bool isMousePressed(int button)
    {
        return window_state.inputManager.isMousePressed(button);
    }
    
    bool isKeyPressed(int key)
    {
        return window_state.inputManager.isKeyPressed(key);
    }
    
    double getFrameDeltaSeconds()
    {
        return window_state.nanoDelta;
    }
    
    double getFrameDeltaMilliseconds()
    {
        return window_state.millisDelta;
    }
    
    std::int64_t getFrameDelta()
    {
        return window_state.deltaTime;
    }
}
