#include "platform/glfw/glfw_window.hpp"

#include <GLFW/glfw3.h>
#include <glad/gl.h>

#include "graphics/opengl/framebuffer.hpp"
#include "graphics/opengl/render_context.hpp"

#include "logging/engine_logger.hpp"
#include "utils/assert.hpp"

#include <cstdint>
#include <limits>
#include <memory>
#include <stdexcept>
#include <utility>

using namespace kuiper;

glfw_window::glfw_window(
    std::uint32_t width, std::uint32_t height, std::u8string_view title, GLFWmonitor* monitor, GLFWwindow* share)
    : m_window_handle(create_glfw_window(width, height, title, monitor, share)),
      m_default_framebuffer(std::make_shared<gl::framebuffer>(gl::framebuffer(0))) {
    if (!m_window_handle) {
        throw std::runtime_error("glfwCreateWindow failed, returned nullptr");
    }

    // Window resize callback

    glfwSetFramebufferSizeCallback(m_window_handle, glfw_window::glfw_framebuffer_size_callback);
    glfwSetWindowContentScaleCallback(m_window_handle, glfw_window::glfw_content_scale_callback);

    // Input callbacks

    glfwSetKeyCallback(m_window_handle, glfw_window::glfw_key_event_callback);
    glfwSetCursorPosCallback(m_window_handle, glfw_window::glfw_cursor_pos_callback);
    glfwSetScrollCallback(m_window_handle, glfw_window::glfw_scroll_callback);
    glfwSetDropCallback(m_window_handle, glfw_window::glfw_dragdrop_callback);

    // Make instance of this GLFWWindow available to static callbacks

    glfwSetWindowUserPointer(m_window_handle, this);

    // Initialise OpenGL context

    glfwMakeContextCurrent(m_window_handle);
    gl::initialise();

    // Fixes framebuffer/viewport not being sized correctly on scalings other than 1.0 x 1.0
    resize_back_buffer();

    // Enable required OpenGL features

    gl::render_context::enable_feature(GL_DEPTH_TEST); // depth buffer
    // gl::render_context::disable_feature(GL_STENCIL_TEST); // stencil buffer
    gl::render_context::enable_feature(GL_CULL_FACE); // cull backfacing triangles
    gl::render_context::enable_feature(GL_BLEND);     // blending
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    return;
}

glfw_window::~glfw_window() {
    if (m_window_handle != nullptr) {
        glfwDestroyWindow(m_window_handle);
    }

    return;
}

void* glfw_window::get_native_handle() const noexcept {
    return m_window_handle;
}

void glfw_window::resize_back_buffer() noexcept {
    const auto framebuffer_size = get_framebuffer_size();
    const auto width            = framebuffer_size.x;
    const auto height           = framebuffer_size.y;
    m_default_framebuffer->resize(width, height);
    gl::render_context::set_viewport_size(width, height);

    return;
}

void glfw_window::initialise_glfw(bool prefer_x11) {
    auto logger = engine_logger::get();

    // Check for potential GLFW version mismatches & incompatibilities

    logger.debug("Compiled against GLFW {}.{}.{}", GLFW_VERSION_MAJOR, GLFW_VERSION_MINOR, GLFW_VERSION_REVISION);
    logger.debug("GLFW version string: {}", glfwGetVersionString());

    int glfw_ver_major = 0, glfw_ver_minor = 0, glfw_ver_patch = 0;
    glfwGetVersion(&glfw_ver_major, &glfw_ver_minor, &glfw_ver_patch);

    if (glfw_ver_major > GLFW_VERSION_MAJOR || glfw_ver_minor < GLFW_VERSION_MINOR) {
        logger.warn("Potentially incompatible GLFW loaded: {}.{}.{} (loaded) vs. {}.{}.{} (compiled-for)",
                    glfw_ver_major,
                    glfw_ver_minor,
                    glfw_ver_patch,
                    GLFW_VERSION_MAJOR,
                    GLFW_VERSION_MINOR,
                    GLFW_VERSION_REVISION);
    }

    // GLFW initialisation

    // Set a callback for logging GLFW errors

    glfwSetErrorCallback(glfw_window::glfw_error_callback);

    // Initialisation hints

    if (prefer_x11) {
        logger.info("Hinting to GLFW that X11 should be used");
        glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11);
    }

    // Disable libdecor because I don't care about it, GTK, GNOME or anything else.
    // Also it prints out an annoying error in the console when it fails to load it.
    glfwInitHint(GLFW_WAYLAND_LIBDECOR, GLFW_WAYLAND_DISABLE_LIBDECOR);

    if (!glfwInit()) {
        throw std::runtime_error("Failed to initialise GLFW");
    }

    return;
}

GLFWwindow* glfw_window::create_glfw_window(std::uint32_t      width,
                                            std::uint32_t      height,
                                            std::u8string_view title,
                                            GLFWmonitor*       monitor,
                                            GLFWwindow*        share) noexcept {
    KUIPER_ASSERT((width < std::numeric_limits<int>::max()) && (height < std::numeric_limits<int>::max()));
    push_window_hints();

    return glfwCreateWindow(
        static_cast<int>(width), static_cast<int>(height), reinterpret_cast<const char*>(title.data()), monitor, share);
}

