#include "Wrapper.h"
#include <random>

DummyClient DummyClient::dummy;

IClient* Engine::client = &DummyClient::dummy;
GLFWwindow* Engine::window = nullptr;

// active program
GLuint Engine::activeProgram = 0;

// world data
ShaderProgram Engine::worldShader;
GLuint Engine::worldFrameBuffer = 0;
GLuint Engine::worldPositionTexture = 0;
GLuint Engine::worldNormalTexture = 0;
GLuint Engine::worldColorTexture = 0;
GLuint Engine::worldDepthTexture = 0;

// ssao shader
ShaderProgram Engine::ssaoShader;
GLuint Engine::ssaoFrameBuffer = 0;
GLuint Engine::ssaoTexture = 0;

// post shader
GLuint Engine::postVba = 0;
GLuint Engine::postVbo = 0;
ShaderProgram Engine::postShader;

int Engine::scale = 1;
int Engine::width = 0;
int Engine::height = 0;

bool Engine::lineMode = false;

float Engine::testX = 0;
float Engine::testY = 0;
float Engine::testZ = 0;

// ssao kernel data
Vector3D Engine::ssaoKernel[ssaoKernelAmount];
GLuint Engine::noiseTexture = 0;
Matrix3D Engine::testMat;

bool Engine::init(int width, int height, const char* name)
{
    Engine::width = width;
    Engine::height = height;
    updateScale();
    
    if(!glfwInit())
    {
        cout << "could not initialize GLFW" << endl;
        return false;
    }

    glfwDefaultWindowHints();
    glfwWindowHint(GLFW_VISIBLE, 0);
    glfwWindowHint(GLFW_RESIZABLE, 1);
    window = glfwCreateWindow(width, height, name, nullptr, nullptr);
    if(!window)
    {
        cout << "could not create window" << endl;
        glfwTerminate();
        return false;
    }

    glfwMakeContextCurrent(window);
    glfwSwapInterval(1);

    glfwShowWindow(window);

    GLenum err = glewInit();
    if(GLEW_OK != err)
    {
        cout << "could not initialize GLEW: " << glewGetErrorString(err) << endl;
        return false;
    }
    cout << "Status: Using GLEW " << glewGetString(GLEW_VERSION) << endl;

    worldShader.compile("shader/worldVertex.vs", "shader/worldFragment.fs");
    if(!worldShader.isValid())
    {
        glfwDestroyWindow(window);
        glfwTerminate();
        return false;
    }
    activeProgram = worldShader.getProgram();

    ssaoShader.compile("shader/ssaoVertex.vs", "shader/ssaoFragment.fs");
    if(!ssaoShader.isValid())
    {
        glfwDestroyWindow(window);
        glfwTerminate();
        return false;
    }
    
    postShader.compile("shader/postVertex.vs", "shader/postFragment.fs");
    if(!postShader.isValid())
    {
        glfwDestroyWindow(window);
        glfwTerminate();
        return false;
    }
    
    glfwSetKeyCallback(window, onKeyEvent);
    glfwSetMouseButtonCallback(window, onMouseClick);
    glfwSetFramebufferSizeCallback(window, onWindowResize); 
    //glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
    //glfwSetCursorPosCallback(window, onMouseMove);
    return true;
}

