#include <stdio.h>

#include <GL/glew.h>
#include <GLFW/glfw3.h>

#include "vertexShader.c"
#include "fragmentShader.c"

#include "matrixMath.h"
#include "transformation.h"

#include <stdlib.h>
#include <math.h>
#include <string.h>
#include <stdbool.h>

#define RESTART 345678

GLuint program;
GLuint vao;
GLuint cubeIndicesBufferObject;

bool exitRequested = false;

GLFWwindow* window;

GLfloat aspectRatio = 1.0f;

GLfloat step = 0.0f;
const GLfloat pi = 3.14159f;

GLfloat cameraPosition[3] = {0.0f, 0.0f, 2.0f};

GLfloat cube[] = {
	 1.0f,  1.0f,  1.0f,
	 1.0f,  1.0f, -1.0f,
	 1.0f, -1.0f,  1.0f,
	 1.0f, -1.0f, -1.0f,
	-1.0f,  1.0f,  1.0f,
	-1.0f,  1.0f, -1.0f,
	-1.0f, -1.0f,  1.0f,
	-1.0f, -1.0f, -1.0f
};

GLfloat ground[] = {
	1.0f, 0.0f, 1.0f,
	1.0f, 0.0f, -1.0f,
	-1.0f, 0.0f, 0.0f,
	-1.0f, 0.0f, -1.0f
};

GLuint cubeIndices[] = {
	0, 1, 2,
	1, 3, 2,

	1, 7, 3,
	1, 5, 7,

	4, 6, 5,
	5, 6, 7,

	0, 2, 4,
	4, 2, 6,

	7, 6, 3,
	6, 2, 3,

	4, 5, 1,
	4, 1, 0
};

GLuint groundIndices[] = {
	0, 1, 2,
	1, 3, 2
};

void handleInputs(void) {
	if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) {
		cameraPosition[2] += 0.02f;
	}
	if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) {
		cameraPosition[2] -= 0.02f;
	}
	if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS) {
		cameraPosition[1] += 0.02f;
	}
	if (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS) {
		cameraPosition[1] -= 0.02f;
	}
}

void keyboardHandler(GLFWwindow* window, int key, int scancode, int action, int mods) {
	if (action == GLFW_PRESS) {
		if (key == GLFW_KEY_ESCAPE) {
			exitRequested = true;
		}
	}
}

void init(void) {
	// create and compile vertex shader
	const GLchar *vertexTextConst = vertexShader_glsl;

	GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertexShader, 1, &vertexTextConst, &vertexShader_glsl_len);
	glCompileShader(vertexShader);


	GLint status;
	glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &status);
	
	if (!status) {
		printf("Error compiling vertex shader: ");
		GLchar infoLog[1024];
		glGetShaderInfoLog(vertexShader, 1024, NULL, infoLog);
		printf("%s",infoLog);
	}

	vertexTextConst = NULL;



	// create and compile fragment shader
	
	const GLchar *fragmentTextConst = fragmentShader_glsl;

	GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragmentShader, 1, &fragmentTextConst, &fragmentShader_glsl_len);
	glCompileShader(fragmentShader);

	glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &status);
	
	if (!status) {
		printf("Error compiling fragment shader: ");
		GLchar infoLog[1024];
		glGetShaderInfoLog(fragmentShader, 1024, NULL, infoLog);
		printf("%s",infoLog);
	}

	// create and link shader program
	program = glCreateProgram();
	glAttachShader(program, vertexShader);
	glAttachShader(program, fragmentShader);
	glLinkProgram(program);

	glGetProgramiv(program, GL_LINK_STATUS, &status);

	if (!status) {
		printf("Error linking program: ");
		GLchar infoLog[1024];
		glGetProgramInfoLog(program, 1024, NULL, infoLog);
		printf("%s",infoLog);
	}
	glValidateProgram(program);


	glGetProgramiv(program, GL_VALIDATE_STATUS, &status);

	if (!status) {
		printf("Error validating program: ");
		GLchar infoLog[1024];
		glGetProgramInfoLog(program, 1024, NULL, infoLog);
		printf("%s",infoLog);
	}


	GLuint triangleVertexBufferObject;
	glGenBuffers(1, &triangleVertexBufferObject);
	glBindBuffer(GL_ARRAY_BUFFER, triangleVertexBufferObject);
	glBufferData(GL_ARRAY_BUFFER, sizeof(cube), cube, GL_STATIC_DRAW);
	glBindBuffer(GL_ARRAY_BUFFER, 0);


	// create vertex array object
	glGenVertexArrays(1, &vao);
	glBindVertexArray(vao);
	glBindBuffer(GL_ARRAY_BUFFER, triangleVertexBufferObject);
	glVertexAttribPointer(
		0,
		3,
		GL_FLOAT,
		GL_FALSE,
		sizeof(GLfloat) * 3,
		0
	);
	glEnableVertexAttribArray(0);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindVertexArray(0);


	// ENABLE BACKFACE CULLING
	glFrontFace(GL_CW);
	glEnable(GL_CULL_FACE);


	// ENABLE RESTARTING
	glEnable(GL_PRIMITIVE_RESTART);
	glPrimitiveRestartIndex(RESTART);

	// DEFINE INDEX ARRAY FOR ELEMENT DRAWING
	glGenBuffers(1, &cubeIndicesBufferObject);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, cubeIndicesBufferObject);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(cubeIndices), cubeIndices, GL_STATIC_DRAW);


	glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
}

