#include <string.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <stdlib.h>

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <GL/gl.h> //autocomplete goes brrrr
#include <GL/glu.h>

#include "include/logger.h"
#include "include/render.h"
#include "include/matrix.h"
#include "include/player.h"
#include "include/model.h"
#include "include/map.h"
#include "include/collision.h"

int advanced = 0;

struct shader {
    GLuint vert, frag, program;
} typedef shader;

shader shaderProgramFallback, shaderProgramWireframe;

struct object **objects;
int objectCount = 2;

void checkShader(GLuint shaderID, char* description) {
    logData(LOGLEVEL_DEBUG, "Checking %s shader (%d)", __FILE__, __LINE__, (void*[]){(void*)2,&description, &shaderID});
    GLint Result = GL_FALSE;
    int InfoLogLength;
    glGetShaderiv(shaderID, GL_COMPILE_STATUS, &Result);
	glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &InfoLogLength);
	if ( InfoLogLength > 0 ){
        char* text = malloc(InfoLogLength+1);
		glGetShaderInfoLog(shaderID, InfoLogLength, NULL, &text[0]);
		write(1,&text[0],InfoLogLength);
        free(text);
        exit(1);
	}
}

shader loadShader(char*vertfilename, char* fragfilename) {
    GLuint shadervert = glCreateShader(GL_VERTEX_SHADER), shaderfrag = glCreateShader(GL_FRAGMENT_SHADER);
    GLuint program = glCreateProgram();
    GLchar* shaderSourceVert, *shaderSourceFrag;

    FILE *vert_fd = fopen(vertfilename, "r"), *frag_fd = fopen(fragfilename, "r");

    if (vert_fd == NULL || frag_fd == NULL) {
        logData(LOGLEVEL_CRIT, "Unable to create shader from %s, %s", __FILE__, __LINE__, (void*[]) {(void*)2, &vertfilename, &fragfilename});
        exit(1);
    }
    int size;
    fseek(vert_fd, 0, SEEK_END);
    size = ftell(vert_fd);
    rewind(vert_fd);
    shaderSourceVert = malloc(size+1);
    fread(shaderSourceVert, sizeof(char), size, vert_fd);
    shaderSourceVert[size] = '\0';

    fseek(frag_fd, 0, SEEK_END);
    size = ftell(frag_fd);
    rewind(frag_fd);
    shaderSourceFrag = malloc(size+1);
    fread(shaderSourceFrag, sizeof(char), size, frag_fd);
    shaderSourceFrag[size] = '\0';

    glShaderSource(shadervert, 1, (const GLchar**)&shaderSourceVert, NULL);
    glShaderSource(shaderfrag, 1, (const GLchar**)&shaderSourceFrag, NULL);
    glCompileShader(shaderfrag);
    glCompileShader(shadervert);

    checkShader(shadervert, "vertex");
    checkShader(shaderfrag, "fragment");
    glAttachShader(program, shadervert);
    glAttachShader(program, shaderfrag);
    glLinkProgram(program);
    glDeleteShader(shadervert);
    glDeleteShader(shaderfrag);

    free(shaderSourceFrag);
    free(shaderSourceVert);
    return (shader) {shadervert, shaderfrag, program};
}

void initializeDefaultShaders() {
    glUseProgram(0);
    shaderProgramFallback = loadShader("res/shaders/vertexShader.vert", "res/shaders/fragmentShader.frag");
    shaderProgramWireframe = loadShader("res/shaders/wireframe_vertexShader.vert", "res/shaders/wireframe_fragmentShader.frag");

    glUseProgram(shaderProgramFallback.program);

}
void reloadShaders() {
    logData(LOGLEVEL_INFO, "Reloading shaders...", __FILE__, __LINE__, NULL);
    glDeleteProgram(shaderProgramFallback.program);
    glDeleteProgram(shaderProgramWireframe.program);
    initializeDefaultShaders();
    logData(LOGLEVEL_INFO, "Done", __FILE__, __LINE__, NULL);
}

GLuint VAO_xyzGraph;
GLuint VBO_xyzGraph;
float projectionMatrix[4][4]={0};


struct object* loadAsset(const char* filename) {
    struct object* target = NULL;
    target = loadOBJ(filename);
    if (target == NULL) {
        logData(LOGLEVEL_CRIT, "Did not recieve model data from %s", __FILE__, __LINE__, (void*[]) {(void*)1, &filename});
        exit(1);
    }