void glfw_window::push_window_hints() noexcept {
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_FALSE);
    glfwWindowHint(GLFW_SCALE_FRAMEBUFFER, GLFW_FALSE);

#if !defined(NDEBUG)
    glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);
#endif

    return;
}

bool glfw_window::should_close() const noexcept {
    return glfwWindowShouldClose(m_window_handle);
}

void glfw_window::poll_events() noexcept {
    const auto old_mouse_pos = m_input_state.get_mouse_pos();

    glfwPollEvents();

    const auto delta = m_input_state.get_mouse_pos() - old_mouse_pos;
    m_input_state.set_mouse_delta(delta);

    return;
}

void glfw_window::swap_buffers() noexcept {
    return glfwSwapBuffers(m_window_handle);
}

void glfw_window::set_swap_interval(std::uint32_t interval) noexcept {
    return glfwSwapInterval(static_cast<int>(interval));
}

void glfw_window::bind_context() noexcept {
    *(volatile unsigned long long*) nullptr = 0x1337beefcafebabe;
}

gl::framebuffer::s_ptr glfw_window::back_buffer() const noexcept {
    return m_default_framebuffer;
}

bool glfw_window::is_cursor_locked() const noexcept {
    return m_cursor_locked;
}

void glfw_window::lock_cursor() noexcept {
    glfwSetInputMode(m_window_handle, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
    m_cursor_locked = true;
    return;
}

void glfw_window::unlock_cursor() noexcept {
    glfwSetInputMode(m_window_handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
    m_cursor_locked = false;
    return;
}

void glfw_window::enable_raw_input() noexcept {
    if (glfwRawMouseMotionSupported())
        glfwSetInputMode(m_window_handle, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);

    return;
}

void glfw_window::disable_raw_input() noexcept {
    if (glfwRawMouseMotionSupported())
        glfwSetInputMode(m_window_handle, GLFW_RAW_MOUSE_MOTION, GLFW_FALSE);

    return;
}

glm::uvec2 glfw_window::get_size() const noexcept {
    int width  = 0;
    int height = 0;
    glfwGetWindowSize(m_window_handle, &width, &height);

    // I don't understand why the width and height is a signed integer
    // I'd like to see a window with negative width & height
    KUIPER_ASSERT((width > 0) && (height > 0));

    return {static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height)};
}

glm::uvec2 glfw_window::get_framebuffer_size() const noexcept {
    int width  = 0;
    int height = 0;
    glfwGetFramebufferSize(m_window_handle, &width, &height);

    // Once again: why would the framebuffer ever have negative dimensions?
    KUIPER_ASSERT((width > 0) && (height > 0));

    return {static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height)};
}

glm::vec2 glfw_window::get_content_scale() const noexcept {
    float xscale = 1.0f;
    float yscale = 1.0f;
    glfwGetWindowContentScale(m_window_handle, &xscale, &yscale);

    return glm::vec2(xscale, yscale);
}

void glfw_window::set_title(std::u8string_view title) noexcept {
    return glfwSetWindowTitle(m_window_handle, reinterpret_cast<const char*>(title.data()));
}

bool glfw_window::is_minimised() const noexcept {
    int iconified = glfwGetWindowAttrib(m_window_handle, GLFW_ICONIFIED);

    return (iconified == GLFW_TRUE);
}

// Callbacks

void glfw_window::glfw_error_callback(int error_code, const char* description) {
    engine_logger::get().error("GLFW error {}: {}", error_code, description);

    return;
}

void glfw_window::glfw_framebuffer_size_callback(GLFWwindow* window, int width, int height) {
    KUIPER_ASSERT((width > 0) && (height > 0));
    engine_logger::get().debug("Framebuffer size changed: {} x {}", width, height);

    glfw_window* window_instance = static_cast<glfw_window*>(glfwGetWindowUserPointer(window));
    window_instance->m_default_framebuffer->resize(width, height);

    return;
}

void glfw_window::glfw_content_scale_callback(GLFWwindow* window, float xscale, float yscale) {
    KUIPER_ASSERT(xscale > 0 && yscale > 0);
    engine_logger::get().debug("Window content scale changed: x: {}, y: {}", xscale, yscale);

    return;
}

void glfw_window::glfw_key_event_callback(GLFWwindow* window, int key, int scancode, int action, int mods) {
    glfw_window* window_instance = static_cast<glfw_window*>(glfwGetWindowUserPointer(window));
    window_instance->m_input_state.set_key_state(key, action);

    return;
}

void glfw_window::glfw_cursor_pos_callback(GLFWwindow* window, double xpos, double ypos) {
    glfw_window* window_instance = static_cast<glfw_window*>(glfwGetWindowUserPointer(window));
    window_instance->m_input_state.set_mouse_pos({xpos, ypos});

    return;
}

void glfw_window::glfw_scroll_callback(GLFWwindow* window, double xoffset, double yoffset) {
    return;
}

void glfw_window::glfw_dragdrop_callback(GLFWwindow* window, int path_count, const char* paths[]) {
    return;
}