void drawCube(GLfloat* position, GLfloat* scaleVec, GLfloat* rotateVec) {
	GLfloat modelTransformation[16];

	identity(modelTransformation);

	scale(modelTransformation, modelTransformation, scaleVec);

	rotateX(modelTransformation, modelTransformation, rotateVec[0]);
	rotateY(modelTransformation, modelTransformation, rotateVec[1]);
	rotateZ(modelTransformation, modelTransformation, rotateVec[2]);

	translate(modelTransformation, modelTransformation, position);


	glUniformMatrix4fv(glGetUniformLocation(program, "modelTransformation"), 1, GL_FALSE, modelTransformation);

	glUniform3f(glGetUniformLocation(program, "color"), 1.0f, 0.0f, 0.0f);
	glDrawElements(GL_TRIANGLES, 6,  GL_UNSIGNED_INT, (void*)(0 * sizeof(GLuint)));

	glUniform3f(glGetUniformLocation(program, "color"), 1.0f, 1.0f, 0.0f);
	glDrawElements(GL_TRIANGLES, 6,  GL_UNSIGNED_INT, (void*)(6 * sizeof(GLuint)));

	glUniform3f(glGetUniformLocation(program, "color"), 1.0f, 1.0f, 1.0f);
	glDrawElements(GL_TRIANGLES, 6,  GL_UNSIGNED_INT, (void*)(12 * sizeof(GLuint)));

	glUniform3f(glGetUniformLocation(program, "color"), 0.0f, 1.0f, 1.0f);
	glDrawElements(GL_TRIANGLES, 6,  GL_UNSIGNED_INT, (void*)(18 * sizeof(GLuint)));

	glUniform3f(glGetUniformLocation(program, "color"), 0.0f, 0.0f, 1.0f);
	glDrawElements(GL_TRIANGLES, 6,  GL_UNSIGNED_INT, (void*)(24 * sizeof(GLuint)));

	glUniform3f(glGetUniformLocation(program, "color"), 1.0f, 0.0f, 1.0f);
	glDrawElements(GL_TRIANGLES, 6,  GL_UNSIGNED_INT, (void*)(30 * sizeof(GLuint)));
}