void Engine::start(IClient* client)
{
    if(client != nullptr)
    {
        Engine::client = client;
    }
    
    onInit();

    glEnable(GL_CULL_FACE);
    glDepthFunc(GL_LEQUAL);
    
    uint64_t newTime = glfwGetTimerValue();
    uint64_t oldTime = newTime;
    uint64_t lag = 0;
    while(!glfwWindowShouldClose(window))
    {
        oldTime = newTime;
        newTime = glfwGetTimerValue();
        lag += newTime - oldTime;

        int ticksPerFrame = 0;
        while(lag >= NANOS_PER_TICK)
        {
            lag -= NANOS_PER_TICK;

            Engine::client->tick();
            ticksPerFrame++;

            if(ticksPerFrame >= MAX_TICKS_PER_FRAME)
            {
                long skip = lag / NANOS_PER_TICK;
                lag -= skip * NANOS_PER_TICK;
                if(skip > 0)
                {
                    cout << "skipped " << skip << " game ticks " << lag << endl;
                }
                break;
            }
        }

        onRenderTick((float) lag / NANOS_PER_TICK);
        
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
    
    onTerm();
    glfwDestroyWindow(window);
    glfwTerminate();
}

void Engine::stop()
{
    glfwSetWindowShouldClose(window, 1);
}

void Engine::onKeyEvent(GLFWwindow* w, int key, int scancode, int action, int mods)
{
    client->onKeyEvent(key, scancode, action, mods);
}

void Engine::onMouseClick(GLFWwindow* w, int button, int action, int mods)
{
    client->onMouseClick(button, action, mods);
}

void Engine::onWindowResize(GLFWwindow* w, int width, int height)
{
    glViewport(0, 0, width, height);
    Engine::width = width;
    Engine::height = height;
    updateScale();
    
    /*glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer); 
    glBindTexture(GL_TEXTURE_2D, frameTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    glBindTexture(GL_TEXTURE_2D, 0);
    glBindRenderbuffer(GL_RENDERBUFFER, depthTexture); 
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, width, height);  
    glBindRenderbuffer(GL_RENDERBUFFER, 0);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);*/
}

void Engine::updateScale()
{
    scale = 1;
    while(width / (scale + 1) >= 400 && height / (scale + 1) >= 300)
    {
        scale++;
    }
}

int Engine::getScale()
{
    return scale;
}

int Engine::getWidth()
{
    return width;
}

int Engine::getHeight()
{
    return height;
}

GLint Engine::getUniformLocation(const GLchar* name)
{
    return glGetUniformLocation(activeProgram, name);
}

void Engine::setMatrix(GLint location, const GLfloat* m)
{
    glUniformMatrix4fv(location, 1, 0, m);
}

void Engine::setInt(GLint location, GLint i)
{
    glUniform1i(location, i);
}

void Engine::setFloat(GLint location, GLfloat f1, GLfloat f2, GLfloat f3, GLfloat f4)
{
    glUniform4f(location, f1, f2, f3, f4);
}

void Engine::printError()
{
    GLenum error = glGetError();
    switch(error)
    {
        case GL_NO_ERROR: 
            cout << "> No error has been recorded." << endl;
            break;
        case GL_INVALID_ENUM: 
            cout << "> An unacceptable value is specified for an enumerated argument." << endl;
            break;
        case GL_INVALID_VALUE: 
            cout << "> A numeric argument is out of range." << endl;
            break;
        case GL_INVALID_OPERATION: 
            cout << "> The specified operation is not allowed in the current state." << endl;
            break;
        case GL_INVALID_FRAMEBUFFER_OPERATION: 
            cout << "> The framebuffer object is not complete." << endl;
            break;
        case GL_OUT_OF_MEMORY: 
            cout << "> There is not enough memory left to execute the command." << endl;
            break;
        case GL_STACK_UNDERFLOW: 
            cout << "> An attempt has been made to perform an operation that would cause an internal stack to underflow." << endl;
            break;
        case GL_STACK_OVERFLOW: 
            cout << "> An attempt has been made to perform an operation that would cause an internal stack to overflow." << endl;
            break;
        default:
            cout << "> Unknown OpenGL error: " << error << endl;
    }
}

void Engine::onInit()
{
    // generate framebuffer and textures for world buffer
    glGenFramebuffers(1, &worldFrameBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, worldFrameBuffer);
    // world position texture
    glGenTextures(1, &worldPositionTexture);
    glBindTexture(GL_TEXTURE_2D, worldPositionTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, worldPositionTexture, 0);   
    // world normal texture
    glGenTextures(1, &worldNormalTexture);
    glBindTexture(GL_TEXTURE_2D, worldNormalTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, worldNormalTexture, 0);   
    // world color texture
    glGenTextures(1, &worldColorTexture);
    glBindTexture(GL_TEXTURE_2D, worldColorTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, worldColorTexture, 0);  
    // set color attachements for the worldFrameBuffer
    GLuint attachments[3] = 
    {
        GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 
    };
    glDrawBuffers(3, attachments);
    // world depth texture
    glGenTextures(1, &worldDepthTexture);
    glBindTexture(GL_TEXTURE_2D, worldDepthTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, width, height, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, worldDepthTexture, 0); 
    // check if world framebuffer is okay
    if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
    {
	cout << "world frame buffer is not complete!" << endl;
    }
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    
    // generate data for drawing previously generated framebuffer as rectangle
    glGenVertexArrays(1, &postVba);
    glBindVertexArray(postVba);
    
    glGenBuffers(1, &postVbo);
    glBindBuffer(GL_ARRAY_BUFFER, postVbo);

    glVertexAttribPointer(0, 2, GL_FLOAT, 0, sizeof(float) * 4, (GLvoid*) 0);
    glEnableVertexAttribArray(0);

    glVertexAttribPointer(1, 2, GL_FLOAT, 0, sizeof(float) * 4, (GLvoid*) (sizeof(float) * 2));
    glEnableVertexAttribArray(1);
    
    float data[] = 
    {
        -1.0f, 1.0f, 0.0f, 1.0f, 
        -1.0f, -1.0f, 0.0f, 0.0f,
        1.0f, -1.0f, 1.0f, 0.0f,
        -1.0f, 1.0f, 0.0f, 1.0f,
        1.0f, -1.0f, 1.0f, 0.0f,
        1.0f, 1.0f, 1.0f, 1.0f
    };
    
    glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 24, data, GL_STATIC_DRAW);
    
    // generate ssao kernel data
    glGenFramebuffers(1, &ssaoFrameBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, ssaoFrameBuffer);
    // ssao color texture
    glGenTextures(1, &ssaoTexture);
    glBindTexture(GL_TEXTURE_2D, ssaoTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, width, height, 0, GL_RGB, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoTexture, 0);  
    // check if world framebuffer is okay
    if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
    {
	cout << "ssao frame buffer is not complete!" << endl;
    }
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    
    std::uniform_real_distribution<float> randomF(0.0, 1.0); // random floats between 0.0 - 1.0
    std::default_random_engine gen;
    for(int i = 0; i < ssaoKernelAmount; i++)
    {
        ssaoKernel[i].set(randomF(gen) * 2.0 - 1.0, randomF(gen) * 2.0 - 1.0, randomF(gen));
        ssaoKernel[i].normalize();
        ssaoKernel[i].mul(randomF(gen));
        
        float scale = (float) i / ssaoKernelAmount; 
        scale  = 0.1f + (scale * scale) * 0.9f;
        ssaoKernel[i].mul(scale);
        //cout << ssaoKernel[i] << endl;
    }
    
    float noise[48];
    for(int i = 0; i < 16; i++)
    {
        noise[i * 3] = randomF(gen) * 2.0 - 1.0;
        noise[i * 3 + 1] = randomF(gen) * 2.0 - 1.0;
        noise[i * 3 + 2] = 0.0f;
    }  
    
    glGenTextures(1, &noiseTexture);
    glBindTexture(GL_TEXTURE_2D, noiseTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, 4, 4, 0, GL_RGB, GL_FLOAT, noise);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); 
}

