
#include "window_helper.hpp"
#include "opengl_helper.hpp"

#include <iostream>
#include <vector>
#include <array>
#include <cmath>


// ************************************ //
//          Global variables
// ************************************ //
GLuint shader_program = 0;    // Id of the shader used to draw the data (only one shader)
GLuint vbo_index = 0;         // Buffer storing indices of triangles
GLuint vao = 0;               // Set of attributes to draw the data (only one attribute)
int counter_drawing_loop = 0; // Counter to handle the animation

// variables used for translation
float x_prev = 0.0f; // previous x-position for cursor
float y_prev = 0.0f; // previous y-position for cursor
float tr_x = 0.0f;   // translation in x
float tr_y = 0.0f;   // translation in y

// ************************************ //
//          Function headers
// ************************************ //
void load_data();             // Load and send data to the GPU once
void draw_data();             // Drawing calls within the animation loop

// tag::translation_event[]
// Function called every time the mouse is moved
void cursor_position_callback(GLFWwindow* window, double xpos, double ypos)
{
    const bool mouse_click_left   = (glfwGetMouseButton(window,GLFW_MOUSE_BUTTON_LEFT )==GLFW_PRESS);
    const bool mouse_click_right  = (glfwGetMouseButton(window,GLFW_MOUSE_BUTTON_RIGHT )==GLFW_PRESS);
    if(mouse_click_left)
    {
        tr_x += 0.0025*(xpos-x_prev);
        tr_y -= 0.0025*(ypos-y_prev);
    }

    x_prev = xpos;
    y_prev = ypos;
}
// end::translation_event[]


/** Main function, call the general functions and setup the animation loop */
int main()
{
    std::cout<<"*** Init GLFW ***"<<std::endl;
    glfw_init();

    std::cout<<"*** Create window ***"<<std::endl;
    auto window = glfw_create_window(500,500,"My Window");
    glfwMakeContextCurrent(window);

    std::cout<<"*** Init GLAD ***"<<std::endl;
    glad_init();

    std::cout<<"*** Set OpenGL error callback ***"<<std::endl;
    glEnable              ( GL_DEBUG_OUTPUT );
    glDebugMessageCallback( (GLDEBUGPROC) error_opengl_callback, 0 );
    glDebugMessageControl ( GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, NULL, true );

    print_opengl_information();

    std::cout<<"*** GLFW Callback function ***"<<std::endl;
    glfwSetCursorPosCallback(window, cursor_position_callback );

    std::cout<<"*** Setup Data ***"<<std::endl;
    load_data();

    std::cout<<"*** Compile Shader ***"<<std::endl;
    shader_program = create_shader_program("shaders/vertex_shader.glsl","shaders/fragment_shader.glsl");

    std::cout<<"*** Start GLFW loop ***"<<std::endl;
    while( !glfwWindowShouldClose(window) ) {

        draw_data();

        ++counter_drawing_loop;
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
    std::cout<<"*** Terminate GLFW loop ***"<<std::endl;

}



/** Create (or load) data and send them to GPU */
void load_data()
{
    // ********************************* //
    // Create data
    // ********************************* //


    // Geometry (vertex position)
    const std::vector<GLfloat> position = {
        -0.5f, -0.5f, -2.0f,
         0.5f, -0.5f, -2.0f,
         0.5f,  0.5f, -2.0f,
        -0.5f,  0.5f, -2.0f
    };

    // Normals
    const std::vector<GLfloat> normal = {
        0.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f
    };

    // Connectivity (triangle index)
    const std::vector<GLuint> index = {
         0, 1, 2,
         0, 2, 3
    };




    // ********************************* //
    // Send data on the GPU
    // ********************************* //



    // Fill VBO for position
    GLuint vbo_position = 0;
    glGenBuffers(1, &vbo_position);
    glBindBuffer(GL_ARRAY_BUFFER, vbo_position);
    glBufferData(GL_ARRAY_BUFFER, position.size()*sizeof(GLfloat), &position[0], GL_STATIC_DRAW );
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // Fill VBO for normals
    GLuint vbo_normal = 0;
    glGenBuffers(1, &vbo_normal);
    glBindBuffer(GL_ARRAY_BUFFER, vbo_normal);
    glBufferData(GL_ARRAY_BUFFER, normal.size()*sizeof(GLfloat), &normal[0], GL_STATIC_DRAW );
    glBindBuffer(GL_ARRAY_BUFFER, 0);


    // Fill VBO for index
    glGenBuffers(1, &vbo_index);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo_index);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, index.size()*sizeof(GLuint), &index[0], GL_STATIC_DRAW );
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);



    // ********************************* //
    // Set shader attributes
    // ********************************* //

    glGenVertexArrays(1,&vao);
    glBindVertexArray(vao);

    // position at layout 0
    glBindBuffer(GL_ARRAY_BUFFER, vbo_position);
    glEnableVertexAttribArray( 0 );
    glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 0, nullptr );

    // normals at layout 1
    glBindBuffer(GL_ARRAY_BUFFER, vbo_normal);
    glEnableVertexAttribArray( 1 );
    glVertexAttribPointer( 1, 3, GL_FLOAT, GL_FALSE, 0, nullptr );

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

}


/** Create a perspective matrix */
std::array<float,16> perspective_matrix(float angle_of_view, float image_aspect, float z_near, float z_far)
{
    const float fy = 1/std::tan(angle_of_view/2);
    const float fx = fy/image_aspect;
    const float L = z_near-z_far;

    const float C = (z_far+z_near)/L;
    const float D = (2*z_far*z_near)/L;

    return {
        fx,0,0,0,
        0,fy,0,0,
        0,0,C,D,
        0,0,-1,0
    };

}


/** Function called within the animation loop.
    Setup uniform variables and drawing calls  */
void draw_data()
{


    // ******************************** //
    // Clear screen
    // ******************************** //
    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);


    // ******************************** //
    // Draw data
    // ******************************** //
    glUseProgram(shader_program);
    glBindVertexArray(vao);

    //Perspective projection matrix
    const auto perspective = perspective_matrix( 45.0f*M_PI/180.0f, 1.0f, 0.01f, 500.0f);
    glUniformMatrix4fv(glGetUniformLocation(shader_program, "perspective"), 1, GL_TRUE, &perspective[0]);
    // Translation
    glUniform4f(glGetUniformLocation(shader_program, "translation"), tr_x, tr_y, 0.0f, 0.0f);
    // Color
    glUniform4f(glGetUniformLocation(shader_program, "color"), 0.9f,0.8f,0.4f,1.0f);

    // Draw call
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo_index);
    glDrawElements(GL_TRIANGLES, 3*2, GL_UNSIGNED_INT, nullptr);


    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
    glUseProgram(0);
}