void draw(void) {

	handleInputs();

	glClear(GL_COLOR_BUFFER_BIT);
	glUseProgram(program);
	glBindVertexArray(vao);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, cubeIndicesBufferObject);

	step += 0.002f;
	if (step > 1.0f) step -= 1.0f;
	GLfloat stepi = step * pi * 2;


	// ------------- MODEL TRANSFORMATION ---------------------
	// SCALE -> ROTATE -> TRANSLATE

	// GLfloat cubePosition[3] = {0.0f, -2.0f, 0.0f};
	// GLfloat cubeScale[3] = {4.0f, 0.2f, 4.0f};

	// GLfloat modelTransformation[16];
	// identity(modelTransformation);
	// scale(modelTransformation, modelTransformation, cubeScale);

	// rotateY(modelTransformation, modelTransformation, stepi);
	// rotateX(modelTransformation, modelTransformation, stepi + 1.0f);
	// rotateZ(modelTransformation, modelTransformation, stepi + 0.5f);

	// translate(modelTransformation, modelTransformation, cubePosition);



	// ------------- VIEWING TRANSFORMATION -------------------
	GLfloat origin[3] = {0.0f, 0.0f, 0.0f};
	GLfloat up[3] = {0.0f, 1.0f, 0.0f};

	GLfloat viewingTransformation[16];
	GLfloat fixedCameraPosition[3] = {cos(stepi) * 4.0f, 1.5f, sin(stepi) *  4.0f};
	lookAt(viewingTransformation, fixedCameraPosition, origin, up);



	// -------------- PROJECTION TRANSFORMATION ----------------
	GLfloat projectionTransformation[16];
	GLfloat near = 0.1f;
	GLfloat far = 5.0f;
	perspectiveProjection(projectionTransformation, near, far);



	// -------------- NORMALISATION TRANSFORMATION -------------
	GLfloat normalisationTransformation[16];
	GLfloat r = 0.2f;
	GLfloat l = -0.2f;
	GLfloat t = 0.2f;
	GLfloat b = -0.2f;
	normalisedDeviceCoordinates(normalisationTransformation, r, l, t, b, near, far);


	// --------------- LAZY ASPECT RATIO ADJUSTMENT ------------
	GLfloat aspectScale[16];
	GLfloat aspectScaleVector[3] = {1.0f / aspectRatio, 1.0f, 1.0f};
	scaling(aspectScale, aspectScaleVector);


	GLfloat globalTransformation[16];
	identity(globalTransformation);

	multiply(globalTransformation, viewingTransformation, globalTransformation);
	multiply(globalTransformation, projectionTransformation, globalTransformation);
	multiply(globalTransformation, normalisationTransformation, globalTransformation);
	multiply(globalTransformation, aspectScale, globalTransformation);

	glUniformMatrix4fv(glGetUniformLocation(program, "globalTransformation"), 1, GL_FALSE, globalTransformation);

	GLfloat position[3] = {0.0f, -2.0f, 0.0f};
	GLfloat scaleVec[3] = {3.0f, 0.2f, 3.0f};
	GLfloat rotateVec[3] = {0.0f, 0.0f, 0.0f};
	for (int i=0; i < 10; i++) {
		position[1] += 0.4;
		rotateVec[1] += 0.2;
		scaleVec[0] -= 0.3;
		scaleVec[2] -= 0.3;
		drawCube(position, scaleVec, rotateVec);
	}

}

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

int main(void) {

	GLfloat test[16] = {
		1.0f, 2.0f, 3.0f, 4.0f,
		5.0f, 6.0f, 7.0f, 8.0f,
		9.0f, 1.0f, 2.0f, 3.0f,
		4.0f, 5.0f, 6.0f, 7.0f
	};
	transpose(test, test);

	GLfloat test2[16] = {
		0.0f, 1.0f, 0.0f, 1.0f,
		1.0f, 2.0f, 1.0f, 2.0f,
		2.0f, 3.0f, 2.0f, 3.0f,
		3.0f, 4.0f, 3.0f, 4.0f
	};
	transpose(test2, test2);

	multiply(test, test, test2);
	// mat4Print(test);

	// return 0;

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

	window = glfwCreateWindow(700, 700, "Computergrafik 1", NULL, NULL);

	if (!window) {
		printf("Failed to create window\n");
		glfwTerminate();
		return -1;
	}


	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
	glfwMakeContextCurrent(window);

	// register keyboard event handler
	glfwSetKeyCallback(window, keyboardHandler);

	glewInit();

	printf("OpenGL version supported by this platform (%s):\n", glGetString(GL_VERSION));


	init();

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

		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	glfwTerminate();

	return 0;
}