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

DummyClient DummyClient::dummy;

IClient* Engine::client = &DummyClient::dummy;

// window data
GLFWwindow* Engine::window = nullptr;
int Engine::scale = 1;
int Engine::width = 0;
int Engine::height = 0;

// projection data
float Engine::fovY = 60;
float Engine::nearClip = 0.1f;
float Engine::farClip = 1000.0f;
Matrix3D Engine::projMatrix;

// rectangle for framebuffer drawing
FramebufferRectangle Engine::rectangle;

// shader stage 1 - world
WorldShader Engine::worldShader;
// shader stage 2 - world ssao
SSAOShader Engine::ssaoShader;
// shader stage 3 - world ssao blur
SSAOBlurShader Engine::ssaoBlurShader;
// shader stage 4 - world post
WorldPostShader Engine::worldPostShader;
// shader stage 5 - 2D overlay
OverlayShader Engine::overlayShader;

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;

    if(!worldShader.init() || !ssaoShader.init() || !ssaoBlurShader.init() || !worldPostShader.init() || !overlayShader.init() || !rectangle.init())
    {
        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;
    }
    
    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();
    }
    
    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();
    worldShader.resize();
    ssaoShader.resize();
    ssaoBlurShader.resize();
    worldPostShader.resize();
}

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;
}

float Engine::getFieldOfView()
{
    return fovY;
}

float Engine::getNearClip()
{
    return nearClip;
}

float Engine::getFarClip()
{
    return farClip;
}

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::onRenderTick(float lag)
{
    // update projection matrix
    float tan = tanf((0.5f * fovY) * M_PI / 180.0f);
    float q = 1.0f / tan;
    float aspect = (float) width / height;

    projMatrix.set(0, 0, q / aspect);
    projMatrix.set(1, 1, q);
    projMatrix.set(2, 2, (nearClip + farClip) / (nearClip - farClip));
    projMatrix.set(3, 2, -1.0f);
    projMatrix.set(2, 3, (2.0f * nearClip * farClip) / (nearClip - farClip));
    projMatrix.set(3, 3, 0); 
    
    // -------------------------------------------------------------------------
    // shader stage 1 - world
    // -------------------------------------------------------------------------
    worldShader.preRender(projMatrix.getValues());
    // call render tick for further drawing
    client->render3DTick(lag);
    
    // -------------------------------------------------------------------------
    // shader stage 2 - world ssao
    // -------------------------------------------------------------------------
    ssaoShader.preRender(projMatrix.getValues());
 
    // bind previously generated texture data buffers
    worldShader.bindPositionTexture(1);
    worldShader.bindNormalTexture(2);
    worldShader.bindColorTexture(3);
    worldShader.bindDepthTexture(4);
    ssaoShader.bindNoiseTexture(5);
    
    rectangle.draw();
    
    // -------------------------------------------------------------------------
    // shader stage 3 - world ssao blur
    // -------------------------------------------------------------------------
    ssaoBlurShader.preRender();
    ssaoShader.bindTexture(6);
    rectangle.draw();
    
    // -------------------------------------------------------------------------
    // shader stage 4 - world post
    // -------------------------------------------------------------------------
    worldPostShader.preRender();
    ssaoBlurShader.bindTexture(7);
    rectangle.draw();
    
    // -------------------------------------------------------------------------
    // shader stage 5 - 2D overlay
    // -------------------------------------------------------------------------
    
    overlayShader.preRender();
    worldPostShader.bindTexture(0);
    rectangle.draw();
    overlayShader.setViewMatrix();
    
    overlayShader.setUseColor(true);
    overlayShader.setUseTexture(true);
    
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glBlendEquation(GL_FUNC_ADD);
    client->render2DTick(lag);
    glDisable(GL_BLEND);
}

void Engine::setWorldViewMatrix(const float* data)
{
    worldShader.setViewMatrix(data);
}

void Engine::setWorldModelMatrix(const float* data)
{
    worldShader.setModelMatrix(data);
}

void Engine::setOverlayModelMatrix(const float* data)
{
    overlayShader.setModelMatrix(data);
}
// 562