#include <stdio.h>

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

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

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

#define RESTART 345678

typedef struct {
	GLfloat x;
	GLfloat y;
	GLfloat z;
} Point;

GLuint program;
GLuint vao;
GLuint indicesBufferObject;

GLfloat aspectRatio = 1.0f;

GLfloat step = 0.0f;


Point corners[] = {
	{  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 }
};

GLuint indices[] = {
	0, 1,
	0, 2,
	0, 4,

	1, 3,
	1, 5,

	2, 3,
	2, 6,

	3, 7,

	4, 5,
	4, 6,

	5, 7,

	6, 7
};



// CREATE 4x4 IDENTITY MATRIX
void identity(GLfloat* out) {
	for (int i = 0; i < 16; i++) {
		out[i] = (i % 4 == i / 4);
	}
}

// CREATE 4x4 TRANSLATION MATRIX
void translation(GLfloat* out, GLfloat* v) {
	identity(out);
	for (int i = 0; i < 3; i++) {
		out[3 * 4 + i] = v[i];
	}
}

// CREATE 4x4 SCALING MATRIX
void scaling(GLfloat* out, GLfloat* v) {
	identity(out);
	for (int i = 0; i < 3; i++) {
		out[i * 5] = v[i];
	}
}

// CREATE 4x4 ROTATION MATRIX AROUND Z AXIS
/*  cos a  -sin a   0      0
 *  sin a   cos a   0      0
 *    0       0     1      0
 *    0       0     0      1
*/
void rotationZ(GLfloat* out, GLfloat angle) {
	identity(out);
	out[0] = cos(angle);
	out[1] = sin(angle);
	out[4] = -sin(angle);
	out[5] = cos(angle);
}

// CREATE 4x4 ROTATION MATRIX AROUND Y AXIS
void rotationY(GLfloat* out, GLfloat angle) {
	identity(out);
	out[0] = cos(angle);
	out[2] = -sin(angle);
	out[8] = sin(angle);
	out[10] = cos(angle);
}

// CREATE 4x4 ROTATION MATRIX AROUND Y AXIS
void rotationX(GLfloat* out, GLfloat angle) {
	identity(out);
	out[5] = cos(angle);
	out[6] = sin(angle);
	out[9] = -sin(angle);
	out[10] = cos(angle);
}

// MULTIPLY ANY TO MATRICES
void multiplyAny(GLfloat* A, GLfloat* B, GLfloat* out, int wA, int hA, int wB) {
	int sizeOut = hA * wB;
	GLfloat* result = (GLfloat*) malloc(sizeOut * sizeof(GLfloat));
	for (int i = 0; i < sizeOut; i++) {
		result[i] = 0;
		// printf("%d: ", i);
		for (int j = 0; j < wA; j++) {
			// printf("%d : %f ", j * hA + i % hA, A[j * hA + i % hA]);
			result[i] += A[j * hA + i % hA] * B[j + i / hA * wB];
		}
		// printf("\n");
	}
	memcpy(out, result, sizeOut * sizeof(GLfloat));
	free(result);
}

// MULTIPLY TWO 4x4 MATRICES
void multiply(GLfloat* A, GLfloat* B, GLfloat* out) {
	multiplyAny(A, B, out, 4, 4, 4);
}

// MULTIPLY 4x4 MATRIX WITH VEC4
void multiplyV(GLfloat* M, GLfloat* v, GLfloat* out) {
	multiplyAny(M, v, out, 4, 4, 1);
}

// MULTIPLY in WITH TRANSLATION MATRIX OF v
void translate(GLfloat* out, GLfloat* in, GLfloat* v) {
	GLfloat translationMatrix[16];
	translation(translationMatrix, v);
	multiply(translationMatrix, in, out);
}

// MULTIPLY in WITH SCALING MATRIX OF v
void scale(GLfloat* out, GLfloat* in, GLfloat* v) {
	GLfloat scalingMatrix[16];
	scaling(scalingMatrix, v);
	multiply(scalingMatrix, in, out);
}

