#include "window.h"
#include "core/glad.h"
#include "input/input.h"

#include <glad/gl.h>

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

namespace Window {

static constexpr unsigned INIT_WINDOW_WIDTH = 1280;
static constexpr unsigned INIT_WINDOW_HEIGHT = 720;
static constexpr double MOUSE_SENSITIVITY = 0.15;

static void glfw_error_callback(int error, char const* description);
static void framebuffer_size_callback(GLFWwindow* glfw_window, int width, int height);
static void window_size_callback(GLFWwindow* glfw_window, int width, int height);
static void window_close_callback(GLFWwindow* glfw_window);
static void key_callback(GLFWwindow* glfw_window, int key, int scancode, int action, int mods);
static void mouse_cursor_callback(GLFWwindow* glfw_window, double xpos, double ypos);

WindowModule::WindowModule(flecs::world& world)
{
    glfwInit();
    world.component<Window>();
    world.component<PrimaryWindow>();
    world.component<std::shared_ptr<GLFWwindow>>();
    world.component<std::optional<CursorPosition>>();
    world.component<LogicalSize>();
    world.component<PhysicalSize>();

    spawn_window(world);
}

flecs::entity spawn_window(flecs::world& world)
{
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifndef NDEBUG
    glfwSetErrorCallback(glfw_error_callback);
    glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);
#else
    // Maximize in release build
    glfwWindowHint(GLFW_MAXIMIZED, GLFW_TRUE);
#endif

    auto glfw_window = std::shared_ptr<GLFWwindow>(
        glfwCreateWindow(INIT_WINDOW_WIDTH, INIT_WINDOW_HEIGHT, "OpenGL", nullptr, nullptr),
        [](GLFWwindow* window) { glfwDestroyWindow(window); });

    if (!glfw_window) {
        spdlog::critical("Failed to create window");
    }

    // Create OpenGL context
    glfwMakeContextCurrent(glfw_window.get());

    // Callbacks
    glfwSetWindowUserPointer(glfw_window.get(), &world);
    glfwSetKeyCallback(glfw_window.get(), key_callback);
    glfwSetCursorPosCallback(glfw_window.get(), mouse_cursor_callback);
    glfwSetWindowSizeCallback(glfw_window.get(), window_size_callback);
    glfwSetWindowCloseCallback(glfw_window.get(), window_close_callback);
    glfwSetFramebufferSizeCallback(glfw_window.get(), framebuffer_size_callback);

    init_glad();

    int window_width, window_height;
    glfwGetWindowSize(glfw_window.get(), &window_width, &window_height);

    int framebuffer_width, framebuffer_height;
    glfwGetFramebufferSize(glfw_window.get(), &framebuffer_width, &framebuffer_height);
    glViewport(0, 0, framebuffer_width, framebuffer_height);

    flecs::entity window = world.entity("PrimaryWindow");
    window.add<Window>();
    window.add<Input::ButtonInput<Input::KeyCode>>();
    window.add<std::optional<CursorPosition>>();
    window.set<std::shared_ptr<GLFWwindow>>(glfw_window);
    window.set<LogicalSize>({window_width, window_height});
    window.set<PhysicalSize>({framebuffer_width, framebuffer_height});

    world.set<PrimaryWindow>({window});

    return window;
}

static void glfw_error_callback(int error, char const* description)
{
    spdlog::warn("GLFW [{:d}]: {:s}\n", error, description);
}

void window_size_callback(GLFWwindow* glfw_window, int width, int height)
{
    auto* world = static_cast<flecs::world*>(glfwGetWindowUserPointer(glfw_window));
    auto window = world->get<PrimaryWindow>()->window;

    window.set<LogicalSize>({width, height});
}

void window_close_callback(GLFWwindow* glfw_window)
{
    auto* world = static_cast<flecs::world*>(glfwGetWindowUserPointer(glfw_window));
    world->quit();
}

void framebuffer_size_callback(GLFWwindow* glfw_window, int width, int height)
{
    auto* world = static_cast<flecs::world*>(glfwGetWindowUserPointer(glfw_window));
    auto window = world->get<PrimaryWindow>()->window;

    window.set<PhysicalSize>({width, height});

    glViewport(0, 0, width, height);
}

void key_callback(GLFWwindow* glfw_window,
                  int key,
                  [[maybe_unused]] int scancode,
                  int action,
                  [[maybe_unused]] int mods)
{
    auto* world = static_cast<flecs::world*>(glfwGetWindowUserPointer(glfw_window));

    // Maybe lookup with the glfw pointer instead?
    auto window = world->get<PrimaryWindow>()->window;

    world->component<Input::KeyboardInput>().set<Input::KeyboardInput>(
        {.key_code = static_cast<Input::KeyCode>(key),
         .action = static_cast<Input::Action>(action),
         .window = window});
}

void mouse_cursor_callback(GLFWwindow* glfw_window, double xpos, double ypos)
{
    auto* world = static_cast<flecs::world*>(glfwGetWindowUserPointer(glfw_window));
    auto window = world->get<PrimaryWindow>()->window;

    auto cursor_position = window.get_mut<std::optional<CursorPosition>>();
    bool had_value = cursor_position->has_value();

    *cursor_position = {xpos, ypos};

    // Prevent invalid delta events when the cursor position wasn't valid
    if (!had_value)
        return;

    glm::vec2 delta{xpos - cursor_position->value().x, ypos - cursor_position->value().y};

    delta *= MOUSE_SENSITIVITY;

    world->component<Input::MouseMotion>().set<Input::MouseMotion>({delta});
}

} // namespace Window
