#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
    ) = delete;

protected:

    struct Uniform
    {
        GLint location;
        bool set;
    };

    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
    );

    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_;
};


// 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 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; \
    }


#endif // GLSHADER_SHADER_HPP_
