/// Guards


#ifdef GLBACKEND_SDL


/// Includes


#include <glbackend_sdl.hpp>

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

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

#include <SDL2/SDL.h>
#include <SDL2/SDL_error.h>
#include <SDL2/SDL_events.h>
#include <SDL2/SDL_keycode.h>
#include <SDL2/SDL_version.h>

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


/// Constants


auto static constexpr sdl_subsystems_ =
    SDL_INIT_VIDEO  |
    SDL_INIT_EVENTS |
    SDL_INIT_TIMER;


/// Special member functions


GLBackendSDL::GLBackendSDL(
    std::string const & title,
    std::array<int, 2>  size,
    std::array<int, 2>  version,
    int                 samples,
    bool                fullscreen,
    bool                transparent
)
:
    GLBackend(),
    window_ {nullptr},
    context_{nullptr},
    running_{true},
    time_   {0.0F}
{
    // Backend init
    if (SDL_InitSubSystem(sdl_subsystems_) != 0)
        STR_THROW(
            "Failed to initialize SDL" << ":\n" <<
            SDL_GetError()
        );

    // Window options
    SDL_GL_ResetAttributes();
    auto window_flags = Uint32{0};
    window_flags |= SDL_WINDOW_OPENGL;
    if (fullscreen)
        window_flags |= SDL_WINDOW_FULLSCREEN;
    if (transparent)
    {
        window_flags |= SDL_WINDOW_BORDERLESS;
        window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
    }
    if (size[0] == 0 || size[1] == 0)
    {
        auto mode = SDL_DisplayMode{};
        SDL_GetCurrentDisplayMode(0, &mode);
        size[0] = mode.w;
        size[1] = mode.h;
    }
    size_ = size;

    // Context options
    auto context_flags = int{0};
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, SDL_TRUE);
    SDL_GL_SetAttribute(SDL_GL_RED_SIZE,      8); // NOLINT
    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE,    8); // NOLINT
    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE,     8); // NOLINT
    SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE,    8); // NOLINT
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE,   24); // NOLINT
    SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE,  8); // NOLINT
    if (samples)
    {
        SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
        SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, samples);
    }
    if (version[0])
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, version[0]);
    if (version[1])
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, version[1]);
    if ((version[0] == 3 && version[1] >= 2) || version[0] >= 4) // NOLINT
    {
        SDL_GL_SetAttribute(
            SDL_GL_CONTEXT_PROFILE_MASK,
            SDL_GL_CONTEXT_PROFILE_CORE
        );
        context_flags |= SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG; // NOLINT
    }
    if (debug() >= 1)
        context_flags |= SDL_GL_CONTEXT_DEBUG_FLAG; // NOLINT
    else if ((version[0] == 4 && version[1] >= 6) || version[0] >= 5) // NOLINT
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_NO_ERROR, SDL_TRUE);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, context_flags);

    try
    {
        // Window
        window_ = SDL_CreateWindow(
            title.c_str(),
            SDL_WINDOWPOS_UNDEFINED, // NOLINT
            SDL_WINDOWPOS_UNDEFINED, // NOLINT
            size[0],
            size[1],
            window_flags
        );
        if (!window_)
            STR_THROW(
                "Failed to create SDL window" << ":\n" <<
                SDL_GetError()
            );

        // Context
        context_ = SDL_GL_CreateContext(window_);
        if (!context_)
            STR_THROW(
                "Failed to create SDL context" << ":\n" <<
                SDL_GetError()
            );
        SDL_GL_MakeCurrent(window_, context_);
        if (debug() == 0)
            SDL_GL_SetSwapInterval(1);
        init_();

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


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


GLBackendSDL::GLBackendSDL(GLBackendSDL && other) noexcept
:
    GLBackend(std::move(other)),
    window_ {other.window_},
    context_{other.context_},
    running_{other.running_},
    time_   {other.time_}
{
    other.window_  = nullptr;
    other.context_ = nullptr;
}


void GLBackendSDL::destroy_()
{
    GLBackendSDL::lock(false);
    if (context_)
        SDL_GL_DeleteContext(context_);
    if (window_)
        SDL_DestroyWindow(window_);
    SDL_QuitSubSystem(sdl_subsystems_);
}


/// Render loop


inline void GLBackendSDL::events()
{
    auto event = SDL_Event{};
    while (SDL_PollEvent(&event))
    {
        switch (event.type)
        {
            case SDL_KEYDOWN:
                if (event.key.repeat)
                    break;
                if (callback_key_)
                {
                    auto key = event.key.keysym.sym;
                    if (key == SDLK_RETURN)
                        callback_key_("Enter");
                    else if (key == SDLK_LCTRL  || key == SDLK_RCTRL)
                        callback_key_("Control");
                    else if (key == SDLK_LSHIFT || key == SDLK_RSHIFT)
                        callback_key_("Shift");
                    else if (key == SDLK_LALT   || key == SDLK_RALT)
                        callback_key_("Alt");
                    else
                        callback_key_(SDL_GetKeyName(key));
                }
                break;
            case SDL_MOUSEBUTTONDOWN:
                if (callback_button_)
                    callback_button_(event.button.button);
                break;
            case SDL_MOUSEWHEEL:
                scroll_ = {
                    (float)event.wheel.x,
                    (float)event.wheel.y,
                };
                if (callback_scroll_)
                    callback_scroll_(scroll_);
                break;
            case SDL_MOUSEMOTION:
                position_ = {
                    (float)event.motion.x,
                    (float)event.motion.y,
                };
                move_ = {
                    (float)event.motion.xrel,
                    (float)event.motion.yrel,
                };
                if (callback_position_)
                    callback_position_(position_);
                if (callback_move_)
                    callback_move_(move_);
                break;
            case SDL_WINDOWEVENT:
                switch (event.window.event)
                {
                    case SDL_WINDOWEVENT_SIZE_CHANGED:
                        // size_ = {
                        //     event.window.data1,
                        //     event.window.data2,
                        // };
                        SDL_GL_GetDrawableSize(
                            window_,
                            &size_[0],
                            &size_[1]
                        );
                        if (callback_size_)
                            callback_size_(size_);
                        break;
                    case SDL_WINDOWEVENT_CLOSE:
                            running_ = false;
                        break;
                }
                break;
        }
    }
}


/// Debug


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

    auto version_compiled = SDL_version{};
    auto version_linked   = SDL_version{};
    SDL_VERSION(&version_compiled);
    SDL_GetVersion(&version_linked);
    debug_info_(ostream, "SDL", {
        {
            "VERSION_COMPILED",
                std::to_string(version_compiled.major) + "." +
                std::to_string(version_compiled.minor) + "." +
                std::to_string(version_compiled.patch),
        },
        {
            "VERSION_LINKED",
                std::to_string(version_linked.major) + "." +
                std::to_string(version_linked.minor) + "." +
                std::to_string(version_linked.patch),
        },
    });

    ostream << GLBackend::debug_info();

    return ostream.str();
}


/// Guards


#endif