#include <cstddef>
#include <iomanip>
#include <iostream>
#include <string>
#include <type_traits>

#include <glm/glm.hpp>

#include <glbase.hpp>
#include <gltraits.hpp>


struct GLTraitsTest : protected GLBase
{

    template<typename Value>
    void static test_value()
    {
        using Traits = GLTraits::Value<Value>;
        static_assert(
            std::is_empty<Traits>::value,
            "GLTraits::Value must be empty"
        );
        #define GLTRAITS_TEST_VALUE(NAME, VALUE) \
            << std::left << std::setw(35) << "  " NAME ":" << VALUE << "\n"
        #define GLTRAITS_TEST_VALUE_DEC(NAME) \
            GLTRAITS_TEST_VALUE(#NAME, std::dec << Traits::NAME)
        #define GLTRAITS_TEST_VALUE_BOOL(NAME) \
            GLTRAITS_TEST_VALUE(#NAME, std::boolalpha << Traits::NAME)
        #define GLTRAITS_TEST_VALUE_HEX(NAME) \
            GLTRAITS_TEST_VALUE(#NAME, str_enum_(Traits::NAME))
        #define GLTRAITS_TEST_VALUE_ENUM(NAME, SUFFIX) \
            GLTRAITS_TEST_VALUE( \
                #NAME #SUFFIX, \
                str_##NAME##_(Traits::NAME##SUFFIX) \
            )
        std::cout
            << "Value<" << Traits::name << ">" << "\n"
            GLTRAITS_TEST_VALUE_DEC(columns)
            GLTRAITS_TEST_VALUE_DEC(rows)
            GLTRAITS_TEST_VALUE_ENUM(glsl,)
            GLTRAITS_TEST_VALUE_ENUM(format,)
            GLTRAITS_TEST_VALUE_ENUM(type,)
            GLTRAITS_TEST_VALUE_ENUM(internal_format,)
            GLTRAITS_TEST_VALUE_ENUM(internal_format, _srgb)
            GLTRAITS_TEST_VALUE_ENUM(internal_format, _compressed)
            GLTRAITS_TEST_VALUE_ENUM(internal_format, _compressed_srgb)
            GLTRAITS_TEST_VALUE_BOOL(integer)
            GLTRAITS_TEST_VALUE_HEX(id);
    }

    template<GLenum id>
    void static test_value_id()
    {
        using Traits = GLTraits::ValueID<id>;
        static_assert(
            std::is_empty<Traits>::value,
            "GLTraits::ValueID must be empty"
        );
        std::cout
            << "ValueID<" << str_enum_(id) << ">" << "\n"
            << "  " << GLTraits::Value<typename Traits::Value>::name << "\n";
    }

    template<GLenum object_type, typename... Args>
    void static test_object()
    {
        using Traits = GLTraits::Object<object_type>;
        static_assert(
            std::is_empty<Traits>::value,
            "GLTraits::Object must be empty"
        );
        #define GLTRAITS_TEST_OBJECT(NAME) \
            << std::left << std::setw(33) \
            << "  " #NAME ":" \
            << (void *)Traits::NAME \
            << "\n"
        std::cout
            << "Object<" << Traits::name << ">" << "\n"
            GLTRAITS_TEST_OBJECT(template gen_objects<Args...>)
            GLTRAITS_TEST_OBJECT(delete_objects)
            GLTRAITS_TEST_OBJECT(info_log);
    }

    template<std::size_t N>
    void static test_texture()
    {
        using Traits = GLTraits::Texture<N>;
        static_assert(
            std::is_empty<Traits>::value,
            "GLTraits::Texture must be empty"
        );
        #define GLTRAITS_TEST_TEXTURE(NAME) \
            << std::left << std::setw(32) \
            << "  " #NAME ":" \
            << (void *)Traits::NAME \
            << "\n"
        std::cout
            << "Texture<" << N << ">" << "\n"
            GLTRAITS_TEST_TEXTURE(template tex_image<>)
            GLTRAITS_TEST_TEXTURE(template tex_storage<>)
            GLTRAITS_TEST_TEXTURE(template texture_storage<>)
            GLTRAITS_TEST_TEXTURE(template tex_sub_image<>)
            GLTRAITS_TEST_TEXTURE(template texture_sub_image<>)
            GLTRAITS_TEST_TEXTURE(copy_tex_sub_image)
            GLTRAITS_TEST_TEXTURE(copy_texture_sub_image)
            GLTRAITS_TEST_TEXTURE(compressed_tex_image)
            GLTRAITS_TEST_TEXTURE(compressed_tex_sub_image)
            GLTRAITS_TEST_TEXTURE(compressed_texture_sub_image);
    }

    #if __cplusplus >= 201402L
    template<
        typename    T,
        typename    Value,
        typename... Values
    >
    void static test_members(
        GLTraits::Members<T, Value, Values...>,
        std::size_t offset = 0
    )
    {
        auto unaligned = offset % alignof(Value);
        if (unaligned)
            offset += alignof(Value) - unaligned;
        auto end = offset + sizeof(Value);
        using Traits = GLTraits::Value<Value>;
        static_assert(
            std::is_empty<GLTraits::Members<
                T, Value, Values...
            >>::value,
            "GLTraits::Members must be empty"
        );
        #define GLTRAITS_TEST_MEMBERS(IND, NAME, OFFSET, END) \
            std::cout \
                << std::left  << std::setw(IND)           << "" \
                << std::left  << std::setw(12 - IND)      << NAME   << " " \
                << std::right << std::setw(2) << std::dec << OFFSET << " " \
                << std::right << std::setw(3) << std::dec << END    << "\n";
        GLTRAITS_TEST_MEMBERS(2, Traits::name, offset, end)
        test_members(GLTraits::Members<T, Values...>{}, end);
    }
    template<typename T>
    void static test_members(GLTraits::Members<T>, std::size_t)
    {}
    #endif

};


int main()
{
    GLTraitsTest::test_value<GLfloat>();
    GLTraitsTest::test_value<bool>();
    GLTraitsTest::test_value<GLshort>();
    GLTraitsTest::test_value<GLdouble>();
    GLTraitsTest::test_value<glm::mat4x3>();
    GLTraitsTest::test_value<glm::uvec2>();
    GLTraitsTest::test_value<glm::dvec2>();

    GLTraitsTest::test_value_id<GL_FLOAT>();
    GLTraitsTest::test_value_id<GL_BOOL>();
    GLTraitsTest::test_value_id<GL_SHORT>();
    GLTraitsTest::test_value_id<GL_DOUBLE>();
    GLTraitsTest::test_value_id<GL_FLOAT_MAT4x3>();
    GLTraitsTest::test_value_id<GL_UNSIGNED_INT_VEC2>();
    GLTraitsTest::test_value_id<GL_DOUBLE_VEC2>();

    GLTraitsTest::test_object<GL_SHADER, GLenum>();
    GLTraitsTest::test_object<GL_PROGRAM_PIPELINE>();

    GLTraitsTest::test_texture<1>();
    GLTraitsTest::test_texture<2>();
    GLTraitsTest::test_texture<3>();

    #if __cplusplus >= 201402L
    struct Vertex
    {
        glm::vec3  position;
        glm::vec2  tex_coord;
        glm::mat3  tbn;
        glm::ivec4 bone_indices;
        glm::vec4  bone_weights;
        GLubyte    flags;
        GLdouble   unaligned;
    };
    #define GLTRAITS_TEST_MEMBERS_T(T) \
        GLTRAITS_TEST_MEMBERS(0, #T, 0, sizeof(T))
    #define GLTRAITS_TEST_MEMBERS_OFFSETOF(T, MEMBER) \
        GLTRAITS_TEST_MEMBERS( \
            2, \
            GLTraits::Value<decltype(T::MEMBER)>::name, \
            offsetof(T, MEMBER), \
            offsetof(T, MEMBER) + sizeof(T::MEMBER) \
        )
    GLTRAITS_TEST_MEMBERS_T(Vertex)
    GLTRAITS_TEST_MEMBERS_OFFSETOF(Vertex, position)
    GLTRAITS_TEST_MEMBERS_OFFSETOF(Vertex, tex_coord)
    GLTRAITS_TEST_MEMBERS_OFFSETOF(Vertex, tbn)
    GLTRAITS_TEST_MEMBERS_OFFSETOF(Vertex, bone_indices)
    GLTRAITS_TEST_MEMBERS_OFFSETOF(Vertex, bone_weights)
    GLTRAITS_TEST_MEMBERS_OFFSETOF(Vertex, flags)
    GLTRAITS_TEST_MEMBERS_OFFSETOF(Vertex, unaligned)
    GLTRAITS_TEST_MEMBERS_T(Vertex)
    GLTraitsTest::test_members(GLTraits::members<Vertex>());
    #endif
}