// MULTIPLY in WITH ROTATION MATRIX OF a AROUND Z AXIS
void rotateZ(GLfloat* out, GLfloat* in, GLfloat angle) {
	GLfloat rotationMatrix[16];
	rotationZ(rotationMatrix, angle);
	multiply(rotationMatrix, in, out);
}
// MULTIPLY in WITH ROTATION MATRIX OF a AROUND Y AXIS
void rotateY(GLfloat* out, GLfloat* in, GLfloat angle) {
	GLfloat rotationMatrix[16];
	rotationY(rotationMatrix, angle);
	multiply(rotationMatrix, in, out);
}
// MULTIPLY in WITH ROTATION MATRIX OF a AROUND X AXIS
void rotateX(GLfloat* out, GLfloat* in, GLfloat angle) {
	GLfloat rotationMatrix[16];
	rotationX(rotationMatrix, angle);
	multiply(rotationMatrix, in, out);
}

void init(void) {

	// create and compile vertex shader
	GLchar *vertexText = malloc(vertexShader_glsl_len);
	memcpy(vertexText, vertexShader_glsl, vertexShader_glsl_len);
	const GLchar *vertexTextConst = vertexText;

	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);
	}

	free(vertexText);
	vertexText = NULL;
	vertexTextConst = NULL;



	// create and compile fragment shader
	
	GLchar *fragmentText = malloc(fragmentShader_glsl_len);
	memcpy(fragmentText, fragmentShader_glsl, fragmentShader_glsl_len);
	const GLchar *fragmentTextConst = fragmentText;

	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);
	}

	free(fragmentText);
	fragmentText = NULL;
	fragmentTextConst = NULL;

	// 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(corners), corners, 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(Point),
		0
	);
	glEnableVertexAttribArray(0);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindVertexArray(0);


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


	glEnable(GL_PRIMITIVE_RESTART);
	glPrimitiveRestartIndex(RESTART);

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


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

void draw(void) {
	glClear(GL_COLOR_BUFFER_BIT);
	glUseProgram(program);
	glBindVertexArray(vao);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indicesBufferObject);

	step += 0.001f;
	if (step > 1.0f) step -= 1.0f;

	GLfloat M[16];
	GLfloat scaleFactor = sin(step * 3.14159f * 2) * 0.1f + 0.2f;
	// GLfloat scaleFactor = 0.2f;

	GLfloat scaleBy[3] = {scaleFactor / aspectRatio, scaleFactor, scaleFactor};

	GLfloat translateBy[3] = {sin(step * 3.14159f * 4.0f), 0.0f, 0.0f};

	identity(M);
	rotateY(M, M, step * 3.14159f * 2);
	rotateX(M, M, step * 3.14159f * 2 + 1.0f);
	rotateZ(M, M, step * 3.14159f * 2 + 0.5f);
	scale(M, M, scaleBy);
	// translate(M, M, translateBy);

	glUniformMatrix4fv(glGetUniformLocation(program, "transformation"), 1, GL_FALSE, M);

	glDrawElements(GL_LINES, sizeof(indices) / sizeof(GLuint),  GL_UNSIGNED_INT, NULL);
}

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

int main(void) {

	// GLfloat A[16];
	// GLfloat B[16];
	// GLfloat v[] = {1.0f, 2.0f, 3.0f};
	// GLfloat angle = 1.5f;

	// identity(A);
	// identity(B);
	// multiplyAny(A, B, A, 4, 4, 4);

	// translate(A, A, v);
	// scale(A, A, v);
	// rotateZ(A, A, angle);


	// // DEBUG
	// // PRINT COLUMN MAJOR MATRIX
	// for (int i = 0; i < 16; i++) {
	// 	if (i != 0 && i % 4 == 0) printf("\n");

	// 	// cursed stuff to print column major form
	// 	printf("%.2f ", A[i % 4 * 4 + i / 4]);
	// }
	// printf("\n");
	// // ------

	// return 0;

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

	GLFWwindow *window = glfwCreateWindow(600, 600, "Computergrafik 1", NULL, NULL);

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


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

	glewInit();

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


	init();

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

		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	glfwTerminate();

	return 0;
}