#include "CGModel.h"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <GLFW/glfw3.h>
#include <iostream>
#include "CGCamera.h"
#include "CGScene.h"
#include "resource.h"

//
// FUNCIÓN: CGModel::initialize(int, int)
//
// PROPÓSITO: Initializa el modelo 3D
//
void CGModel::initialize(int w, int h)
{
    // Crea el programa para la primera pasada
    renderProgram = new CGShaderProgram(IDR_HTML1, IDR_HTML2, IDR_HTML3, -1, -1);
    if (renderProgram->IsCompiled() == GL_TRUE) renderProgram->Link();
    if (renderProgram->IsLinked() == GL_FALSE)
    {
        std::cout << renderProgram->GetLog() << std::endl; 
        return;
    }

    // Crea el programa para la segunda pasada
    volumeProgram = new CGShaderProgram(IDR_HTML4, IDR_HTML5, IDR_HTML6, -1, -1);
    if (volumeProgram->IsCompiled() == GL_TRUE) volumeProgram->Link();
    if (volumeProgram->IsLinked() == GL_FALSE)
    {
        std::cout << volumeProgram->GetLog() << std::endl;
        return;
    }

    // Crea el programa para la primera pasada
    compProgram = new CGShaderProgram(IDR_HTML7, IDR_HTML8, -1, -1, -1);
    if (compProgram->IsCompiled() == GL_TRUE) compProgram->Link();
    if (compProgram->IsLinked() == GL_FALSE)
    {
        std::cout << compProgram->GetLog() << std::endl;
        return;
    }

    // Crea la cámara
    camera = new CGCamera();
    camera->SetPosition(0.0f, 5.0f, 30.0f);

    // Crea la escena
    scene = new CGScene();

    // Asigna el viewport y el clipping volume
    resize(w, h);
    
    // Inicializa la estructura para la tercera pasada
    initVAO();

    // Opciones de dibujo
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
    glClearStencil(0);
    glEnable(GL_DEPTH_TEST);
}

//
// FUNCIÓN: CGModel::finalize()
//
// PROPÓSITO: Libera los recursos del modelo 3D
//
void CGModel::finalize()
{
    delete camera;
    delete scene;
    delete renderProgram;
    delete volumeProgram;
    delete compProgram;
}

//
// FUNCIÓN: CGModel::resize(int w, int h)
//
// PROPÓSITO: Asigna el viewport y el clipping volume
//
void CGModel::resize(int w, int h)
{
    double fov = glm::radians(15.0);
    double sin_fov = sin(fov);
    double cos_fov = cos(fov);
    if (h == 0) h = 1;
    GLfloat aspectRatio = (GLfloat)w / (GLfloat)h;
    GLfloat wHeight = (GLfloat)(sin_fov * 0.2 / cos_fov);
    GLfloat wWidth = wHeight * aspectRatio;

    wndWidth = w;
    wndHeight = h;

    glViewport(0, 0, w, h);
    projection = glm::frustum(-wWidth, wWidth, -wHeight, wHeight, 0.2f, 400.0f);

    initFBO();
}

//
// FUNCIÓN: CGModel::initFBO()
//
// PROPÓSITO: Initializa el FrameBuffer que almacena por separado la componente ambiental
//            y la difusa y especular
//
GLvoid CGModel::initFBO()
{

    // The depth buffer
    GLuint depthBuf;
    glGenRenderbuffers(1, &depthBuf);
    glBindRenderbuffer(GL_RENDERBUFFER, depthBuf);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, wndWidth, wndHeight);

    // The ambient buffer
    GLuint ambBuf;
    glGenRenderbuffers(1, &ambBuf);
    glBindRenderbuffer(GL_RENDERBUFFER, ambBuf);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA, wndWidth, wndHeight);

    // The diffuse+specular component
    glActiveTexture(GL_TEXTURE0);
    GLuint diffSpecTex;
    glGenTextures(1, &diffSpecTex);
    glBindTexture(GL_TEXTURE_2D, diffSpecTex);
    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, wndWidth, wndHeight);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

    // Create and set up the FBO
    glGenFramebuffers(1, &colorDepthFBO);
    glBindFramebuffer(GL_FRAMEBUFFER, colorDepthFBO);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthBuf);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, ambBuf);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, diffSpecTex, 0);

    GLenum drawBuffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
    glDrawBuffers(2, drawBuffers);

    GLenum result = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (result != GL_FRAMEBUFFER_COMPLETE) std::cout << "Framebuffer is not complete." << std::endl;

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

//
// FUNCIÓN: CGModel::initVAO()
//
// PROPÓSITO: Initializa el VAO que cubre la pantalla completa para la última pasada
//
GLvoid CGModel::initVAO()
{

    // Set up a  VAO for the full-screen quad
    GLfloat verts[] = { 
      -1.0f, -1.0f, 0.0f, 
       1.0f, -1.0f, 0.0f,
       1.0f,  1.0f, 0.0f,
      -1.0f, 1.0f, 0.0f 
    };

    GLuint vbo;
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, 4 * 3 * sizeof(GLfloat), verts, GL_STATIC_DRAW);

    // Set up the vertex array object
    glGenVertexArrays(1, &fsVAO);
    glBindVertexArray(fsVAO);

    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(0);  // Vertex position
}

//
// FUNCIÓN: CGModel::render()
//
// PROPÓSITO: Genera la imagen
//
void CGModel::render()
{
    pass1();
    glFlush();
    pass2();
    glFlush();
    pass3();
}

