#ifndef GLOBJECT_HPP_
#define GLOBJECT_HPP_


#include <array>
#include <functional>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>

#ifndef GLOBJECT_LOADER
#define GLOBJECT_LOADER <GL/glew.h>
#endif
// cppcheck-suppress preprocessorErrorDirective
#include GLOBJECT_LOADER


/// Getters and setters

#define GLOBJECT_GET_(TYPE, NAME, STATIC, CONST) \
    TYPE STATIC const & NAME() CONST \
    { \
        return NAME##_; \
    }

#define GLOBJECT_SET_(TYPE, NAME, STATIC, CONST) \
    template<typename Type> \
    TYPE STATIC NAME(Type && NAME) \
    { \
        auto NAME##_old = std::move         (NAME##_); \
        NAME##_         = std::forward<Type>(NAME); \
        return NAME##_old; \
    }

#define GLOBJECT_THREAD(NAME, INIT) \
    decltype(NAME) thread_local NAME INIT;

#define GLOBJECT_GET(       TYPE, NAME) GLOBJECT_GET_(TYPE, NAME,, const)
#define GLOBJECT_SET(       TYPE, NAME) GLOBJECT_SET_(TYPE, NAME,, const)
#define GLOBJECT_GET_THREAD(TYPE, NAME) GLOBJECT_GET_(TYPE, NAME, static,)
#define GLOBJECT_SET_THREAD(TYPE, NAME) GLOBJECT_SET_(TYPE, NAME, static,)

#define GLOBJECT_ACCESS(TYPE, NAME) \
    GLOBJECT_GET(TYPE, NAME) \
    GLOBJECT_SET(TYPE, NAME)

#define GLOBJECT_ACCESS_THREAD(TYPE, NAME) \
    GLOBJECT_GET_THREAD(TYPE, NAME) \
    GLOBJECT_SET_THREAD(TYPE, NAME)

/// Debug

#ifndef GLOBJECT_DEBUG
#define GLOBJECT_DEBUG 1
#endif

#if GLOBJECT_DEBUG
#define GLOBJECT_DEBUG_IF(D) if (GLObject::debug() >= D)
#else
#define GLOBJECT_DEBUG_IF(D) if (false)
#endif


/// Class

class GLObject
{
public:

    /// Core

    using Version = std::array<GLint, 2>;

    bool static supported(
        Version version_min,
        std::string const & extension = {}
    );

    GLint static get_integer(GLenum name);

    GLOBJECT_GET(GLuint, object)

    operator GLuint() const { return object(); }

    /// Data

    template<typename Data>
    struct DataTraits;

    /// Path

    using Path  = std::string;
    using Paths = std::vector<Path>;

    /// Debug

    using DebugCallback = std::function<void (std::string const & message)>;
    using DebugObjects  = std::vector<GLObject *>;

    GLOBJECT_ACCESS_THREAD(int,           debug)
    GLOBJECT_ACCESS_THREAD(DebugCallback, debug_callback)
    GLOBJECT_GET_THREAD   (DebugObjects,  debug_objects)

    std::string         debug_name() const;
    std::string virtual debug_info() const;
    std::string static  debug_objects_name();
    std::string static  debug_objects_info();

    /// Exceptions

    struct Exception : std::runtime_error
    {
        using std::runtime_error::runtime_error;
    };

protected:

    /// Special member functions

    using GLGenObjects    = void (*)(GLsizei n, GLuint       * objects);
    using GLDeleteObjects = void (*)(GLsizei n, GLuint const * objects);

    using GLCreateObject = GLuint (*)();
    using GLDeleteObject = void   (*)(GLuint);

    template<GLCreateObject gl_create_object>
    void static gl_create_object_(GLsizei n, GLuint * objects);

    template<GLDeleteObject gl_delete_object>
    void static gl_delete_object_(GLsizei n, GLuint * objects);

    explicit GLObject(
        GLGenObjects    gl_gen_objects,
        GLDeleteObjects gl_delete_objects,
        GLenum          object_type,
        std::string     object_label
    );
    virtual ~GLObject();
    GLObject(GLObject && other) noexcept;
    GLObject(GLObject const &) = delete;
    GLObject & operator=(GLObject &&) = delete;
    GLObject & operator=(GLObject const &) = delete;

