#ifndef GLSHADER_SHADER_HPP_
#define GLSHADER_SHADER_HPP_


#include <map>
#include <set>
#include <string>
#include <unordered_map>

#include <GL/glew.h>


class Shader
{
public:

    using Paths = std::set<std::string>;

    explicit Shader(Paths const & paths);
    virtual ~Shader();
    Shader(Shader &&) noexcept;
    Shader(Shader const &) = delete;
    Shader & operator=(Shader &&) = delete;
    Shader & operator=(Shader const &) = delete;

    using Defines = std::map<std::string, std::string>;
    using Locations = std::map<std::string, GLuint>;

    static void root(std::string const & root);
    static void defines(Defines const & defines);
    static void verts(Locations const & verts);
    static void frags(Locations const & frags);

    GLuint program() const;

    Shader & validate();
    Shader & use();

    template<typename Value>
    Shader & uniform(
        std::string const & name,
        Value       const & value,
        bool                required = true
    );
    template<typename Value>
    Shader & uniform(
        std::string const & name,
        Value             * value,
        bool                required = true
    ) = delete;

    template<typename Value>
    static void uniform_buffer(
        std::string const & name,
        Value       const & value,
        bool                required = true
    );
    template<typename Value>
    static void uniform_buffer(
        std::string const & name,
        Value             * value,
        bool                required = true
    ) = delete;

protected:

    struct Uniform
    {
        GLint location;
        bool set;
    };

    struct UniformBuffer
    {
        GLuint buffer;
        GLsizeiptr size;
        GLuint binding;
        bool set;
    };

    struct UniformBlock
    {
        UniformBuffer & buffer;
    };

    void validate_() const;
    void current_(
        std::string const & error
    ) const;

    static void error_(
        std::string const & error,
        std::string const & error_hint = {}
    );

    Uniform * uniform_(
        std::string const & error,
        std::string const & name,
        bool                required
    );
    UniformBlock * uniform_block_(
        std::string const & error,
        std::string const & name,
        bool                required,
        GLsizeiptr          size
    );
    static UniformBuffer * uniform_buffer_(
        std::string const & error,
        std::string const & name,
        bool                required,
        GLsizeiptr          size
    );

    template<typename Value>
    using ByName = std::unordered_map<std::string, Value>;

    GLuint                       program_;
    std::string                  program_name_;
    std::string           static root_;
    Defines               static defines_;
    Locations             static verts_;
    Locations             static frags_;
    ByName<Uniform>              uniforms_;
    ByName<UniformBlock>         uniform_blocks_;
    ByName<UniformBuffer> static uniform_buffers_;
};


// Debug macros.

#ifndef NDEBUG
    #define GLSHADER_DEBUG_(...) __VA_ARGS__
    #define GLSHADER_DEBUG_ERROR_(...) \
        auto error = std::string{} + __VA_ARGS__;
#else
    #define GLSHADER_DEBUG_(...)
    #define GLSHADER_DEBUG_ERROR_(...) \
        auto error = std::string{};
#endif // NDEBUG


// Inline definitions.

#define GLSHADER_SET_(TYPE, NAME) \
    inline void Shader::NAME(TYPE const & NAME) \
    { \
        NAME##_ = NAME; \
    }
GLSHADER_SET_(std::string, root)
GLSHADER_SET_(Defines, defines)
GLSHADER_SET_(Locations, verts)
GLSHADER_SET_(Locations, frags)

inline GLuint Shader::program() const
{
    return program_;
}

inline Shader & Shader::validate()
{
    GLSHADER_DEBUG_(validate_();)
    return *this;
}

inline Shader & Shader::use()
{
    glUseProgram(program_);
    return *this;
}


// Uniform template definitions.

#define GLSHADER_UNIFORM_BUFFER_(BLOCK_OR_BUFFER, BUFFER, SET) \
    if (auto block_or_buffer = BLOCK_OR_BUFFER( \
        error, name, required, sizeof(value) \
    )) \
    { \
        glBindBuffer(GL_UNIFORM_BUFFER, block_or_buffer->BUFFER); \
        GLSHADER_DEBUG_(error_(error, "unprocessed previous error");) \
        glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(value), &value); \
        GLSHADER_DEBUG_(error_(error);) \
        GLSHADER_DEBUG_(block_or_buffer->SET = true;) \
    }

template<typename Value>
inline void Shader::uniform_buffer(
    std::string const & name,
    Value       const & value,
    bool                required
)
{
    GLSHADER_DEBUG_ERROR_(
        "Failed to set uniform buffer '" + name + "'"
    )
    GLSHADER_UNIFORM_BUFFER_(uniform_buffer_, buffer, set)
}

template<typename Value>
inline Shader & Shader::uniform(
    std::string const & name,
    Value       const & value,
    bool                required
)
{
    GLSHADER_DEBUG_ERROR_(
        "Failed to set uniform block '" + name + "' of " + program_name_
    )
    GLSHADER_DEBUG_(current_(error);)
    GLSHADER_UNIFORM_BUFFER_(uniform_block_, buffer.buffer, buffer.set)
    return *this;
}


// Uniform template specializations.

#define GLSHADER_UNIFORM_SIGNATURE_(TYPE) \
    template<> \
    inline Shader & Shader::uniform( \
        std::string const & name, \
        TYPE        const & value, \
        bool                required \
    )

#define GLSHADER_UNIFORM_DELETE(TYPE) \
    GLSHADER_UNIFORM_SIGNATURE_(TYPE) = delete;

#define GLSHADER_UNIFORM(TYPE, CODE) \
    GLSHADER_UNIFORM_SIGNATURE_(TYPE) \
    { \
        GLSHADER_DEBUG_ERROR_( \
            "Failed to set uniform '" + name + "' of " + program_name_ \
        ) \
        GLSHADER_DEBUG_(current_(error);) \
        if (auto * uniform = uniform_(error, name, required)) \
        { \
            GLint const & location = uniform->location; \
            GLSHADER_DEBUG_(error_(error, "unprocessed previous error");) \
            CODE \
            GLSHADER_DEBUG_(error_(error, "wrong type?");) \
            GLSHADER_DEBUG_(uniform->set = true;) \
        } \
        return *this; \
    }


// Uniform scalar template specializations.

#define GLSHADER_UNIFORM_SCALAR_(TYPE, SUFFIX) \
    GLSHADER_UNIFORM( \
        TYPE, \
        glUniform1##SUFFIX( \
            location, value \
        ); \
    )

GLSHADER_UNIFORM_SCALAR_(bool,      i)
GLSHADER_UNIFORM_SCALAR_(GLboolean, i)
GLSHADER_UNIFORM_SCALAR_(GLint,     i)
GLSHADER_UNIFORM_SCALAR_(GLuint,    ui)
GLSHADER_UNIFORM_SCALAR_(GLfloat,   f)

GLSHADER_UNIFORM_DELETE(GLdouble)


#endif // GLSHADER_SHADER_HPP_
