#include <GL/glew.h>
#include <GL/gl.h>
#include <GLFW/glfw3.h>
#include <math.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "shader.h"
#include "log.h"
#include "shader.h"
#include "matrix-math.h"
#include "wavefrontobj.h"

#define STATUS_INTERVAL 0.5
#define PI 3.14159f

GLuint program;

GLuint vertexArrayObject;
GLuint numVertices;

GLuint initialWindowWidth = 800;
GLuint initialWindowHeight = 600;

GLfloat aspect;
mat4 projectionMatrix;

int frameCount = 0;
struct timespec last_time, current_time;

typedef struct ColorRGB {
	GLfloat r;
	GLfloat g;
	GLfloat b;
} ColorRGB;

// Color Conversion Functions from https://gist.github.com/ciembor/1494530
GLfloat hueToRgb(GLfloat p, GLfloat q, GLfloat t) {
	if (t < 0) t += 1;
	if (t > 1) t -= 1;
	if (t < 1./6) return p + (q - p) * 6 * t;
	if (t < 1./2) return q;
	if (t < 2./3) return p + (q - p) * (2./3 - t) * 6;
	
  return p;
}

// Color Conversion Functions from https://gist.github.com/ciembor/1494530
ColorRGB hslToRgb(GLfloat h, GLfloat s, GLfloat l) {
	ColorRGB result = {0, 0, 0};

	if(0 == s) {
		result.r = result.g = result.b = l; // achromatic
	} else {
		float q = l < 0.5 ? l * (1 + s) : l + s - l * s;
		float p = 2 * l - q;
		result.r = hueToRgb(p, q, h + 1./3);
		result.g = hueToRgb(p, q, h);
		result.b = hueToRgb(p, q, h - 1./3);
	}

	return result;
}

void initialiseStatusDisplay() {
	clock_gettime(CLOCK_MONOTONIC, &last_time);
}

void updateStatusDisplay() {
	frameCount++;
	clock_gettime(CLOCK_MONOTONIC, &current_time);
	double elapsed = (current_time.tv_sec - last_time.tv_sec) +
					 (current_time.tv_nsec - last_time.tv_nsec) / 1e9;
	if (elapsed >= STATUS_INTERVAL) {
		double fps = frameCount / elapsed;
		frameCount = 0;
		last_time = current_time;
		printf("\rFPS: %.2f ", fps);
		fflush(stdout);
	}
}

void recalculateProjectionMatrix() {
	mat4BuildPerspective(projectionMatrix, 60 * M_PI / 180, aspect, 0.1, 10);
	DEBUG("Recalculating Projection Matrix");
}

void init(void) {

	INFO("Building Programs...");
	ProgramLinkResult linkResult = buildShaderProgram("src/shaders/vertex.glsl", "src/shaders/fragment.glsl");
	if (!linkResult.success) {
		FATAL("Failed to link Program");
		exit(1);
	}

	program = linkResult.program;

	INFO("Shader Program Done.");

	// create triangle buffer
	/**
	 * -0.35              0.35
	 * |     -0.2    0.2  |
	 * |    |        |    |
	 * 
	 * 0----1        4----5    ---  0.6
	 * |    |        |    |    
	 * |    |        |    |
	 * |    2--------3    |    ---  0.1
	 * |                  |
	 * |    9--------8    |    --- -0.1
	 * |    |        |    |
	 * |    |        |    |
	 * 11---10       7----6    --- -0.6
	 *
	 * 12-----------------13   --- -0.7
	 * |                  |
	 * 15-----------------14    --- -0.9
	 *   
	 */    

	DEBUG("Loading OBJ File");
	ParsedObjFile f = readObjFile("obj/smooth_monkey.obj");
	numVertices = f.length * sizeof(face) / sizeof(vertex);

	DEBUG("Creating vertext buffer");
	GLuint vertexBuffer;
	glGenBuffers(1, &vertexBuffer);
	glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
	glBufferData(GL_ARRAY_BUFFER, f.length * sizeof(face), f.faces, GL_STATIC_DRAW);
	glBindBuffer(GL_ARRAY_BUFFER, 0);

	DEBUG("Creating vertex array object");
	// create vertex array object
	glGenVertexArrays(1, &vertexArrayObject);
	glBindVertexArray(vertexArrayObject);
	glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);

	// vertex position data
	glVertexAttribPointer(
		0, // shader location
		3, // number of values to read
		GL_FLOAT, // type of value
		GL_FALSE, // if values are normalised
		sizeof(vertex), // stride - distance between vertices
		0 // start offset
	);
	glEnableVertexAttribArray(0);

	glVertexAttribPointer(
		1,
		3,
		GL_FLOAT,
		GL_FALSE,
		sizeof(vertex),
		(void*)offsetof(vertex, normal)
	);
	glEnableVertexAttribArray(1);

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

	glClearColor(0.0f, 0.0f, 0.0f, 0.3f);
	glViewport(0, 0, initialWindowWidth, initialWindowHeight);

	glEnable(GL_CULL_FACE);
	glFrontFace(GL_CCW);

	glEnable(GL_DEPTH_TEST);

	initialiseStatusDisplay();

	recalculateProjectionMatrix();

	INFO("--- Initialisation done ---");
}