    /// Path

    Path static path_prefix_(
        Path const & path,
        Path const & prefix
    );

    /// TGA

    class TGA
    {
    public:
        using Size = std::array<GLsizei, 2>;
        using Data = std::vector<GLubyte>;
        explicit TGA(Size size, Data data);
        TGA  static  read (Path const & path);
        void         write(Path const & path) const;
        Size         size() const;
        Data const & data();
    private:
        std::string static str_size_(Size size);
        void check_header_() const;
        void check_data_() const;
    private:
        // NOLINTNEXTLINE
        struct Header : std::array<GLubyte, 18>
        {
            explicit Header(Size size);
        };
        Header header_;
        Data   data_;
    };

    /// Check

    void static check_path_(Path const & path);
    void static check_error_(GLenum error);
    void static check_supported_(
        Version version_min,
        std::string const & extension = {}
    );
    void static check_format_(
        GLenum format,
        GLenum format_expected
    );
    void static check_type_(
        GLenum type,
        GLenum type_expected
    );
    void static check_internal_format_(
        GLenum internal_format
    );


    /// Fail

    [[noreturn]] void fail_action_(std::string const & action) const;

    /// String

    std::string static str_path_           (Path  const & path);
    std::string static str_paths_          (Paths const & paths);
    std::string static str_enum_           (GLenum name);
    std::string static str_error_          (GLenum error);
    std::string static str_format_         (GLenum format);
    std::string static str_type_           (GLenum type);
    std::string static str_internal_format_(GLenum internal_format);

private:

    /// Debug

    void debug_label_() const;

    void static debug_objects_push_back_(GLObject * debug_object);
    void static debug_objects_erase_    (GLObject * debug_object);

    /// String

    std::string static str_object_type_(GLenum object_type);

private:

    /// Special member functions

    GLDeleteObjects const gl_delete_objects_;

    /// Core

    auto static constexpr object_pseudo_ = (GLuint)-1;

    GLenum      const object_type_;
    std::string       object_label_;
    GLuint            object_;

    /// Debug

    int           static thread_local debug_;
    DebugCallback static thread_local debug_callback_;
    DebugObjects  static thread_local debug_objects_;
};


/// Special member functions

template<GLObject::GLCreateObject gl_create_object>
void GLObject::gl_create_object_(GLsizei n, GLuint * objects)
{
    for (auto i = GLsizei{0}; i < n; ++i)
        objects[i] = gl_create_object();
}

template<GLObject::GLDeleteObject gl_delete_object>
void GLObject::gl_delete_object_(GLsizei n, GLuint * objects)
{
    for (auto i = GLsizei{0}; i < n; ++i)
        gl_delete_object(objects[i]);
}

/// Data

#define GLOBJECT_DATA( \
    DATA, \
    V1A, \
    V2A, \
    V1U, \
    V2U, \
    COLUMNS, \
    ROWS, \
    ATTRIB, \
    UNIFORM, \
    SUFFIX, \
    FORMAT, \
    TYPE, \
    INTERNAL_FORMAT, \
    VALUE, \
    FOR_COLUMNS, \
    PLUS_COLUMN, \
    PLUS_COLUMN_TIMES_ROWS, \
    ... \
) \
    template<> \
    struct GLObject::DataTraits<DATA> \
    { \
        auto static constexpr name            =       #DATA; \
        auto static constexpr columns         = GLint {COLUMNS}; \
        auto static constexpr rows            = GLint {ROWS}; \
        auto static constexpr format          = GLenum{FORMAT}; \
        auto static constexpr type            = GLenum{TYPE}; \
        auto static constexpr internal_format = GLenum{INTERNAL_FORMAT}; \
        void static attrib(GLuint index, DATA const & value) \
        { \
            GLOBJECT_DEBUG_IF(1) \
                check_supported_({V1A, V2A}); \
            FOR_COLUMNS \
                glVertexAttrib##ATTRIB##SUFFIX( \
                    index PLUS_COLUMN, \
                    VALUE PLUS_COLUMN_TIMES_ROWS \
                ); \
        } \
        void static uniform(GLint location, DATA const & value) \
        { \
            GLOBJECT_DEBUG_IF(1) \
                check_supported_({V1U, V2U}); \
            glUniform##UNIFORM##SUFFIX( \
                location, \
                __VA_ARGS__ \
                VALUE \
            ); \
        } \
    };