//
// FUNCIÓN: CGModel::pass1()
//
// PROPÓSITO: Realiza la primera pasada. Es un renderizado normal separando la
//            componente ambiental de la componente difusa y especular.
//
void CGModel::pass1() {
    glDepthMask(GL_TRUE);
    glDisable(GL_STENCIL_TEST);
    glEnable(GL_DEPTH_TEST);
    //glm::mat4 proj2 = glm::infinitePerspective(glm::radians(50.0f), (float)wndWidth / wndHeight, 0.5f);
    glm::mat4 view = camera->ViewMatrix();

    renderProgram->Use();

    glBindFramebuffer(GL_FRAMEBUFFER, colorDepthFBO);
    glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    scene->Draw(renderProgram, projection, view, GL_FALSE);
}

//
// FUNCIÓN: CGModel::pass2()
//
// PROPÓSITO: Realiza la segunda pasada. Se generan los volúmenes de sombra por medio
//            del shader de geometría.
//
void CGModel::pass2() {
    volumeProgram->Use();

    // Copy the depth and color buffers from the FBO into the default FBO
    // The color buffer should contain the ambient component.
    glBindFramebuffer(GL_READ_FRAMEBUFFER, colorDepthFBO);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
    glBlitFramebuffer(0, 0, wndWidth - 1, wndHeight - 1, 0, 0, wndWidth - 1, wndHeight - 1, GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT, GL_NEAREST);

    // Disable writing to the color buffer and depth buffer
    glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
    glDepthMask(GL_FALSE);

    // Re-bind to the default framebuffer
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    // Set up the stencil test so that it always succeeds, increments
    // for front faces, and decrements for back faces.
    glClearStencil(0x0);
    glClear(GL_STENCIL_BUFFER_BIT);
    glEnable(GL_STENCIL_TEST);
    glStencilFunc(GL_ALWAYS, 0, 0xffff);
    glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_KEEP, GL_INCR_WRAP);
    glStencilOpSeparate(GL_BACK, GL_KEEP, GL_KEEP, GL_DECR_WRAP);

    // Draw only the shadow casters
    scene->Draw(volumeProgram, projection, camera->ViewMatrix(), GL_TRUE);

    // Enable writing to the color buffer
    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
}

//
// FUNCIÓN: CGModel::pass3()
//
// PROPÓSITO: Realiza la tercera pasada. Se combinan las componentes de color
//            en función del stencil test.
//
void CGModel::pass3() {
    // We don't need the depth test
    glDisable(GL_DEPTH_TEST);

    // We want to just sum the ambient component and the diffuse + specular
    // when the stencil test succeeds, so we'll use this simple blend function.
    glEnable(GL_BLEND);
    glBlendFunc(GL_ONE, GL_ONE);

    // We want to only render those pixels that have a stencil value
    // equal to zero.
    glStencilFunc(GL_EQUAL, 0, 0xffff);
    glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);

    compProgram->Use();
    compProgram->SetUniformI("DiffSpecTex", 0);

    // Just draw a screen filling quad
    glBindVertexArray(fsVAO);
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
    glBindVertexArray(0);

    // Restore some state
    glDisable(GL_BLEND);
    glEnable(GL_DEPTH_TEST);
}

//
// FUNCIÓN: CGModel::update()
//
// PROPÓSITO: Anima la escena
//
void CGModel::update()
{
    camera->MoveFront();
}

//
// FUNCIÓN: CGModel::key_pressed(int)
//
// PROPÓSITO: Respuesta a acciones de teclado
//
void CGModel::key_pressed(int key)
{
    switch (key)
    {
    case GLFW_KEY_UP:
        camera->TurnDown();
        break;
    case GLFW_KEY_DOWN:
        camera->TurnUp();
        break;
    case GLFW_KEY_LEFT:
        camera->TurnCCW();
        break;
    case GLFW_KEY_RIGHT:
        camera->TurnCW();
        break;
    case GLFW_KEY_S:
        camera->SetMoveStep(0.0f);
        break;
    case GLFW_KEY_RIGHT_BRACKET:
    case GLFW_KEY_KP_ADD:
        camera->SetMoveStep(camera->GetMoveStep() + 0.1f);
        break;
    case GLFW_KEY_MINUS:
    case GLFW_KEY_KP_SUBTRACT:
        camera->SetMoveStep(camera->GetMoveStep() - 0.1f);
        break;
    case GLFW_KEY_Q:
        camera->SetMoveStep(0.1f);
        camera->MoveUp();
        camera->SetMoveStep(0.0f);
        break;
    case GLFW_KEY_A:
        camera->SetMoveStep(0.1f);
        camera->MoveDown();
        camera->SetMoveStep(0.0f);
        break;
    case GLFW_KEY_O:
        camera->SetMoveStep(0.1f);
        camera->MoveLeft();
        camera->SetMoveStep(0.0f);
        break;
    case GLFW_KEY_P:
        camera->SetMoveStep(0.1f);
        camera->MoveRight();
        camera->SetMoveStep(0.0f);
        break;
    case GLFW_KEY_K:
        camera->TurnLeft();
        break;
    case GLFW_KEY_L:
        camera->TurnRight();
        break;
    }
}

//
//  FUNCIÓN: CGModel:::mouse_button(int button, int action)
//
//  PROPÓSITO: Respuesta del modelo a un click del ratón.
//
void CGModel::mouse_button(int button, int action)
{
}

//
//  FUNCIÓN: CGModel::mouse_move(double xpos, double ypos)
//
//  PROPÓSITO: Respuesta del modelo a un movimiento del ratón.
//
void CGModel::mouse_move(double xpos, double ypos)
{
}
