/// Guards


#ifdef GLBACKEND_GLFW


/// Includes


#include <glbackend_glfw.hpp>

#include <array>
#include <functional>
#include <limits>
#include <sstream>
#include <string>
#include <type_traits>
#include <utility>

#include <glbase.hpp>
#include <glbackend.hpp>

#include <GLFW/glfw3.h>

// NOLINTNEXTLINE
#define STR_EXCEPTION GLBase::Exception
#include <str.hpp>


/// Special member functions


GLBackendGLFW::GLBackendGLFW(
    std::string const & title,
    std::array<int, 2>  size,
    std::array<int, 2>  version,
    int                 samples,
    bool                fullscreen,
    bool                transparent
)
:
    GLBackend(),
    window_{nullptr}
{
    //// Backend errors
    glfwSetErrorCallback(debug_glfw_error_callback_);
    auto const glfw_get_error_description = []()
    {
        char const * description{};
        glfwGetError(&description);
        return description;
    };

    //// Backend init
    if (!glfwInit())
        STR_THROW(
            "Failed to initialize GLFW" << ":\n" <<
            glfw_get_error_description()
        );
    ++init_count_;

    //// Window options
    glfwDefaultWindowHints();
    glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
    GLFWmonitor * monitor = nullptr;
    if (fullscreen)
        monitor = glfwGetPrimaryMonitor();
    if (transparent)
    {
        glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE);
        glfwWindowHint(GLFW_DECORATED, GLFW_FALSE);
        glfwWindowHint(GLFW_FLOATING, GLFW_TRUE);
    }
    if (size[0] == 0 || size[1] == 0)
    {
        auto const * mode = glfwGetVideoMode(glfwGetPrimaryMonitor());
        size[0] = mode->width;
        size[1] = mode->height;
    }
    size_ = size;

    //// Context options
    glfwWindowHint(GLFW_DOUBLEBUFFER, GLFW_TRUE);
    glfwWindowHint(GLFW_RED_BITS,      8); // NOLINT
    glfwWindowHint(GLFW_GREEN_BITS,    8); // NOLINT
    glfwWindowHint(GLFW_BLUE_BITS,     8); // NOLINT
    glfwWindowHint(GLFW_ALPHA_BITS,    8); // NOLINT
    glfwWindowHint(GLFW_DEPTH_BITS,   24); // NOLINT
    glfwWindowHint(GLFW_STENCIL_BITS,  8); // NOLINT
    if (samples)
        glfwWindowHint(GLFW_SAMPLES, samples);
    if (version[0])
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, version[0]);
    if (version[1])
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, version[1]);
    if ((version[0] == 3 && version[1] >= 2) || version[0] >= 4) // NOLINT
    {
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
        glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
    }
    if (debug() >= 1)
        glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);
    else if (version[0] >= 2) // NOLINT
        glfwWindowHint(GLFW_CONTEXT_NO_ERROR, GLFW_TRUE);

    try
    {
        //// Window
        window_ = glfwCreateWindow(
            size[0],
            size[1],
            title.c_str(),
            monitor,
            nullptr
        );
        if (!window_)
            STR_THROW(
                "Failed to create GLFW window" << ":\n" <<
                glfw_get_error_description()
            );

        //// Context
        glfwMakeContextCurrent(window_);
        if (debug() == 0)
            glfwSwapInterval(1);
        init_();

        //// Lock
        if (fullscreen)
            lock(true);
    }
    catch (...)
    {
        destroy_();
        throw;
    }

    //// Callbacks
    // NOLINTNEXTLINE
    #define GLBACKEND_GLFW_SELF_(WINDOW) \
        (*static_cast<GLBackendGLFW *>(glfwGetWindowUserPointer(WINDOW)))
    glfwSetWindowUserPointer(window_, this);
    glfwSetKeyCallback(
        window_,
        [](
            GLFWwindow * window,
            int key,
            int scancode,
            int action,
            int mods
        )
        {
            (void)scancode;
            (void)mods;
            if (action != GLFW_PRESS)
                return;
            auto & self = GLBACKEND_GLFW_SELF_(window);
            if (self.callback_key_)
            {
                if(key == GLFW_KEY_LEFT)
                    self.callback_key_("Left");
                else if (key == GLFW_KEY_RIGHT)
                    self.callback_key_("Right");
                else if (key == GLFW_KEY_UP)
                    self.callback_key_("Up");
                else if (key == GLFW_KEY_DOWN)
                    self.callback_key_("Down");
                else if (key == GLFW_KEY_ENTER)
                    self.callback_key_("Enter");
                else if (key == GLFW_KEY_ESCAPE)
                    self.callback_key_("Escape");
                else if (key == GLFW_KEY_TAB)
                    self.callback_key_("Tab");
                else if (key == GLFW_KEY_BACKSPACE)
                    self.callback_key_("Backspace");
                else if (
                    key == GLFW_KEY_RIGHT_CONTROL ||
                    key == GLFW_KEY_LEFT_CONTROL
                )
                    self.callback_key_("Control");
                else if (
                    key == GLFW_KEY_RIGHT_SHIFT ||
                    key == GLFW_KEY_LEFT_SHIFT
                )
                    self.callback_key_("Shift");
                else if (
                    key == GLFW_KEY_RIGHT_ALT ||
                    key == GLFW_KEY_LEFT_ALT
                )
                    self.callback_key_("Alt");
                else if (
                    0 <= key && key <= std::numeric_limits<char>::max()
                )
                    self.callback_key_(std::string(1, (char)key));
            }
        }
    );
    glfwSetMouseButtonCallback(
        window_,
        [](
            GLFWwindow * window,
            int button,
            int action,
            int mods
        )
        {
            (void)mods;
            if (action != GLFW_PRESS)
                return;
            auto & self = GLBACKEND_GLFW_SELF_(window);
            if (self.callback_button_)
                self.callback_button_(button);
        }
    );
    glfwSetScrollCallback(
        window_,
        [](
            GLFWwindow * window,
            double scroll_x,
            double scroll_y
        )
        {
            auto & self = GLBACKEND_GLFW_SELF_(window);
            self.scroll_ = {
                (float)scroll_x,
                (float)scroll_y,
            };
            if (self.callback_scroll_)
                self.callback_scroll_(self.scroll_);
        }
    );
    glfwSetCursorPosCallback(
        window_,
        [](
            GLFWwindow * window,
            double x,
            double y
        )
        {
            auto & self = GLBACKEND_GLFW_SELF_(window);
            self.move_ = {
                (float)x - self.position_[0],
                (float)y - self.position_[1],
            };
            self.position_ = {
                (float)x,
                (float)y,
            };
            if (self.callback_position_)
                self.callback_position_(self.position_);
            if (self.callback_move_)
                self.callback_move_(self.move_);
        }
    );
    glfwSetFramebufferSizeCallback(
        window_,
        [](
            GLFWwindow * window,
            int width,
            int height
        )
        {
            auto & self = GLBACKEND_GLFW_SELF_(window);
            self.size_ = {
                width,
                height,
            };
            if (self.callback_size_)
                self.callback_size_(self.size_);
        }
    );
}


