/*
	ApplicationFramework
	by John Ryland
	Copyright (c) 2023
*/

////////////////////////////////////////////////////////////////////////////////////
//	Glfw Window System

#include "GlfwWindowSystem.h"
#include "GlfwPlatform.h"
#include "Window.h"
//#include "imgui_internal.h"
#define GLFW_INCLUDE_NONE
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#include <cstdio>
#include <cstdlib>
#include <vector>
#include <algorithm>

GLFWwindow* window;

static
void GlfwErrorCallback(int error, const char *message)
{
    fprintf(stderr, "[glfw] Error: %d\n[glfw] Message: %s\n", error, message);
}

namespace ApplicationFramework {

struct IWindow
{
    Window*        m_window;
    GLFWwindow*    m_handle;  // m_osData;
    // any other extra data
    void*          m_osHandle;
    void*          m_rendererData;
    void*          m_uiSystemData;
    void*          m_surface;
    void*          m_userPointer;
};

GlfwWindowSystem::GlfwWindowSystem()
{
    glfwSetErrorCallback(GlfwErrorCallback);

    if (!glfwInit())
    {
        fprintf(stderr, "[glfw] failed to init!\n");
        exit(1);
    }

// if Vulkan
    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
// else if OpenGL
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// #ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, true);
// #endif
    /*
    */
// endif
}

// virtual
GlfwWindowSystem::~GlfwWindowSystem()
{
    glfwTerminate();
}

// virtual
void GlfwWindowSystem::Initialize()
{
    for (auto& win : m_windows)
        win->m_window->Initialize();
}

// virtual
void GlfwWindowSystem::Shutdown()
{
    for (auto& win : m_windows)
        win->m_window->Shutdown();
}

// virtual
void GlfwWindowSystem::Update()
{
    for (auto& win : m_windows)
    {
        //ImGui::SetCurrentViewport(NULL, (ImGuiViewportP*)ImGui::FindViewportByPlatformHandle(win->m_handle));
        win->m_window->Update();
        //GImGui->FrameCount++;
    }
}

static
void GlfwMouseButtonCallback(GLFWwindow* window, int button, int action, int modifierKeys)
{
    IWindow* iwindow = (IWindow*)glfwGetWindowUserPointer(window);
    if (action == GLFW_PRESS)
        iwindow->m_window->OnMousePress(button, modifierKeys);
    else
        iwindow->m_window->OnMouseRelease(button, modifierKeys);
}

static
void GlfwScrollCallback(GLFWwindow* window, double xoffset, double yoffset)
{
    ((IWindow*)glfwGetWindowUserPointer(window))->m_window->OnMouseScroll(xoffset, yoffset);
}

static
void GlfwKeyCallback(GLFWwindow* window, int keycode, int scancode, int action, int modifierKeys)
{
    IWindow* iwindow = (IWindow*)glfwGetWindowUserPointer(window);
    if (action == GLFW_PRESS)
        iwindow->m_window->OnKeyPress(keycode, scancode, modifierKeys);
    else if (action == GLFW_RELEASE)
        iwindow->m_window->OnKeyRelease(keycode, scancode, modifierKeys);
    else
        iwindow->m_window->OnKeyRepeat(keycode, scancode, modifierKeys);
}

static
void GlfwWindowFocusCallback(GLFWwindow* window, int focused)
{
    IWindow* iwindow = (IWindow*)glfwGetWindowUserPointer(window);
    if (focused == GLFW_TRUE)
        iwindow->m_window->OnFocusEnter();
    else
        iwindow->m_window->OnFocusLeave();
}

static
void GlfwCursorPosCallback(GLFWwindow* window, double x, double y)
{
    ((IWindow*)glfwGetWindowUserPointer(window))->m_window->OnMouseMove(x, y);
}

static
void GlfwCursorEnterCallback(GLFWwindow* window, int entered)
{
    IWindow* iwindow = (IWindow*)glfwGetWindowUserPointer(window);
    if (entered == GLFW_TRUE)
        iwindow->m_window->OnEnter();
    else
        iwindow->m_window->OnLeave();
}

static
void GlfwCharCallback(GLFWwindow* window, unsigned int unicodeCodePoint)
{
    ((IWindow*)glfwGetWindowUserPointer(window))->m_window->OnTextEntered(unicodeCodePoint);
}

static
void InstallCallbacks(GLFWwindow* window)
{
    glfwSetWindowFocusCallback(window, GlfwWindowFocusCallback);
    glfwSetCursorEnterCallback(window, GlfwCursorEnterCallback);
    glfwSetCursorPosCallback(window, GlfwCursorPosCallback);
    glfwSetMouseButtonCallback(window, GlfwMouseButtonCallback);
    glfwSetScrollCallback(window, GlfwScrollCallback);
    glfwSetKeyCallback(window, GlfwKeyCallback);
    glfwSetCharCallback(window, GlfwCharCallback);
}

// virtual
IWindow* GlfwWindowSystem::CreateWindow(Window* win, int width, int height, const char* title)
{
    //GLFWwindow* 
    window = glfwCreateWindow(width, height, title, NULL, NULL);
    IWindow* iwindow = new IWindow{ win, window };
    glfwSetWindowUserPointer(window, (void*)iwindow);
    InstallCallbacks(window);
    m_windows.emplace_back(iwindow);
    return iwindow;
}

// TODO: might be able to remove this shim and just call directly in code below it, however this is useful for now as can
// inject whatever for all of the glfw functions to modify this easily as the design evolves
template <typename T, typename R, typename ...A>
R ExecuteIfFindWindow(T& windowMap, IWindow* iwindow, R (*function)(GLFWwindow*, A...), A... args)
{
    //if (windowMap.count(*win))
    //if (iwindow->m_handle)
    {
        // return function(windowMap[win], args...);
        return function(iwindow->m_handle, args...);
    }
    //return R();
}

// virtual
void GlfwWindowSystem::DestroyWindow(IWindow* iwindow)
{
    ExecuteIfFindWindow(m_windows, iwindow, glfwDestroyWindow);
    delete iwindow;
}

//virtual
void GlfwWindowSystem::ShowWindow(IWindow* iwindow)
{
    ExecuteIfFindWindow(m_windows, iwindow, glfwShowWindow);
}

//virtual
void GlfwWindowSystem::CloseWindow(IWindow* iwindow)
{
    ExecuteIfFindWindow(m_windows, iwindow, glfwHideWindow);
    ExecuteIfFindWindow(m_windows, iwindow, glfwSetWindowShouldClose, (int)true);
    // The window will get destroyed when LastWindowClosed is next called
}

//virtual
void GlfwWindowSystem::SetWindowPosition(IWindow* iwindow, uint32_t x, uint32_t y)
{
    ExecuteIfFindWindow(m_windows, iwindow, glfwSetWindowPos, (int)x, (int)y);
}

//virtual
void GlfwWindowSystem::GetWindowPosition(IWindow* iwindow, uint32_t& x, uint32_t& y)
{
    ExecuteIfFindWindow(m_windows, iwindow, glfwGetWindowPos, (int*)&x, (int*)&y);
}

//virtual
void GlfwWindowSystem::SetWindowSize(IWindow* iwindow, uint32_t width, uint32_t height)
{
    ExecuteIfFindWindow(m_windows, iwindow, glfwSetWindowSize, (int)width, (int)height);
}

//virtual
void GlfwWindowSystem::GetWindowSize(IWindow* iwindow, uint32_t& width, uint32_t& height)
{
    ExecuteIfFindWindow(m_windows, iwindow, glfwGetWindowSize, (int*)&width, (int*)&height);
}

//virtual
void GlfwWindowSystem::SetWindowFocus(IWindow* iwindow)
{
    ExecuteIfFindWindow(m_windows, iwindow, glfwFocusWindow);
}

//virtual
bool GlfwWindowSystem::GetWindowFocus(IWindow* iwindow)
{
    return ExecuteIfFindWindow(m_windows, iwindow, glfwGetWindowAttrib, GLFW_FOCUSED);
}

//virtual
bool GlfwWindowSystem::GetWindowMinimized(IWindow* iwindow)
{
    return ExecuteIfFindWindow(m_windows, iwindow, glfwGetWindowAttrib, GLFW_ICONIFIED);
}

//virtual
void GlfwWindowSystem::SetWindowTitle(IWindow* iwindow, const char* str)
{
    ExecuteIfFindWindow(m_windows, iwindow, glfwSetWindowTitle, str);
}

//virtual
void GlfwWindowSystem::SetWindowAlpha(IWindow* iwindow, float alpha)
{
    ExecuteIfFindWindow(m_windows, iwindow, glfwSetWindowOpacity, alpha);
}

//virtual
bool GlfwWindowSystem::IsKeyPressed(IWindow* iwindow, int key)
{
    return ExecuteIfFindWindow(m_windows, iwindow, glfwGetKey, key) == GLFW_PRESS;
}

// virtual
bool GlfwWindowSystem::LastWindowClosed()
{
    std::vector<IWindow*> closedWindows;
    if (m_windows.size())
    {
        for (auto& iwindow : m_windows)
            if (glfwWindowShouldClose(iwindow->m_handle))
                closedWindows.emplace_back(iwindow);
        for (auto& iwindow : closedWindows)
        {
            glfwHideWindow(iwindow->m_handle);
            iwindow->m_window->Shutdown();
            m_windows.erase(std::remove(m_windows.begin(), m_windows.end(), iwindow));
        }
        return !m_windows.size();
    }
    return false;
}

// virtual
void GlfwWindowSystem::Prepare()
{
    // Poll and handle events (inputs, window resize, etc.)
    // You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs.
    // - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy of the mouse data.
    // - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data.
    // Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags.
    glfwPollEvents();
}

// virtual
void GlfwWindowSystem::Render()
{
}

// virtual
void GlfwWindowSystem::Present()
{
}

} // ApplicationFramework namespace
