#ifndef GLW_PROGRAM_H
#define GLW_PROGRAM_H

#include <memory.h>

#include <string>
#include <vector>
#include <map>

#include "./vertexshader.h"
#include "./geometryshader.h"
#include "./fragmentshader.h"

namespace glw
{

typedef std::vector<ShaderHandle> ShaderHandleVector;

class VertexAttributeBinding
{
    public:

        typedef void                   BaseType;
        typedef VertexAttributeBinding ThisType;

        typedef std::map<std::string, GLuint> Map;
        typedef Map::const_iterator           ConstIterator;
        typedef Map::iterator                 Iterator;
        typedef Map::value_type               Value;

        Map bindings;

        VertexAttributeBinding(void)
        {
            this->clear();
        }

        void clear(void)
        {
            this->bindings.clear();
        }

        GLuint operator [] (const std::string & attributeName) const
        {
            return this->bindings.find(attributeName)->second;
        }

        GLuint & operator [] (const std::string & attributeName)
        {
            return this->bindings[attributeName];
        }
};

class GeometryStage
{
    public:

        typedef void          BaseType;
        typedef GeometryStage ThisType;

        /*
        GLenum inputPrimitiveType;
        GLenum outputPrimitiveType;
        GLint  maxOutputVertices;
        */

        GeometryStage(void)
        {
            this->clear();
        }

        void clear(void)
        {
            /*
            this->inputPrimitiveType  = GLW_DONT_CARE;
            this->outputPrimitiveType = GLW_DONT_CARE;
            this->maxOutputVertices   = GLW_DONT_CARE;
            */
        }
};

class TransformFeedbackStream
{
    public:

        typedef void                    BaseType;
        typedef TransformFeedbackStream ThisType;

        typedef std::vector<std::string> VaryingVector;

        VaryingVector varyings;
        GLenum        bufferMode;

        TransformFeedbackStream(void)
        {
            this->clear();
        }

        void clear(void)
        {
            this->varyings.clear();
            this->bufferMode = GL_INTERLEAVED_ATTRIBS;
        }
};

class RasterizerSettings
{
    public:

        typedef void               BaseType;
        typedef RasterizerSettings ThisType;

        enum RasterizerExecution
        {
            DontCare = 0,
            Autodetect,
            ForceEnabled,
            ForceDisabled
        };

        // TODO
        //RasterizerExecution execution;

        RasterizerSettings(void)
        {
            this->clear();
        }

        void clear(void)
        {
            //this->execution = ThisType::Autodetect;
        }
};

class FragmentOutputBinding
{
    public:

        typedef void                  BaseType;
        typedef FragmentOutputBinding ThisType;

        typedef std::map<std::string, GLuint> Map;
        typedef Map::const_iterator           ConstIterator;
        typedef Map::iterator                 Iterator;
        typedef Map::value_type               Value;

        Map bindings;

        FragmentOutputBinding(void)
        {
            this->clear();
        }

        void clear(void)
        {
            this->bindings.clear();
        }

        GLuint operator [] (const std::string & outName) const
        {
            return this->bindings.find(outName)->second;
        }

        GLuint & operator [] (const std::string & outName)
        {
            return this->bindings[outName];
        }
};

class ProgramArguments : public ObjectArguments
{
    public:

        typedef ObjectArguments    BaseType;
        typedef ProgramArguments   ThisType;

        ShaderHandleVector         shaders;
        VertexAttributeBinding     vertexInputs;
        GeometryStage              geometryStage;
        TransformFeedbackStream    feedbackStream;
        RasterizerSettings         rasterSettings;
        FragmentOutputBinding      fragmentOutputs;

        ProgramArguments(void)
            : BaseType()
        {
            ;
        }

        void clear(void)
        {
            BaseType::clear();
            this->shaders         .clear();
            this->vertexInputs    .clear();
            this->geometryStage   .clear();
            this->feedbackStream  .clear();
            this->rasterSettings  .clear();
            this->fragmentOutputs .clear();
        }
};

class Program : public Object
{
    friend class Context;