//                                                                                                                                                                GLOBJECT_DATA(DATA         , V1A, V2A, V1U, V2U, COLUMNS, ROWS, ATTRIB, UNIFORM     , SUFFIX      , FORMAT, TYPE, INTERNAL_FORMAT , VALUE, FOR_COLUMNS                                              , PLUS_COLUMN, PLUS_COLUMN_TIMES_ROWS, ...          )
#define GLOBJECT_DATA_SCALAR(    DATA,      V1A, V2A, V1U, V2U, ATTRIB, SUFFIX, INTERNAL, TYPE                                                                  ) GLOBJECT_DATA(DATA         , V1A, V2A, V1U, V2U, 1      , 1   , ATTRIB,             , 1##SUFFIX   , GL_RED, TYPE, GL_R  ##INTERNAL, value,                                                          ,            ,                       ,              )
#define GLOBJECT_DATA_VECTOR_N(  DATA,      V1A, V2A, V1U, V2U, ATTRIB, SUFFIX, INTERNAL, TYPE,              VALUE,                            FORMAT,      N   ) GLOBJECT_DATA(DATA##N      , V1A, V2A, V1U, V2U, 1      , N   , ATTRIB,             , N##SUFFIX##v, FORMAT, TYPE, FORMAT##INTERNAL, VALUE,                                                          ,            ,                       , 1,           )
#define GLOBJECT_DATA_MATRIX_N(  DATA,      V1A, V2A, V1U, V2U, ATTRIB, SUFFIX, INTERNAL, TYPE,              VALUE,                 TRANSPOSE, FORMAT,      N   ) GLOBJECT_DATA(DATA##N      , V1A, V2A, V1U, V2U, N      , N   , ATTRIB, Matrix      , N##SUFFIX##v, FORMAT, TYPE, FORMAT##INTERNAL, VALUE, for (auto column = GLuint{0}; column < columns; ++column), + column   , + (column * rows)     , 1, TRANSPOSE,)
#define GLOBJECT_DATA_MATRIX_N_M(DATA,      V1A, V2A, V1U, V2U, ATTRIB, SUFFIX, INTERNAL, TYPE,              VALUE,                 TRANSPOSE, FORMAT,      N, M) GLOBJECT_DATA(DATA##N##x##M, V1A, V2A, V1U, V2U, N      , M   , ATTRIB, Matrix##N##x, M##SUFFIX##v, FORMAT, TYPE, FORMAT##INTERNAL, VALUE, for (auto column = GLuint{0}; column < columns; ++column), + column   , + (column * rows)     , 1, TRANSPOSE,)

#define GLOBJECT_DATA_VECTOR(    DATA,      V1A, V2A, V1U, V2U, ATTRIB, SUFFIX, INTERNAL, TYPE,              VALUE                                              ) \
    GLOBJECT_DATA_VECTOR_N(      DATA,      V1A, V2A, V1U, V2U, ATTRIB, SUFFIX, INTERNAL, TYPE,              VALUE,                            GL_RG,       2   ) \
    GLOBJECT_DATA_VECTOR_N(      DATA,      V1A, V2A, V1U, V2U, ATTRIB, SUFFIX, INTERNAL, TYPE,              VALUE,                            GL_RGB,      3   ) \
    GLOBJECT_DATA_VECTOR_N(      DATA,      V1A, V2A, V1U, V2U, ATTRIB, SUFFIX, INTERNAL, TYPE,              VALUE,                            GL_RGBA,     4   )

