/// Includes


#include <glbackend.hpp>

#include <algorithm>
#include <cstddef>
#include <exception>
#include <iomanip>
#include <regex>
#include <sstream>
#include <string>

#include <glbase.hpp>

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


/// Special member functions


GLBackend::GLBackend()
:
    callback_update_{},
    callback_render_{},
    scroll_{},
    position_{},
    move_{},
    size_{},
    callback_key_{},
    callback_button_{},
    callback_scroll_{},
    callback_position_{},
    callback_move_{},
    callback_size_{}
{
}


void GLBackend::init_()
{
    #ifdef __GLEW_H__
    if (auto error = glewInit())
        STR_THROW(
            "Failed to initialize GLEW:" << "\n" <<
            glewGetErrorString(error)
        );
    #endif

    if (debug() >= 1)
    {
        if (supported({4, 3}, "GL_KHR_debug"))
        {
            glEnable(GL_DEBUG_OUTPUT);
            glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
            glDebugMessageControl(
                GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE,
                0, nullptr,
                GL_TRUE
            );
            glDebugMessageControl(
                GL_DONT_CARE, GL_DONT_CARE, GL_DEBUG_SEVERITY_NOTIFICATION,
                0, nullptr,
                GL_FALSE
            );
            glDebugMessageCallback(debug_gl_message_callback_, nullptr);
        }
    }
}


/// Render loop


void GLBackend::run(float dt_fixed)
{
    auto t = time(0);
    running(true);
    while (events(), running())
    {
        if (callback_update_)
        {
            auto t_next = time();
            auto dt = t_next - t;
            if (dt_fixed != 0.0F)
                dt = dt_fixed;
            while (t + dt <= t_next)
            {
                callback_update_(t, dt, !(t + 2 * dt <= t_next));
                t += dt;
            }
        }
        if (callback_render_)
            callback_render_();
        swap();
    }
}


/// Path


GLBASE_GLOBAL(GLBackend::prefix_, {})


/// TGA


void GLBackend::tga_write(
    Path const & path
) const
{
    tga_().write(path_prefix_(path, prefix()));
}


bool GLBackend::tga_compare(
    Path const & path,
    bool         write_on_failed_read
) const
{
    auto path_prefix = path_prefix_(path, prefix());
    auto tga = tga_();
    try
    {
        auto tga_read = TGA_::read(path_prefix);
        return tga_read.data() == tga.data();
    }
    catch (std::exception const & exception)
    {
        if (!write_on_failed_read)
            throw;
        if (debug() >= 1)
            debug_callback()(exception.what());
        debug_callback()(STR("Writing TGA \"" << path_prefix << "\"."));
        tga.write(path_prefix);
        return false;
    }
}


GLBackend::TGA_ GLBackend::tga_() const
{
    glFlush();
    auto size = this->size();
    auto data = std::vector<GLubyte>((4 * (size_t)size[0] * (size_t)size[1]));
    glPixelStorei(GL_PACK_ALIGNMENT, 4);
    glReadPixels(
        0, 0,
        size[0], size[1],
        GL_BGRA, GL_UNSIGNED_BYTE,
        data.data()
    );
    return TGA_(size, std::move(data));
}


