#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <stddef.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <typeinfo>
#include "render.h"
#include "core/shader.h"
#include <color.h>
#include <window.h>
#include <log.h>
#include <mutex>

#include "texture.h"
#include "sprite.h"

GLint textureSlotsCount;

struct
{
    render::blendMode blendMode;
    uint drawType;
    const render::texture **bindedTextures;
    const render::shader *bindedShader;
    const render::camera *activeCamera;
}renderState;

namespace render
{
    std::mutex batchLocker;

    glm::mat4 modelMatrix;
    namespace core{

    void removeAllObjects();

    }

    void setClearColor(const vec4f &color)
    {
        glClearColor(color.x, color.y, color.z, color.w);
    }

    void setBlendMode(blendMode mode)
    {
        if(renderState.blendMode == mode)
            return;
        flush();
        switch(mode)
        {
        case blendMode::add:
            glBlendFunc(GL_SRC_ALPHA, GL_ONE); break;
        default:
            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); break;
        }
        renderState.blendMode = mode;
    }

    void clearScreen()
    {
        glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
    }

    void init()
    {
        _log::out("INITIALIZING RENDER\n");
        glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &textureSlotsCount);
        renderState.bindedTextures = new const texture*[textureSlotsCount];
        renderState.blendMode = blendMode::normal;
        renderState.activeCamera = 0;

        glCullFace(GL_BACK);
        glDepthFunc(GL_LEQUAL);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        glEnable(GL_PROGRAM_POINT_SIZE);
        glEnable(GL_LINE_SMOOTH);

        #if DEBUG
        glEnable(GL_DEBUG_OUTPUT);
        #endif // DEBUG

        primitives2d::init();
        sprite::init();
    }

    void free()
    {
        _log::out("DESTROYING RENDER\n");
        primitives2d::free();
        sprite::free();
        core::removeAllObjects();
        delete [] renderState.bindedTextures;
    }

    void viewport(const vec2u &size, const vec2i &offset)
    {
        glViewport(offset.x, offset.y, size.x, size.y);
    }

    void mode2D()
    {
        glDisable(GL_CULL_FACE);
        glDisable(GL_DEPTH_TEST);
        glEnable(GL_BLEND);
    }
    void mode3D()
    {
        glEnable(GL_CULL_FACE);
        glEnable(GL_DEPTH_TEST);
        glDisable(GL_BLEND);
    }

    void bindCamera(const camera *cam)
    {
        flush();
        renderState.activeCamera = cam;
        if(renderState.bindedShader && cam)
        {
            renderState.bindedShader->loadProjectionMatrix(cam->getProj());
            renderState.bindedShader->loadViewMatrix(cam->getView());
            glm::mat4 modelViewMatrix = modelMatrix*cam->getViewMatrix();
            glm::mat3 normal = glm::inverse(glm::transpose(glm::mat3(modelViewMatrix)));
            //renderState.bindedShader->loadNormalMatrix(&normal[0][0]);
        }
    }

    const shader *getActiveShader()
    {
        return renderState.bindedShader;
    }

    const texture *getActiveTexture(GLint slot)
    {
        if(textureSlotsCount < slot)
            return nullptr;
        return renderState.bindedTextures[slot];
    }

    const camera *getActiveCamera()
    {
        return renderState.activeCamera;
    }

    void flush()
    {
        if(batchLocker.try_lock())
        {
            switch(renderState.drawType)
            {
            case 0:
                primitives2d::flush(); break;
            case 1:
                break;
            case 2:
                break;
            case 3:
                break;
            case 4:
                break;
            case 5:
                sprite::flush(); break;
            }
            batchLocker.unlock();
        }
    }

    void lock()
    {
        batchLocker.lock();
    }

    void unlock()
    {
        batchLocker.unlock();
    }

    void bindShader(const shader *obj,bool bindCamera)
    {
        if(renderState.bindedShader == obj)
            return;
        flush();
        renderState.bindedShader = obj;
        if(!obj)
            return glUseProgram(0);
        glUseProgram(obj->getProgram());

        if(renderState.activeCamera && bindCamera)
        {
            glm::mat4 modelViewMatrix = modelMatrix;
            obj->loadProjectionMatrix(renderState.activeCamera->getProj());
            obj->loadViewMatrix(renderState.activeCamera->getView());
            modelViewMatrix *= renderState.activeCamera->getViewMatrix();
        }
        //obj->loadModelMatrix(&modelMatrix[0][0]);
        //glm::mat3 normal = glm::inverse(glm::transpose(glm::mat3(modelViewMatrix)));
        //obj->loadNormalMatrix(&normal[0][0]);
    }

    void bindTexture(const texture *obj, GLint slot)
    {
        if(textureSlotsCount < slot)
            return;
        if(renderState.bindedTextures[slot] == obj)
            return;
        flush();
        renderState.bindedTextures[slot] = obj;
    }

    void setMode(uint mode)
    {
        if(renderState.drawType == mode)
            return;
        flush();
        renderState.drawType = mode;
    }

    void setModelMatrix(const glm::mat4 &mat)
    {
        modelMatrix = mat;
        if(renderState.bindedShader)
        {
            renderState.bindedShader->loadModelMatrix(&modelMatrix[0][0]);
        }
    }

    void unsetModelMatrix()
    {
        modelMatrix = glm::mat4(1);
        if(renderState.bindedShader)
            renderState.bindedShader->loadModelMatrix(&modelMatrix[0][0]);
    }
}