GLfloat currentHue = 0.0f;

void draw(void) {
	updateStatusDisplay();

	// counter for animation
	currentHue += 0.005f;
	if (currentHue > 1.0) currentHue = 0.0f;

	// clear colour and depth buffer
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	// select program
	glUseProgram(program);

	// build view matrix
	mat4 viewMatrix;

	// mat4Identity(viewMatrix);
	vec3 cameraPos = {cos(currentHue * M_PI * 2), 0.0f, sin(currentHue * M_PI * 2)};
	vec3 cameraLookAt = {0.0f, 0.2f, 0.0f};
	vec3 cameraUp = {0.0f, 1.0f, 0.0f};
	mat4BuildLookAt(viewMatrix, cameraPos, cameraLookAt, cameraUp);

	GLuint projectionLocation = glGetUniformLocation(program, "uProjection");
	glUniformMatrix4fv(projectionLocation, 1, GL_FALSE, projectionMatrix);


	// build model Matrix
	mat4 modelMatrix;
	mat4 modelViewMatrix;

	vec3 scale = {0.2f, 0.2f, 0.2f};

	mat4Identity(modelMatrix);
	mat4Scale(modelMatrix, modelMatrix, scale);
	mat4Multiply(modelViewMatrix, viewMatrix, modelMatrix);

	glUniformMatrix4fv(glGetUniformLocation(program, "uModelView"), 1, GL_FALSE, modelViewMatrix);

	glBindVertexArray(vertexArrayObject);
	glDrawArrays(GL_TRIANGLES, 0, numVertices);
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
	glViewport(0, 0, width, height);
	aspect = (float)width / height;

	recalculateProjectionMatrix();
}

int main(int argc, char const *argv[])
{

	for (int i = 1; i < argc; i++) {
		if (strcmp(argv[i], "-v") == 0) {
			logLevel = LOG_LEVEL_INFO;
		}
		if (strcmp(argv[i], "-vv") == 0) {
			logLevel = LOG_LEVEL_DEBUG;
		}
	}

	INFO("Creating GLFW Window");

	glfwInit();
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

	glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER , GLFW_TRUE);

	GLFWwindow* window = glfwCreateWindow(initialWindowWidth, initialWindowHeight, "CG1", NULL, NULL);

	aspect = (float)initialWindowWidth / initialWindowHeight;
	if (!window) {
		FATAL("Failed to open window");
		glfwTerminate();
		return 1;
	}
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
	glfwMakeContextCurrent(window);

	glewInit();

	init();

	while (!glfwWindowShouldClose(window)) {
		draw();

		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	glfwTerminate();

	return 0;
}