    public:

        typedef Object  BaseType;
        typedef Program ThisType;

        virtual ~Program(void)
        {
            this->destroy();
        }

        virtual Type type(void) const
        {
            return ProgramType;
        }

        const ProgramArguments & arguments(void) const
        {
            return this->m_arguments;
        }

        const std::string & log(void) const
        {
            return this->m_log;
        }

        const std::string & fullLog(void) const
        {
            return this->m_fullLog;
        }

        bool isLinked(void) const
        {
            return this->m_linked;
        }

        GLint getUniformLocation(const std::string & name) const
        {
#if GLW_ASSERT_UNIFORM_LOCATION
            GLW_ASSERT(this->m_uniforms.count(name) > 0);
#endif
            UniformMapConstIterator it = this->m_uniforms.find(name);
            if (it == this->m_uniforms.end()) return -1;
            return it->second.location;
        }

#define _GLW_IMPLEMENT_SCALAR_UNIFORM_(TYPE, FUNCION_SUFFIX) \
    void setUniform    (const std::string & name, TYPE x                                                       ) { glUniform1         ## FUNCION_SUFFIX     (this->getUniformLocation(name),                                       x         ); } \
    void setUniform    (const std::string & name, TYPE x, TYPE y                                               ) { glUniform2         ## FUNCION_SUFFIX     (this->getUniformLocation(name),                                       x, y      ); } \
    void setUniform    (const std::string & name, TYPE x, TYPE y, TYPE z                                       ) { glUniform3         ## FUNCION_SUFFIX     (this->getUniformLocation(name),                                       x, y, z   ); } \
    void setUniform    (const std::string & name, TYPE x, TYPE y, TYPE z, TYPE w                               ) { glUniform4         ## FUNCION_SUFFIX     (this->getUniformLocation(name),                                       x, y, z, w); }

#define _GLW_IMPLEMENT_VECTOR_UNIFORM_(TYPE, FUNCION_SUFFIX) \
    void setUniform1   (const std::string & name, const TYPE * v,                                 int count = 1) { glUniform1         ## FUNCION_SUFFIX ## v (this->getUniformLocation(name), GLsizei(count),                       v         ); } \
    void setUniform2   (const std::string & name, const TYPE * v,                                 int count = 1) { glUniform2         ## FUNCION_SUFFIX ## v (this->getUniformLocation(name), GLsizei(count),                       v         ); } \
    void setUniform3   (const std::string & name, const TYPE * v,                                 int count = 1) { glUniform3         ## FUNCION_SUFFIX ## v (this->getUniformLocation(name), GLsizei(count),                       v         ); } \
    void setUniform4   (const std::string & name, const TYPE * v,                                 int count = 1) { glUniform4         ## FUNCION_SUFFIX ## v (this->getUniformLocation(name), GLsizei(count),                       v         ); }

#define _GLW_IMPLEMENT_MATRIX_UNIFORM_(TYPE, FUNCION_SUFFIX) \
    void setUniform2x2 (const std::string & name, const TYPE * m,                 bool transpose, int count = 1) { glUniformMatrix2   ## FUNCION_SUFFIX ## v (this->getUniformLocation(name), GLsizei(count), GLboolean(transpose), m         ); } \
    void setUniform2x3 (const std::string & name, const TYPE * m,                 bool transpose, int count = 1) { glUniformMatrix2x3 ## FUNCION_SUFFIX ## v (this->getUniformLocation(name), GLsizei(count), GLboolean(transpose), m         ); } \
    void setUniform2x4 (const std::string & name, const TYPE * m,                 bool transpose, int count = 1) { glUniformMatrix2x4 ## FUNCION_SUFFIX ## v (this->getUniformLocation(name), GLsizei(count), GLboolean(transpose), m         ); } \
    void setUniform3x2 (const std::string & name, const TYPE * m,                 bool transpose, int count = 1) { glUniformMatrix3x2 ## FUNCION_SUFFIX ## v (this->getUniformLocation(name), GLsizei(count), GLboolean(transpose), m         ); } \
    void setUniform3x3 (const std::string & name, const TYPE * m,                 bool transpose, int count = 1) { glUniformMatrix3   ## FUNCION_SUFFIX ## v (this->getUniformLocation(name), GLsizei(count), GLboolean(transpose), m         ); } \
    void setUniform3x4 (const std::string & name, const TYPE * m,                 bool transpose, int count = 1) { glUniformMatrix3x4 ## FUNCION_SUFFIX ## v (this->getUniformLocation(name), GLsizei(count), GLboolean(transpose), m         ); } \
    void setUniform4x2 (const std::string & name, const TYPE * m,                 bool transpose, int count = 1) { glUniformMatrix4x2 ## FUNCION_SUFFIX ## v (this->getUniformLocation(name), GLsizei(count), GLboolean(transpose), m         ); } \
    void setUniform4x3 (const std::string & name, const TYPE * m,                 bool transpose, int count = 1) { glUniformMatrix4x3 ## FUNCION_SUFFIX ## v (this->getUniformLocation(name), GLsizei(count), GLboolean(transpose), m         ); } \
    void setUniform4x4 (const std::string & name, const TYPE * m,                 bool transpose, int count = 1) { glUniformMatrix4   ## FUNCION_SUFFIX ## v (this->getUniformLocation(name), GLsizei(count), GLboolean(transpose), m         ); }

        _GLW_IMPLEMENT_SCALAR_UNIFORM_(int,          i )
        _GLW_IMPLEMENT_SCALAR_UNIFORM_(unsigned int, ui)
        _GLW_IMPLEMENT_SCALAR_UNIFORM_(float,        f )
//        _GLW_IMPLEMENT_SCALAR_UNIFORM_(double,       d )
        _GLW_IMPLEMENT_VECTOR_UNIFORM_(int,          i )
        _GLW_IMPLEMENT_VECTOR_UNIFORM_(unsigned int, ui)
        _GLW_IMPLEMENT_VECTOR_UNIFORM_(float,        f )
//        _GLW_IMPLEMENT_VECTOR_UNIFORM_(double,       d )
        _GLW_IMPLEMENT_MATRIX_UNIFORM_(float,        f )
//        _GLW_IMPLEMENT_MATRIX_UNIFORM_(double,       d )

        GLW_IMPLEMENT_CUSTOM_UNIFORMS;

#undef _GLW_IMPLEMENT_SCALAR_UNIFORM_
#undef _GLW_IMPLEMENT_VECTOR_UNIFORM_
#undef _GLW_IMPLEMENT_MATRIX_UNIFORM_

    protected:

        Program(Context * ctx)
            : BaseType (ctx)
            , m_linked (false)
        {
            ;
        }

        bool create(const ProgramArguments & args)
        {
            this->destroy();

            this->m_arguments = args;

            GLint boundName = 0;
            glGetIntegerv(GL_CURRENT_PROGRAM, &boundName);

            this->m_name = glCreateProgram();
            this->m_fullLog = "";

            // shaders
            {
                for (size_t i=0; i<this->m_arguments.shaders.size(); ++i)
                {
                    const ShaderHandle & shader = this->m_arguments.shaders[i];
                    if (!shader) continue;
                    this->m_fullLog += shader->log();
                    if (!shader->isCompiled()) continue;
                    glAttachShader(this->m_name, shader->name());
                }
            }

            // vertex
            {
                for (VertexAttributeBinding::ConstIterator it=this->m_arguments.vertexInputs.bindings.begin(); it!=this->m_arguments.vertexInputs.bindings.end(); ++it)
                {
                    glBindAttribLocation(this->m_name, it->second, it->first.c_str());
                }
            }

            // geometry
            {
                ;
            }

            // transform feedback
            {
                const size_t count = this->m_arguments.feedbackStream.varyings.size();
                if (count > 0)
                {
                    const char ** varyings = new const char * [count];
                    for (size_t i=0; i<count; ++i)
                    {
                        varyings[i]  = this->m_arguments.feedbackStream.varyings[i].c_str();
                    }
                    glTransformFeedbackVaryings(this->m_name, GLsizei(count), varyings, this->m_arguments.feedbackStream.bufferMode);
                    delete [] varyings;
                }
            }

            // TODO
            // rasterizer
            {
                ;
            }

            // fragment
            {
                for (FragmentOutputBinding::ConstIterator it=this->m_arguments.fragmentOutputs.bindings.begin(); it!=this->m_arguments.fragmentOutputs.bindings.end(); ++it)
                {
                    glBindFragDataLocation(this->m_name, it->second, it->first.c_str());
                }
            }

            glLinkProgram(this->m_name);

            GLint linkStatus = 0;
            glGetProgramiv(this->m_name, GL_LINK_STATUS, &linkStatus);

            this->m_log      = ThisType::getInfoLog(this->m_name);
            this->m_fullLog += this->m_log;
            this->m_linked   = (linkStatus != GL_FALSE);

#if GLW_PRINT_LOG_TO_STDERR
            std::cerr << "---------------------------" << std::endl;
            std::cerr << "[Program Link Log]: " << ((this->m_linked) ? ("OK") : ("FAILED")) << std::endl;
            std::cerr << this->m_log << std::endl;
            std::cerr << "---------------------------" << std::endl;
#endif

            if (this->m_linked)
            {
                this->postLink();
            }

            glUseProgram(boundName);

            return this->m_linked;
        }

        virtual void doDestroy()
        {
            glDeleteProgram(this->m_name);
            this->m_arguments.clear();
            this->m_log.clear();
            this->m_fullLog.clear();
            this->m_linked = false;
        }

        virtual bool doIsValid(void) const
        {
            return this->m_linked;
        }

    private:

        class UniformInfo
        {
            public:

                typedef void        BaseType;
                typedef UniformInfo ThisType;

                std::string name;
                GLint       location;
                GLenum      type;
                GLint       size;

                UniformInfo(void)
                    : location (-1)
                    , type     (GL_NONE)
                    , size     (0)
                {
                    ;
                }
        };

        typedef std::map<std::string, UniformInfo> UniformMap;
        typedef UniformMap::const_iterator         UniformMapConstIterator;
        typedef UniformMap::iterator               UniformMapIterator;
        typedef UniformMap::value_type             UniformMapValue;

        ProgramArguments m_arguments;
        UniformMap       m_uniforms;
        std::string      m_log;
        std::string      m_fullLog;
        bool             m_linked;

        static std::string getInfoLog(GLuint Program)
        {
            std::string log;
            GLint logLen = 0;
            glGetProgramiv(Program, GL_INFO_LOG_LENGTH, &logLen);
            if (logLen > 0)
            {
                char * sLog = new char[logLen + 1];
                glGetProgramInfoLog(Program, logLen, &logLen, sLog);
                if (logLen > 0)
                {
                    if (sLog[0] != '\0')
                    {
                        sLog[logLen - 1] = '\0';
                        log = sLog;
                    }
                }
                delete [] sLog;
            }
            return log;
        }

        void setupUniforms(void)
        {
            this->m_uniforms.clear();

            GLint ucount = 0;
            glGetProgramiv(this->m_name,  GL_ACTIVE_UNIFORMS, &ucount);
            if (ucount <= 0) return;

            GLint ulen = 0;
            glGetProgramiv(this->m_name, GL_ACTIVE_UNIFORM_MAX_LENGTH, &ulen);
            ulen++; // according to specs, +1 (for null) is already accounted, but some implementations are broken.
            if (ulen <= 0) return;

            UniformInfo info;
            GLchar * uname = new GLchar [ulen + 1];
            for (int i=0; i<int(ucount); ++i)
            {
                GLsizei length = 0;
                glGetActiveUniform(this->m_name, GLuint(i), GLsizei(ulen), &length, &(info.size), &(info.type), uname);
                info.name     = uname;
                info.location = glGetUniformLocation(this->m_name, uname);
                this->m_uniforms.insert(UniformMapValue(info.name, info));
            }
            delete [] uname;
        }

        void postLink(void)
        {
            this->setupUniforms();
        }
};

namespace detail { template <> struct BaseOf <Program> { typedef Object Type; }; };
typedef   detail::ObjectSharedPointerTraits  <Program> ::Type ProgramPtr;

class SafeProgram : public SafeObject
{
    friend class Context;
    friend class BoundProgram;

    public:

        typedef SafeObject  BaseType;
        typedef SafeProgram ThisType;

        const ProgramArguments & arguments(void) const
        {
            return this->object()->arguments();
        }

        const std::string & log(void) const
        {
            return this->object()->log();
        }

        const std::string & fullLog(void) const
        {
            return this->object()->fullLog();
        }

        bool isLinked(void) const
        {
            return this->object()->isLinked();
        }

    protected:

        SafeProgram(const ProgramPtr & program)
            : BaseType(program)
        {
            ;
        }

        const ProgramPtr & object(void) const
        {
            return static_cast<const ProgramPtr &>(BaseType::object());
        }

        ProgramPtr & object(void)
        {
            return static_cast<ProgramPtr &>(BaseType::object());
        }
};

namespace detail { template <> struct BaseOf     <SafeProgram> { typedef SafeObject Type; }; };
namespace detail { template <> struct ObjectBase <SafeProgram> { typedef Program     Type; }; };
namespace detail { template <> struct ObjectSafe <Program    > { typedef SafeProgram Type; }; };
typedef   detail::ObjectSharedPointerTraits      <SafeProgram> ::Type ProgramHandle;

class ProgramBindingParams : public ObjectBindingParams
{
    public:

        typedef ObjectBindingParams BaseType;
        typedef ProgramBindingParams ThisType;

        ProgramBindingParams(void)
            : BaseType(GL_CURRENT_PROGRAM, 0)
        {
            ;
        }
};

class BoundProgram : public BoundObject
{
    friend class Context;

    public:

        typedef BoundObject BaseType;
        typedef BoundProgram ThisType;

        BoundProgram(void)
            : BaseType()
        {
            ;
        }

        const ProgramHandle & handle(void) const
        {
            return static_cast<const ProgramHandle &>(BaseType::handle());
        }

        ProgramHandle & handle(void)
        {
            return static_cast<ProgramHandle &>(BaseType::handle());
        }

#define _GLW_FORWARD_SCALAR_UNIFORM_(TYPE) \
    void setUniform    (const std::string & name, TYPE x                                                       ) { this->object()->setUniform(name, x         ); } \
    void setUniform    (const std::string & name, TYPE x, TYPE y                                               ) { this->object()->setUniform(name, x, y      ); } \
    void setUniform    (const std::string & name, TYPE x, TYPE y, TYPE z                                       ) { this->object()->setUniform(name, x, y, z   ); } \
    void setUniform    (const std::string & name, TYPE x, TYPE y, TYPE z, TYPE w                               ) { this->object()->setUniform(name, x, y, z, w); }

#define _GLW_FORWARD_VECTOR_UNIFORM_(TYPE) \
    void setUniform1   (const std::string & name, const TYPE * v,                                 int count = 1) { this->object()->setUniform1(name, v, count); } \
    void setUniform2   (const std::string & name, const TYPE * v,                                 int count = 1) { this->object()->setUniform2(name, v, count); } \
    void setUniform3   (const std::string & name, const TYPE * v,                                 int count = 1) { this->object()->setUniform3(name, v, count); } \
    void setUniform4   (const std::string & name, const TYPE * v,                                 int count = 1) { this->object()->setUniform4(name, v, count); }

#define _GLW_FORWARD_MATRIX_UNIFORM_(TYPE) \
    void setUniform2x2 (const std::string & name, const TYPE * m,                 bool transpose, int count = 1) { this->object()->setUniform2x2(name, m, transpose, count); } \
    void setUniform2x3 (const std::string & name, const TYPE * m,                 bool transpose, int count = 1) { this->object()->setUniform2x3(name, m, transpose, count); } \
    void setUniform2x4 (const std::string & name, const TYPE * m,                 bool transpose, int count = 1) { this->object()->setUniform2x4(name, m, transpose, count); } \
    void setUniform3x2 (const std::string & name, const TYPE * m,                 bool transpose, int count = 1) { this->object()->setUniform3x2(name, m, transpose, count); } \
    void setUniform3x3 (const std::string & name, const TYPE * m,                 bool transpose, int count = 1) { this->object()->setUniform3x3(name, m, transpose, count); } \
    void setUniform3x4 (const std::string & name, const TYPE * m,                 bool transpose, int count = 1) { this->object()->setUniform3x4(name, m, transpose, count); } \
    void setUniform4x2 (const std::string & name, const TYPE * m,                 bool transpose, int count = 1) { this->object()->setUniform4x2(name, m, transpose, count); } \
    void setUniform4x3 (const std::string & name, const TYPE * m,                 bool transpose, int count = 1) { this->object()->setUniform4x3(name, m, transpose, count); } \
    void setUniform4x4 (const std::string & name, const TYPE * m,                 bool transpose, int count = 1) { this->object()->setUniform4x4(name, m, transpose, count); }

        _GLW_FORWARD_SCALAR_UNIFORM_(int)
        _GLW_FORWARD_SCALAR_UNIFORM_(unsigned int)
        _GLW_FORWARD_SCALAR_UNIFORM_(float)
//        _GLW_FORWARD_SCALAR_UNIFORM_(double)
        _GLW_FORWARD_VECTOR_UNIFORM_(int)
        _GLW_FORWARD_VECTOR_UNIFORM_(unsigned int)
        _GLW_FORWARD_VECTOR_UNIFORM_(float)
//        _GLW_FORWARD_VECTOR_UNIFORM_(double)
        _GLW_FORWARD_MATRIX_UNIFORM_(float)
//        _GLW_FORWARD_MATRIX_UNIFORM_(double)

#undef _GLW_FORWARD_SCALAR_UNIFORM_
#undef _GLW_FORWARD_VECTOR_UNIFORM_
#undef _GLW_FORWARD_MATRIX_UNIFORM_

    protected:

        BoundProgram(const ProgramHandle & handle, const ProgramBindingParams & params)
            : BaseType(handle, params)
        {
            ;
        }

        const ProgramPtr & object(void) const
        {
            return this->handle()->object();
        }

        ProgramPtr & object(void)
        {
            return this->handle()->object();
        }

        virtual void bind(void)
        {
            glUseProgram(this->object()->name());
        }

        virtual void unbind(void)
        {
            glUseProgram(0);
        }
};

namespace detail { template <> struct ParamsOf    <BoundProgram> { typedef ProgramBindingParams Type; }; };
namespace detail { template <> struct BaseOf      <BoundProgram> { typedef BoundObject Type; }; };
namespace detail { template <> struct ObjectBase  <BoundProgram> { typedef Program      Type; }; };
namespace detail { template <> struct ObjectBound <Program     > { typedef BoundProgram Type; }; };
typedef   detail::ObjectSharedPointerTraits       <BoundProgram> ::Type  BoundProgramHandle;

};

#endif // GLW_PROGRAM_H