    glGenVertexArrays(1, &target->handle.VAO);
    glBindVertexArray(target->handle.VAO);
    glGenBuffers(5, target->handle.VBOs);
    
    glBindBuffer(GL_ARRAY_BUFFER, target->handle.VBOs[VBO_VERTICES]);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);

    glBindBuffer(GL_ARRAY_BUFFER, target->handle.VBOs[VBO_COLORS]);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);

    glBindBuffer(GL_ARRAY_BUFFER, target->handle.VBOs[VBO_NORMALS]);
    glEnableVertexAttribArray(2);
    glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE,0,0);

    glBindBuffer(GL_ARRAY_BUFFER, target->handle.VBOs[VBO_UV]);
    glEnableVertexAttribArray(3);
    glVertexAttribPointer(3, 2, GL_FLOAT, GL_FALSE,0,0);



    glBindBuffer(GL_ARRAY_BUFFER, target->handle.VBOs[VBO_VERTICES]);
    glBufferData(GL_ARRAY_BUFFER, target->vertices_size * 3 * sizeof(GLfloat), target->vertices, GL_STATIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,target->handle.VBOs[VBO_INDICES]);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, target->indices_size*sizeof(GLuint), target->indices, GL_DYNAMIC_DRAW);

    glBindBuffer(GL_ARRAY_BUFFER, target->handle.VBOs[VBO_COLORS]);
    glBufferData(GL_ARRAY_BUFFER, target->vertices_size * 3 * sizeof(GLfloat), target->colors, GL_STATIC_DRAW);

    glBindBuffer(GL_ARRAY_BUFFER, target->handle.VBOs[VBO_NORMALS]);
    glBufferData(GL_ARRAY_BUFFER, target->normals_size * 3 * sizeof(GLfloat), target->normals, GL_STATIC_DRAW);

    glBindBuffer(GL_ARRAY_BUFFER, target->handle.VBOs[VBO_UV]);
    glBufferData(GL_ARRAY_BUFFER, target->UV_coords_size * 2 * sizeof(GLfloat), target->UV_coords, GL_STATIC_DRAW);


    // TODO implement
    target->position = (vec4) {0, 0, 0};
    target->rotation = (vec4) {0, 0, 0};

    return target;
}

void initializeRenderer() {
    //glEnable(GL_CULL_FACE);

    createProjectionMat4x4(projectionMatrix, PI/4, 4.0/3, 0.1, 100);

    initializeDefaultShaders();
    
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LESS);

    
    // xyz graph
    glGenVertexArrays(1, &VAO_xyzGraph);
    glBindVertexArray(VAO_xyzGraph);
    glGenBuffers(1, &VBO_xyzGraph);
    glBindBuffer(GL_ARRAY_BUFFER ,VBO_xyzGraph);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);

    objects = malloc(sizeof(struct object*)*objectCount); // 2 objects

    objects[0] = loadAsset("res/hl_mp_stalkyard.obj");
    objects[1] = loadAsset("res/models/finii.obj");

    logData(LOGLEVEL_DEBUG, "Initialized renderer", __FILE__, __LINE__, NULL);
}



Player player = {
{0,0,0,0},
{0,0,2,0}
};

vec4 upAxis = {0, 1,0,0};

void printMatrix(float mat[4][4]) {
    for (int i = 0; i<4; i++) {
        printf("%-9lf %-9lf %-9lf %-9lf\n", mat[i][0],mat[i][1],mat[i][2],mat[i][3]);
    }
}


float final_matrix[4][4]={0};
float modelMatrix[4][4] = {0};
float viewMatrix[4][4] = {0};

#define _GRAPHSIZE 0.05

