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

private:

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

    void new_();
    void delete_();
    void ensure_current_(
        std::string const & operation,
        std::string const & name = ""
    );
    GLint uniform_location_(std::string const & name);

    GLuint program_;
    std::vector<std::string> paths_;
    std::string name_;
    StringCache<GLint> uniform_location_cache_;
};


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


#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