#include <pch.hpp>

#include <core.hpp>
#include <window/linux_window.hpp>
#include <renderer/graphics_context.hpp>
#include <events/application_event.hpp>
#include <opengl/opengl_context.hpp>
#include <events/mouse_event.hpp>
#include <renderer/renderer.hpp>
#include <events/key_event.hpp>
#include <logging.hpp>

#include <GLFW/glfw3.h>
#include <memory>

namespace OpenEngine {
    static bool glfw_initialized = false;

    static void GLFWErrorCallback(int error, const char* description)
    {
        OE_CORE_ERROR("GLFW Error {}: {}", error, description);
    }

    Scope<Window> Window::Create(const WindowProps& props)
    {
        return CreateScope<LinuxWindow>(props);
    }

    LinuxWindow::LinuxWindow(const WindowProps& props)
    {
        Init(props);
    }

    LinuxWindow::~LinuxWindow()
    {
        Shutdown();
    }

    void LinuxWindow::Init(const WindowProps& props)
    {
        OE_PROFILE_FUNCTION();

        data.title = props.title;
        data.height = props.height;
        data.width = props.width;

        OE_CORE_INFO("Creating window {} ({} {})", props.title, props.width, props.height);

        {
            OE_PROFILE_SCOPE("Initializing GLFW");

            if (!glfw_initialized) {
                int success = glfwInit();
                OE_CORE_ASSERT(success, "Could not initialize GLFW!");

                spdlog::trace("Setting GLFW context version to: 3.3");
                glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
                glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
                spdlog::trace("Setting opengl profile to core.");
                glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

                glfwSetErrorCallback(GLFWErrorCallback);
                glfw_initialized = true;
            }

#ifdef OE_DEBUG_TOOLS
            if (Renderer::GetAPI() == RendererAPI::API::OpenGL)
                glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);
#endif
            gl_window = glfwCreateWindow((int)props.width, (int)props.height, props.title.c_str(), nullptr, nullptr);

            context = CreateScope<OpenGLContext>(gl_window);
            context->Init();
        }


        glfwSetWindowUserPointer(gl_window, &data);
        SetVSync(true);

        glfwSetWindowSizeCallback(gl_window, [](GLFWwindow* window, int width, int height)
                {
                    WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window);
                    data.width = width;
                    data.height = height;

                    WindowResizeEvent event(width, height);
                    data.event_callback(event);
                });

        glfwSetWindowCloseCallback(gl_window, [](GLFWwindow* window)
                {
                    WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window);

                    WindowCloseEvent event;
                    data.event_callback(event);
                });

        glfwSetKeyCallback(gl_window, [](GLFWwindow* window, int key, int scancode, int action, int mods)
                {
                    WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window);

                    switch (action) {
                        case GLFW_PRESS:
                        {
                            KeyPressedEvent event(static_cast<KeyCode>(key), scancode, 0, mods);
                            data.event_callback(event);
                            break;
                        }
                        case GLFW_RELEASE:
                        {
                            KeyReleasedEvent event(static_cast<KeyCode>(key), scancode, mods);
                            data.event_callback(event);
                            break;
                        }
                        case GLFW_REPEAT:
                        {
                            KeyPressedEvent event(static_cast<KeyCode>(key), scancode, 1, mods);
                            data.event_callback(event);
                            break;
                        }
                    }
                });

        glfwSetCharCallback(gl_window, [] (GLFWwindow* window, uint32_t keycode)
                {
                    WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window);

                    KeyTypedEvent event(static_cast<KeyCode>(keycode));
                    data.event_callback(event);
                });

        glfwSetMouseButtonCallback(gl_window, [](GLFWwindow* window, int button, int action, int mods)
                {
                    WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window);

                    switch (action) {
                        case GLFW_PRESS:
                        {
                            MouseButtonPressedEvent event(static_cast<MouseCode>(button));
                            data.event_callback(event);
                            break;
                        }
                        case GLFW_RELEASE:
                        {
                            MouseButtonReleasedEvent event(static_cast<MouseCode>(button));
                            data.event_callback(event);
                            break;
                        }
                    }
                });

        glfwSetScrollCallback(gl_window, [](GLFWwindow* window, double xoffset, double yoffset)
                {
                    WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window);

                    MouseScrolledEvent event((float) xoffset, (float)yoffset);
                    data.event_callback(event);
                });

        glfwSetCursorPosCallback(gl_window, [](GLFWwindow* window, double xpos, double ypos)
                {
                    WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window);

                    MouseMovedEvent event((float) xpos, (float)ypos);
                    data.event_callback(event);
                });
    }

    void LinuxWindow::Shutdown()
    {
        glfwDestroyWindow(gl_window);
        glfwTerminate();
        gl_window = nullptr;
    }

    void LinuxWindow::OnUpdate()
    {
        OE_PROFILE_FUNCTION();

        {
            OE_PROFILE_SCOPE("Polling Events");
            glfwPollEvents();
        }
        {
            OE_PROFILE_SCOPE("Swapping Buffers");
            context->SwapBuffers();
        }
    }

    void LinuxWindow::SetVSync(bool enabled)
    {
        if (enabled)
            glfwSwapInterval(1);
        else
            glfwSwapInterval(0);

        data.vsync = enabled;
    }

    bool LinuxWindow::IsVSync() const
    {
        return data.vsync;
    }
}