void renderXYZ() { // XYZ graph in the top left
    glBindVertexArray(VAO_xyzGraph);
    loadIdentMatrix(final_matrix);
    lookAt(viewMatrix, rotateVec4((vec4) {0, 0, 1}, player.rotation), (vec4) {0, 0, 0}, upAxis);
    multiplyMat4x4ByMat4x4(final_matrix, projectionMatrix);
    multiplyMat4x4ByMat4x4(final_matrix, viewMatrix);
    glUseProgram(shaderProgramWireframe.program);
    glUniformMatrix4fv(glGetUniformLocation(shaderProgramWireframe.program, "translationMatrix"),
         1, GL_TRUE, &final_matrix[0][0]);

    glDisable(GL_DEPTH_TEST);
    GLfloat graph[] = {
        0, 0, 0, //X axis
        _GRAPHSIZE,0 , 0,

        0, 0, 0, //Y axis
        0, _GRAPHSIZE, 0,
        
        0, 0, 0, //Z axis
        0, 0, -_GRAPHSIZE,
    };
    glBindBuffer(GL_ARRAY_BUFFER, VBO_xyzGraph);
    glBufferData(GL_ARRAY_BUFFER, 3*sizeof(GLfloat)*8, graph, GL_DYNAMIC_DRAW);
    glDrawArrays(GL_LINES, 0, 6);
    glEnable(GL_DEPTH_TEST);
}

void renderTexturedObject(struct object target) {
    //bind arrays
    glBindVertexArray(target.handle.VAO);

    //prepare matrices
    //int shader = target.handle.shaderProgram;
    int shader = shaderProgramFallback.program; // temp TODO implement ??????
    loadIdentMatrix(modelMatrix);
    rotateMat4x4(modelMatrix, target.rotation); 
    
    translateMatrix(modelMatrix, target.position);
    glUseProgram(shader);
    glUniformMatrix4fv(glGetUniformLocation(shader, "projectionMatrix"),
         1, GL_TRUE, &projectionMatrix[0][0]);
    glUniformMatrix4fv(glGetUniformLocation(shader, "viewMatrix"),
         1, GL_TRUE, &viewMatrix[0][0]);
    glUniformMatrix4fv(glGetUniformLocation(shader, "modelMatrix"),
         1, GL_TRUE, &modelMatrix[0][0]);

    if (advanced) {
        glUseProgram(shaderProgramWireframe.program);
        memcpy(final_matrix, projectionMatrix, sizeof(float)*16);
        multiplyMat4x4ByMat4x4(final_matrix, viewMatrix);
        multiplyMat4x4ByMat4x4(final_matrix, modelMatrix);
        glUniformMatrix4fv(glGetUniformLocation(shaderProgramWireframe.program, "translationMatrix"),
         1, GL_TRUE, &final_matrix[0][0]);
    }
    glUseProgram(shader);

    // render textured object
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,target.handle.VBOs[VBO_INDICES]);
    for (int i = 0; i<target.textures_size-1; i++) {
        glBindTexture(GL_TEXTURE_2D, target.textures[i]);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint)*(target.texture_counts[i+1]-target.texture_counts[i]), &target.indices[target.texture_counts[i]], GL_DYNAMIC_DRAW);
        glDrawElements(GL_TRIANGLES, (target.texture_counts[i+1]-target.texture_counts[i]), GL_UNSIGNED_INT, NULL);
    }
    //glBindTexture(GL_TEXTURE_2D, target.textures[target.textures_size-1]);
    //glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint)*(target.indices_size-target.texture_counts[target.textures_size-1]), &target.indices[target.texture_counts[target.textures_size-1]], GL_DYNAMIC_DRAW);
    //glDrawElements(GL_TRIANGLES, (target.indices_size-target.texture_counts[target.textures_size-1]), GL_UNSIGNED_INT, NULL);

    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint)*target.indices_size, target.indices, GL_DYNAMIC_DRAW);
    if (advanced) {
            glUseProgram(shaderProgramWireframe.program);
            glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );
            glDrawElements(GL_TRIANGLES, target.indices_size, GL_UNSIGNED_INT, NULL);
            glPolygonMode( GL_FRONT_AND_BACK, GL_FILL );
    }
}

// to avoid division by 0
vec4 gravDir = {0.00001, -1, 0.00001, 0};

void renderWorld() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    loadIdentMatrix(final_matrix); 

    lookAt(viewMatrix, player.position, addVec4(player.position, normalizeVec4(rotateVec4((vec4){0, 0, -1, 0}, player.rotation))), upAxis);
        
    for (int i =0; i<objectCount; i++){
        renderTexturedObject(*objects[i]);
    }

    if (advanced) renderXYZ();
    glfwSwapBuffers(main_window);
    printf("\n%lf\n", Collision(*objects[0], gravDir, objects[1]->position));
}