GLBackendGLFW::GLBackendGLFW(GLBackendGLFW && other) noexcept
:
    GLBackend(std::move(other)),
    window_{other.window_}
{
    other.window_ = nullptr;
}


GLBackendGLFW::~GLBackendGLFW()
{
    destroy_();
}


void GLBackendGLFW::destroy_()
{
    if (window_)
    {
        lock(false);
        glfwDestroyWindow(window_);
    }
    if (--init_count_ == 0)
        glfwTerminate();
}


GLBASE_GLOBAL(GLBackendGLFW::init_count_, {0})


/// Debug


std::string GLBackendGLFW::debug_info() const
{
    auto ostream = std::ostringstream{};

    auto version_major = int{};
    auto version_minor = int{};
    auto version_rev   = int{};
    glfwGetVersion(
        &version_major,
        &version_minor,
        &version_rev
    );
    debug_info_(ostream, "GLFW", {
        {
            "VERSION_COMPILED",
                glfwGetVersionString()
        },
        {
            "VERSION_LINKED",
                std::to_string(version_major) + "." +
                std::to_string(version_minor) + "." +
                std::to_string(version_rev),
        },
    });

    ostream << GLBackend::debug_info();

    return ostream.str();
}


void GLBackendGLFW::debug_glfw_error_callback_(
    int          error,
    char const * message
)
{
    auto ostream = std::ostringstream{};
    ostream << std::hex << std::showbase;

    // https://www.glfw.org/docs/3.3/group__errors.html
    ostream << "GLFW error ";
    // NOLINTNEXTLINE
    #define GLBACKEND_GLFW_CALLBACK_CASE_(VALUE) \
        case GLFW_ ## VALUE: \
            ostream << #VALUE; \
            break;
    switch(error)
    {
        GLBACKEND_GLFW_CALLBACK_CASE_(NOT_INITIALIZED)
        GLBACKEND_GLFW_CALLBACK_CASE_(NO_CURRENT_CONTEXT)
        GLBACKEND_GLFW_CALLBACK_CASE_(INVALID_ENUM)
        GLBACKEND_GLFW_CALLBACK_CASE_(INVALID_VALUE)
        GLBACKEND_GLFW_CALLBACK_CASE_(OUT_OF_MEMORY)
        GLBACKEND_GLFW_CALLBACK_CASE_(API_UNAVAILABLE)
        GLBACKEND_GLFW_CALLBACK_CASE_(VERSION_UNAVAILABLE)
        GLBACKEND_GLFW_CALLBACK_CASE_(PLATFORM_ERROR)
        GLBACKEND_GLFW_CALLBACK_CASE_(FORMAT_UNAVAILABLE)
        GLBACKEND_GLFW_CALLBACK_CASE_(NO_WINDOW_CONTEXT)
        default:
            ostream << error;
    }
    ostream << ":\n";

    ostream << message;

    debug_callback()(ostream.str());
}


/// Guards


#endif