/// Debug


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

    #ifdef __GLEW_H__
    // NOLINTNEXTLINE
    #define GLBACKEND_INFO_GLEW_(NAME) \
        {#NAME, (char const *)glewGetString(GLEW_##NAME)}
    debug_info_(ostream, "GLEW", {
        GLBACKEND_INFO_GLEW_(VERSION),
    });
    #endif

    // NOLINTNEXTLINE
    #define GLBACKEND_INFO_GL_(NAME) \
        {#NAME, (char const *)glGetString(GL_##NAME)}
    // NOLINTNEXTLINE
    #define GLBACKEND_INFO_GL_FLAG_(NAME) \
        { \
            #NAME, \
            (flags & (GLuint)GL_CONTEXT_FLAG_##NAME##_BIT) \
                ? "TRUE" \
                : "FALSE" \
        }
    // NOLINTNEXTLINE
    #define GLBACKEND_INFO_GL_INTEGER_(NAME) \
        {#NAME, std::to_string(integer(GL_##NAME)) }
    auto const flags = (GLuint)integer(GL_CONTEXT_FLAGS);
    debug_info_(ostream, "OpenGL", {
        GLBACKEND_INFO_GL_(VENDOR),
        GLBACKEND_INFO_GL_(RENDERER),
        GLBACKEND_INFO_GL_(VERSION),
        GLBACKEND_INFO_GL_(SHADING_LANGUAGE_VERSION),
        GLBACKEND_INFO_GL_FLAG_(FORWARD_COMPATIBLE),
        GLBACKEND_INFO_GL_FLAG_(DEBUG),
        GLBACKEND_INFO_GL_FLAG_(ROBUST_ACCESS),
        GLBACKEND_INFO_GL_FLAG_(NO_ERROR),
        GLBACKEND_INFO_GL_INTEGER_(SAMPLE_BUFFERS),
        GLBACKEND_INFO_GL_INTEGER_(SAMPLES),
    });

    return ostream.str();
}


void GLBackend::debug_info_(
    std::ostream                                           & ostream,
    std::string                                      const & category,
    std::vector<std::pair<std::string, std::string>> const & values,
    char                                                     fill
)
{
    auto length_max = int{0};
    for (auto const & value : values)
        length_max = std::max(length_max, (int)value.first.length());
    ostream << category << "\n";
    for (auto const & value : values)
        ostream
            << "  "
            << std::left << std::setw(length_max + 1 + 2) << std::setfill(fill)
            << (value.first + " ")
            << (" " + value.second)
            << "\n";
}


void GLAPIENTRY GLBackend::debug_gl_message_callback_(
    GLenum          source,
    GLenum          type,
    GLuint          id,
    GLenum          severity,
    GLsizei         length,
    GLchar  const * message,
    void    const * user_param
)
{
    (void)length;
    (void)user_param;

    auto ostream = std::ostringstream{};
    ostream << std::hex << std::showbase;

    // https://www.khronos.org/opengl/wiki/Debug_Output#Message_Components
    ostream << "GL debug message ";
    // NOLINTNEXTLINE
    #define GLBACKEND_CALLBACK_CASE_(CATEGORY, VALUE) \
        case GL_DEBUG_##CATEGORY##_##VALUE: \
            ostream << #VALUE; \
            break;
    switch(source)
    {
        GLBACKEND_CALLBACK_CASE_(SOURCE, API)
        GLBACKEND_CALLBACK_CASE_(SOURCE, WINDOW_SYSTEM)
        GLBACKEND_CALLBACK_CASE_(SOURCE, SHADER_COMPILER)
        GLBACKEND_CALLBACK_CASE_(SOURCE, THIRD_PARTY)
        GLBACKEND_CALLBACK_CASE_(SOURCE, APPLICATION)
        GLBACKEND_CALLBACK_CASE_(SOURCE, OTHER)
        default:
            ostream << source;
    }
    ostream << " ";
    switch(type)
    {
        GLBACKEND_CALLBACK_CASE_(TYPE, ERROR)
        GLBACKEND_CALLBACK_CASE_(TYPE, DEPRECATED_BEHAVIOR)
        GLBACKEND_CALLBACK_CASE_(TYPE, UNDEFINED_BEHAVIOR)
        GLBACKEND_CALLBACK_CASE_(TYPE, PORTABILITY)
        GLBACKEND_CALLBACK_CASE_(TYPE, PERFORMANCE)
        GLBACKEND_CALLBACK_CASE_(TYPE, MARKER)
        GLBACKEND_CALLBACK_CASE_(TYPE, PUSH_GROUP)
        GLBACKEND_CALLBACK_CASE_(TYPE, POP_GROUP)
        GLBACKEND_CALLBACK_CASE_(TYPE, OTHER)
        default:
            ostream << type;
    }
    ostream << " ";
    switch(severity)
    {
        GLBACKEND_CALLBACK_CASE_(SEVERITY, HIGH)
        GLBACKEND_CALLBACK_CASE_(SEVERITY, MEDIUM)
        GLBACKEND_CALLBACK_CASE_(SEVERITY, LOW)
        GLBACKEND_CALLBACK_CASE_(SEVERITY, NOTIFICATION)
        default:
            ostream << severity;
    }
    ostream << " ";
    ostream << id;
    ostream << ":\n";

    ostream << debug_gl_message_(message);

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


std::string GLBackend::debug_gl_message_(std::string message)
{
    message.erase(message.find_last_not_of(" \n") + 1);

    auto static const file_line = std::regex{R""(^"([^"]+)"(:[0-9]+.*))""};
    auto match = std::smatch{};
    if (std::regex_match(message, match, file_line))
        message = match.str(1) + match.str(2);

    return message;
}