#define GLOBJECT_DATA_MATRIX(    DATA,                          ATTRIB, SUFFIX, INTERNAL, TYPE,              VALUE,                 TRANSPOSE                   ) \
    GLOBJECT_DATA_MATRIX_N(      DATA,      2,   0,   2,   0,   ATTRIB, SUFFIX, INTERNAL, TYPE,              VALUE,                 TRANSPOSE, GL_RG,       2   ) \
    GLOBJECT_DATA_MATRIX_N_M(    DATA,      2,   0,   2,   1,   ATTRIB, SUFFIX, INTERNAL, TYPE,              VALUE,                 TRANSPOSE, GL_RGB,      2, 3) \
    GLOBJECT_DATA_MATRIX_N_M(    DATA,      2,   0,   2,   1,   ATTRIB, SUFFIX, INTERNAL, TYPE,              VALUE,                 TRANSPOSE, GL_RGBA,     2, 4) \
    GLOBJECT_DATA_MATRIX_N_M(    DATA,      2,   0,   2,   1,   ATTRIB, SUFFIX, INTERNAL, TYPE,              VALUE,                 TRANSPOSE, GL_RG,       3, 2) \
    GLOBJECT_DATA_MATRIX_N(      DATA,      2,   0,   2,   0,   ATTRIB, SUFFIX, INTERNAL, TYPE,              VALUE,                 TRANSPOSE, GL_RGB,      3   ) \
    GLOBJECT_DATA_MATRIX_N_M(    DATA,      2,   0,   2,   1,   ATTRIB, SUFFIX, INTERNAL, TYPE,              VALUE,                 TRANSPOSE, GL_RGBA,     3, 4) \
    GLOBJECT_DATA_MATRIX_N_M(    DATA,      2,   0,   2,   1,   ATTRIB, SUFFIX, INTERNAL, TYPE,              VALUE,                 TRANSPOSE, GL_RG,       4, 2) \
    GLOBJECT_DATA_MATRIX_N_M(    DATA,      2,   0,   2,   1,   ATTRIB, SUFFIX, INTERNAL, TYPE,              VALUE,                 TRANSPOSE, GL_RGB,      4, 3) \
    GLOBJECT_DATA_MATRIX_N(      DATA,      2,   0,   2,   0,   ATTRIB, SUFFIX, INTERNAL, TYPE,              VALUE,                 TRANSPOSE, GL_RGBA,     4   )

GLOBJECT_DATA_SCALAR(            GLfloat,   2,   0,   2,   0,   ,       f,      32F,      GL_FLOAT                                                              )
GLOBJECT_DATA_SCALAR(            GLbyte,    3,   0,   2,   0,   I,      i,      8I,       GL_BYTE                                                               )
GLOBJECT_DATA_SCALAR(            GLshort,   3,   0,   2,   0,   I,      i,      16I,      GL_SHORT                                                              )
GLOBJECT_DATA_SCALAR(            GLint,     3,   0,   2,   0,   I,      i,      32I,      GL_INT                                                                )
GLOBJECT_DATA_SCALAR(            GLubyte,   3,   0,   3,   0,   I,      ui,     8UI,      GL_UNSIGNED_BYTE                                                      )
GLOBJECT_DATA_SCALAR(            GLushort,  3,   0,   3,   0,   I,      ui,     16UI,     GL_UNSIGNED_SHORT                                                     )
GLOBJECT_DATA_SCALAR(            GLuint,    3,   0,   3,   0,   I,      ui,     32UI,     GL_UNSIGNED_INT                                                       )

#ifdef GLM_VERSION
    #include <glm/gtc/type_ptr.hpp>
    GLOBJECT_DATA_VECTOR(        glm::vec,  2,   0,   2,   0,   ,       f,      32F,      GL_FLOAT,          glm::value_ptr(value)                              )
    GLOBJECT_DATA_VECTOR(        glm::ivec, 3,   0,   2,   0,   I,      i,      32I,      GL_INT,            glm::value_ptr(value)                              )
    GLOBJECT_DATA_VECTOR(        glm::uvec, 3,   0,   3,   0,   I,      ui,     32UI,     GL_UNSIGNED_INT,   glm::value_ptr(value)                              )
    GLOBJECT_DATA_MATRIX(        glm::mat,                      ,       f,      32F,      GL_FLOAT,          glm::value_ptr(value), GL_FALSE                    )
#endif


#endif // GLOBJECT_HPP_