#ifndef GLSHADER_SHADER_HPP
#define GLSHADER_SHADER_HPP


#include <vector>
#include <string>
#include <unordered_map>

#include <GL/glew.h>
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>


class Shader {
public:

    Shader() : 
    program_                       {},
    paths_                         {},
    name_                          {},
    uniform_location_cache_        {},
    uniform_block_index_cache_     {}
    { 
    };

    
    Shader(std::vector<std::string> paths, std::string name = "");
    Shader(Shader &&);
    Shader(Shader const &);
    Shader & operator=(Shader &&);
    Shader & operator=(Shader const &);
    ~Shader();

    Shader & use();
    Shader & validate();
    template<typename Value>
    Shader & uniform(std::string const & name, Value const & value);
    Shader & uniform(std::string const & name, std::string const & buffer_name);
    template<typename Value>
    static void uniform_buffer(
        std::string const & name,
        Value const & value,
        GLenum usage = GL_DYNAMIC_DRAW
    );
    static void uniform_buffer_delete(std::string const & name);
    static void ubo_delete(std::string const & name);
    GLuint program(){return program_;}

private:

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

    struct UniformBuffer {
        GLuint buffer;
        GLsizeiptr size;
        GLenum usage;
        GLuint binding;
    };

    void new_();
    void delete_();
    void ensure_current_(
        std::string const & operation,
        std::string const & name = ""
    );
    GLint uniform_location_(std::string const & name);
    GLuint uniform_block_index_(std::string const & name);
    static GLuint uniform_buffer_(
        std::string const & name, GLsizeiptr size, GLenum usage
    );

    GLuint program_;
    std::vector<std::string> paths_;
    std::string name_;
    StringCache<GLint> uniform_location_cache_;
    StringCache<GLuint> uniform_block_index_cache_;
    static StringCache<UniformBuffer> uniform_buffer_cache_;
    static GLuint uniform_buffer_binding_next_;
};


#ifdef NDEBUG
    #define GLSHADER_ENSURE_CURRENT(OPERATION, NAME)
#else
    #define GLSHADER_ENSURE_CURRENT(OPERATION, NAME) \
        ensure_current_(OPERATION, NAME)
#endif


template<typename Value>
inline Shader & Shader::uniform(
    std::string const & name, Value const & value
) {
    GLSHADER_ENSURE_CURRENT("set uniform block", name);
    uniform_block_index_(name);
    uniform_buffer(name, value);
    uniform(name, name);
    return *this;
}


template<typename Value>
inline void Shader::uniform_buffer(
    std::string const & name, Value const & value, GLenum usage
) {
    glBindBuffer(
        GL_UNIFORM_BUFFER, uniform_buffer_(name, sizeof(value), usage)
    );
    glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(value), &value);
}


#define GLSHADER_UNIFORM(VALUE_TYPE, CODE) \
    template<> \
    inline Shader & Shader::uniform( \
        std::string const & name, VALUE_TYPE const & value \
    ) { \
        GLSHADER_ENSURE_CURRENT("set uniform", name); \
        CODE; \
        return *this; \
    }

#define GLSHADER_UNIFORM_SCALAR(VALUE_TYPE, GL_TYPE) \
    GLSHADER_UNIFORM( \
        VALUE_TYPE, \
        glUniform1##GL_TYPE( \
            uniform_location_(name), value \
        ) \
    )

#define GLSHADER_UNIFORM_N(N, GLM_VALUE_TYPE, GL_TYPE) \
    GLSHADER_UNIFORM( \
        glm::GLM_VALUE_TYPE##N, \
        glUniform##N##GL_TYPE##v( \
            uniform_location_(name), 1, glm::value_ptr(value) \
        ) \
    )

#define GLSHADER_UNIFORM_N_BOOL(N) \
    GLSHADER_UNIFORM( \
        glm::bvec##N, \
        GLint int_value[N]; \
        for (auto i = 0; i < N; ++i) \
            int_value[i] = value[i]; \
        glUniform##N##iv( \
            uniform_location_(name), 1, &int_value[0] \
        ) \
    )

#define GLSHADER_UNIFORM_MATRIX_N(N) \
    GLSHADER_UNIFORM( \
        glm::mat##N, \
        glUniformMatrix##N##fv( \
            uniform_location_(name), 1, GL_FALSE, glm::value_ptr(value) \
        ) \
    )

#define GLSHADER_UNIFORM_MATRIX_N_M(N, M) \
    GLSHADER_UNIFORM( \
        glm::mat##N##x##M, \
        glUniformMatrix##N##x##M##fv( \
            uniform_location_(name), 1, GL_FALSE, glm::value_ptr(value) \
        ) \
    )

GLSHADER_UNIFORM_SCALAR(bool, i)
GLSHADER_UNIFORM_SCALAR(int, i)
GLSHADER_UNIFORM_SCALAR(glm::uint, ui)
GLSHADER_UNIFORM_SCALAR(float, f)
GLSHADER_UNIFORM_SCALAR(double, f)

GLSHADER_UNIFORM_N_BOOL(2)
GLSHADER_UNIFORM_N_BOOL(3)
GLSHADER_UNIFORM_N_BOOL(4)
GLSHADER_UNIFORM_N(2, ivec, i)
GLSHADER_UNIFORM_N(3, ivec, i)
GLSHADER_UNIFORM_N(4, ivec, i)
GLSHADER_UNIFORM_N(2, uvec, ui)
GLSHADER_UNIFORM_N(3, uvec, ui)
GLSHADER_UNIFORM_N(4, uvec, ui)
GLSHADER_UNIFORM_N(2, vec, f)
GLSHADER_UNIFORM_N(3, vec, f)
GLSHADER_UNIFORM_N(4, vec, f)

GLSHADER_UNIFORM_MATRIX_N(2)
GLSHADER_UNIFORM_MATRIX_N(3)
GLSHADER_UNIFORM_MATRIX_N(4)

GLSHADER_UNIFORM_MATRIX_N_M(2, 3)
GLSHADER_UNIFORM_MATRIX_N_M(2, 4)
GLSHADER_UNIFORM_MATRIX_N_M(3, 2)
GLSHADER_UNIFORM_MATRIX_N_M(3, 4)
GLSHADER_UNIFORM_MATRIX_N_M(4, 2)
GLSHADER_UNIFORM_MATRIX_N_M(4, 3)


#endif // GLSHADER_SHADER_HPP