void Engine::onRenderTick(float lag)
{
    //--------------------------------------------------------------------------
    // Stage 1: draw scene in world framebuffer
    //--------------------------------------------------------------------------
    glActiveTexture(GL_TEXTURE0);
    
    activeProgram = worldShader.getProgram();
    glUseProgram(activeProgram);
    
    glBindFramebuffer(GL_FRAMEBUFFER, worldFrameBuffer);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    glEnable(GL_DEPTH_TEST);
    if(lineMode)
    {
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
        Engine::client->renderTick(lag);
        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    }
    else
    {
        Engine::client->renderTick(lag);
    }

    //--------------------------------------------------------------------------
    // Stage 2: draw textured framebuffer
    //--------------------------------------------------------------------------
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, worldPositionTexture);
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, worldNormalTexture);
    glActiveTexture(GL_TEXTURE3);
    glBindTexture(GL_TEXTURE_2D, worldColorTexture);
    glActiveTexture(GL_TEXTURE4);
    glBindTexture(GL_TEXTURE_2D, worldDepthTexture);
    glActiveTexture(GL_TEXTURE5);
    glBindTexture(GL_TEXTURE_2D, noiseTexture);

    activeProgram = ssaoShader.getProgram();
    glUseProgram(activeProgram);
    
    for(int i = 0; i < ssaoKernelAmount; i++)
    {
        glUniform3f(glGetUniformLocation(activeProgram, (string("ssaoKernel[") + std::to_string(i) + "]").c_str()), 
                ssaoKernel[i].getX(), ssaoKernel[i].getY(), ssaoKernel[i].getZ());
    }
    
    Engine::setMatrix(glGetUniformLocation(activeProgram, "projMatrix"), testMat.getValues());

    glBindFramebuffer(GL_FRAMEBUFFER, ssaoFrameBuffer);
    glClear(GL_COLOR_BUFFER_BIT);
    glDisable(GL_DEPTH_TEST);
    glBindVertexArray(postVba);
    glBindBuffer(GL_ARRAY_BUFFER, postVbo);
    glDrawArrays(GL_TRIANGLES, 0, 6);
    
    glActiveTexture(GL_TEXTURE6);
    glBindTexture(GL_TEXTURE_2D, ssaoTexture);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glClear(GL_COLOR_BUFFER_BIT);
    activeProgram = postShader.getProgram();
    glUseProgram(activeProgram);
    glDrawArrays(GL_TRIANGLES, 0, 6);
}

void Engine::onTerm()
{
    glDeleteVertexArrays(1, &postVba);
    glDeleteBuffers(1, &postVbo);
    
    glDeleteFramebuffers(1, &worldFrameBuffer);
    glDeleteTextures(1, &worldPositionTexture);
    glDeleteTextures(1, &worldNormalTexture);
    glDeleteTextures(1, &worldColorTexture);
    glDeleteTextures(1, &worldDepthTexture);
}

void Engine::setLineMode(bool mode)
{
    lineMode = mode;
}