/*********************************************************************
* This file provide implementations for the functions declared
* in Geometry.h
* It serves as an alternative to the now deprecated fixed-function
* pipeline that GLUT utilizes.
* Written in July 2016 by Hoang Tran. Extended September 2016 to support more
* shapes than just the teapot.
*
* DO NOT MODIFY THIS FILE WHEN DOING THE HOMEWORK ASSIGNMENT
*********************************************************************/

#include "Geometry.h"
#include "Transform.h"
#include "variables.h"

// May need to replace with the absolute path on some systems
#define PATH_TO_TEAPOT_OBJ "teapot.obj"

// Avoiding linker error caused by multiply defined variables
GLuint VAOs[3], VBOs[3], NBOs[3], EBOs[3];
/** TEAPOT RELATED **/
std::vector <vec3> teapotVertices;
std::vector <vec3> teapotNormals;
std::vector <unsigned int> teapotIndices;
/** SPHERE RELATED **/
std::vector <vec3> sphereVertices;
std::vector <vec3> sphereNormals;
int prevStacks = -1, prevSlices = -1;
/** CUBE RELATED **/
// Due to modern OpenGL making the vertices and normals share the same indices, we will need
// to repeat vertices and normals quite a lot in setting things up...

// Cube vertices
const GLfloat cubeVerts[24][3] = {
	// Front face
	{ -0.5f, -0.5f, 0.5f },{ -0.5f, 0.5f, 0.5f },{ 0.5f, 0.5f, 0.5f },{ 0.5f, -0.5f, 0.5f },
	// Back face
	{ -0.5f, -0.5f, -0.5f },{ -0.5f, 0.5f, -0.5f },{ 0.5f, 0.5f, -0.5f },{ 0.5f, -0.5f, -0.5f },
	// Left face
	{ -0.5f, -0.5f, 0.5f },{ -0.5f, 0.5f, 0.5f },{ -0.5f, 0.5f, -0.5f },{ -0.5f, -0.5f, -0.5f },
	// Right face
	{ 0.5f, -0.5f, 0.5f },{ 0.5f, 0.5f, 0.5f },{ 0.5f, 0.5f, -0.5f },{ 0.5f, -0.5f, -0.5f },
	// Top face
	{ 0.5f, 0.5f, 0.5f },{ -0.5f, 0.5f, 0.5f },{ -0.5f, 0.5f, -0.5f },{ 0.5f, 0.5f, -0.5f },
	// Bottom face
	{ 0.5f, -0.5f, 0.5f },{ -0.5f, -0.5f, 0.5f },{ -0.5f, -0.5f, -0.5f },{ 0.5f, -0.5f, -0.5f }
};

// Cube normals
const GLfloat cubeNorms[24][3] = {
	// Front face
	{ 0.0f, 0.0f, 1.0f },{ 0.0f, 0.0f, 1.0f },{ 0.0f, 0.0f, 1.0f },{ 0.0f, 0.0f, 1.0f },
	// Back face
	{ 0.0f, 0.0f, -1.0f },{ 0.0f, 0.0f, -1.0f },{ 0.0f, 0.0f, -1.0f },{ 0.0f, 0.0f, -1.0f },
	// Left face
	{ -1.0f, 0.0f, 0.0f },{ -1.0f, 0.0f, 0.0f },{ -1.0f, 0.0f, 0.0f },{ -1.0f, 0.0f, 0.0f },
	// Right face
	{ 1.0f, 0.0f, 0.0f },{ 1.0f, 0.0f, 0.0f },{ 1.0f, 0.0f, 0.0f },{ 1.0f, 0.0f, 0.0f },
	// Top face
	{ 0.0f, 1.0f, 0.0f },{ 0.0f, 1.0f, 0.0f },{ 0.0f, 1.0f, 0.0f },{ 0.0f, 1.0f, 0.0f },
	// Bottom face
	{ 0.0f, -1.0f, 0.0f },{ 0.0f, -1.0f, 0.0f },{ 0.0f, -1.0f, 0.0f },{ 0.0f, -1.0f, 0.0f }
};
// Cube indices
const GLuint cubeIndices[36] = {
	0, 1, 2, 0, 2, 3, // Front face
	4, 5, 6, 4, 6, 7, // Back face
	8, 9, 10, 8, 10, 11, // Left face
	12, 13, 14, 12, 14, 15, // Right face
	16, 17, 18, 16, 18, 19, // Top face
	20, 21, 22, 20, 22, 23 // Bottom face
};

// Initialize the buffer objects. Can only be called after OpenGL is initialized.
void initBufferObjects() {
	// Tell OpenGL to allocate us some space for the VAO
	glGenVertexArrays(3, VAOs);
	// Now allocate some space for all the buffer objects
	glGenBuffers(3, VBOs);
	glGenBuffers(3, NBOs);
	glGenBuffers(3, EBOs);
}

// Free up any dynamically allocated memory here
void destroyBufferObjects() {
	// Delete every vertex array and buffer generated by OpenGL
	glDeleteVertexArrays(3, VAOs);
	glDeleteBuffers(3, VBOs);
	glDeleteBuffers(3, NBOs);
	glDeleteBuffers(3, EBOs);
}

// Initialize the teapot by reading in the file and populating its VAO
void initTeapot() {
	FILE* fp;
	float x, y, z;
	int fx, fy, fz, ignore;
	int c1, c2;
	float minY = INFINITY, minZ = INFINITY;
	float maxY = -INFINITY, maxZ = -INFINITY;

	fp = fopen(PATH_TO_TEAPOT_OBJ, "rb");

	if (fp == NULL) {
		std::cerr << "Error loading file: " << PATH_TO_TEAPOT_OBJ << std::endl;
		exit(-1);
	}

	while (!feof(fp)) {
		c1 = fgetc(fp);
		while (!(c1 == 'v' || c1 == 'f')) {
			c1 = fgetc(fp);
			if (feof(fp))
				break;
		}
		c2 = fgetc(fp);

		if ((c1 == 'v') && (c2 == ' ')) {
			fscanf(fp, "%f %f %f", &x, &y, &z);
			teapotVertices.push_back(vec3(x, y, z));
			if (y < minY) minY = y;
			if (z < minZ) minZ = z;
			if (y > maxY) maxY = y;
			if (z > maxZ) maxZ = z;
		}
		else if ((c1 == 'v') && (c2 == 'n')) {
			fscanf(fp, "%f %f %f", &x, &y, &z);
			// Ignore the normals in mytest2, as we use a solid color for the teapot.
			teapotNormals.push_back(glm::normalize(vec3(x, y, z)));
		}
		else if ((c1 == 'f'))
		{
			fscanf(fp, "%d//%d %d//%d %d//%d", &fx, &ignore, &fy, &ignore, &fz, &ignore);
			teapotIndices.push_back(fx - 1);
			teapotIndices.push_back(fy - 1);
			teapotIndices.push_back(fz - 1);
		}
	}

	fclose(fp); // Finished parsing
				// Recenter the teapot
	float avgY = (minY + maxY) / 2.0f - 0.02f;
	float avgZ = (minZ + maxZ) / 2.0f;
	for (unsigned int i = 0; i < teapotVertices.size(); ++i) {
		vec3 shiftedVertex = (teapotVertices[i] - vec3(0.0f, avgY, avgZ)) * vec3(1.58f, 1.58f, 1.58f);
		teapotVertices[i] = shiftedVertex;
	}
	// Done loading teapot file, now bind it
	glBindVertexArray(VAOs[TEAPOT]);

	// Bind vertices to layout location 0
	glBindBuffer(GL_ARRAY_BUFFER, VBOs[TEAPOT]);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vec3) * teapotVertices.size(), &teapotVertices[0], GL_STATIC_DRAW);
	glEnableVertexAttribArray(0); // This allows usage of layout location 0 in the vertex shader
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), 0);

	// Bind normals to layout location 1
	glBindBuffer(GL_ARRAY_BUFFER, NBOs[TEAPOT]);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vec3) * teapotNormals.size(), &teapotNormals[0], GL_STATIC_DRAW);
	glEnableVertexAttribArray(1); // This allows usage of layout location 1 in the vertex shader
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), 0);

	// The indices array tells OpenGL what order to iterate through the buffers in when the shaders execute
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBOs[TEAPOT]);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(unsigned int) * teapotIndices.size(), &teapotIndices[0], GL_STATIC_DRAW);

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

// This function initializes a cube
void initCube() {
	// Done defining the cube properties, now bind it to a VAO
	glBindVertexArray(VAOs[CUBE]);

	// Bind vertices to layout location 0
	glBindBuffer(GL_ARRAY_BUFFER, VBOs[CUBE]);
	glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVerts), cubeVerts, GL_STATIC_DRAW);
	glEnableVertexAttribArray(0); // This allows usage of layout location 0 in the vertex shader
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), 0);

	// Bind normals to layout location 1
	glBindBuffer(GL_ARRAY_BUFFER, NBOs[CUBE]);
	glBufferData(GL_ARRAY_BUFFER, sizeof(cubeNorms), cubeNorms, GL_STATIC_DRAW);
	glEnableVertexAttribArray(1); // This allows usage of layout location 1 in the vertex shader
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), 0);

	// The indices array tells OpenGL what order to iterate through the buffers in when the shaders execute
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBOs[CUBE]);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(cubeIndices), cubeIndices, GL_STATIC_DRAW);

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


// This function initializes the properties of a sphere. Only sets up the buffer attributes,
// actual vertices and normals are assigned later.
void initSphere() {
	glBindVertexArray(VAOs[SPHERE]);

	// Bind vertices to layout location 0
	glBindBuffer(GL_ARRAY_BUFFER, VBOs[SPHERE]);
	glBufferData(GL_ARRAY_BUFFER, 0, 0, GL_STATIC_DRAW); // Don't give it any data
	glEnableVertexAttribArray(0); // This allows usage of layout location 0 in the vertex shader
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), 0);

	// Bind normals to layout location 1
	glBindBuffer(GL_ARRAY_BUFFER, NBOs[SPHERE]);
	glBufferData(GL_ARRAY_BUFFER, 0, 0, GL_STATIC_DRAW); // Don't give it any data
	glEnableVertexAttribArray(1); // This allows usage of layout location 1 in the vertex shader
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), 0);

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

// Draws a solid teapot
void solidTeapot(float size) {
	glUniformMatrix4fv(modelviewPos, 1, GL_FALSE, &(modelview * glm::scale(mat4(1.0f), vec3(size, size, size)))[0][0]);
	glBindVertexArray(VAOs[TEAPOT]);
	glDrawElements(GL_TRIANGLES, teapotIndices.size(), GL_UNSIGNED_INT, 0);
	glBindVertexArray(0);
}

// Draws a solid cube
void solidCube(float size) {
	glUniformMatrix4fv(modelviewPos, 1, GL_FALSE, &(modelview * glm::scale(mat4(1.0f), vec3(size, size, size)))[0][0]);
	glBindVertexArray(VAOs[CUBE]);
	glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0); // 36 vertices to form 12 triangles for 1 cube
	glBindVertexArray(0);
}

void solidSphere(float radius, int stacks, int slices) {
	// No need to generate the sphere again if the previous rendering already
	// used the same number of stacks and slices
	if (prevStacks != stacks || prevSlices != slices){ 
		prevStacks = stacks; prevSlices = slices;
		sphereVertices.clear();
		sphereNormals.clear();
		float fstacks = (float) stacks;
		float fslices = (float) slices;
		for (int i = 0; i < slices; i++) {
			for (int j = 0; j < stacks; j++) {
				// Top left
				sphereVertices.push_back(vec3(
					radius * -cos(2.0f * pi * i / fstacks) * sin(pi * (j + 1.0f) / fslices),
					radius * -cos(pi * (j + 1.0f) / fslices),
					radius * sin(2.0f * pi * i / fstacks) * sin(pi * (j + 1.0f) / fslices)));
				sphereNormals.push_back(glm::normalize(vec3(
					-cos(2.0f * pi * i / fstacks) * sin(pi * (j + 1.0f) / fslices),
					-cos(pi * (j + 1.0f) / fslices),
					sin(2.0f * pi * i / fstacks) * sin(pi * (j + 1.0f) / fslices))));
				// Top right
				sphereVertices.push_back(vec3(
					radius * -cos(2.0f * pi * (i + 1.0) / fstacks) * sin(pi * (j + 1.0) / fslices),
					radius * -cos(pi * (j + 1.0) / fslices),
					radius * sin(2.0f * pi * (i + 1.0) / fstacks) * sin(pi * (j + 1.0) / fslices)));
				sphereNormals.push_back(glm::normalize(vec3(
					-cos(2.0f * pi * (i + 1.0) / fstacks) * sin(pi * (j + 1.0) / fslices),
					-cos(pi * (j + 1.0) / fslices),
					sin(2.0f * pi * (i + 1.0) / fstacks) * sin(pi * (j + 1.0) / fslices))));
				// Bottom right
				sphereVertices.push_back(vec3(
					radius * -cos(2.0f * pi * (i + 1.0) / fstacks) * sin(pi * j / fslices),
					radius * -cos(pi * j / fslices),
					radius * sin(2.0f * pi * (i + 1.0) / fstacks) * sin(pi * j / fslices)));
				sphereNormals.push_back(glm::normalize(vec3(
					-cos(2.0f * pi * (i + 1.0) / fstacks) * sin(pi * j / fslices),
					-cos(pi * j / fslices),
					sin(2.0f * pi * (i + 1.0) / fstacks) * sin(pi * j / fslices))));

				// Need to repeat 2 of the vertices since we can only draw triangles. Eliminates the confusion
				// of array indices.
				// Top left
				sphereVertices.push_back(vec3(
					radius * -cos(2.0f * pi * i / fstacks) * sin(pi * (j + 1.0f) / fslices),
					radius * -cos(pi * (j + 1.0f) / fslices),
					radius * sin(2.0f * pi * i / fstacks) * sin(pi * (j + 1.0f) / fslices)));
				sphereNormals.push_back(glm::normalize(vec3(
					-cos(2.0f * pi * i / fstacks) * sin(pi * (j + 1.0f) / fslices),
					-cos(pi * (j + 1.0f) / fslices),
					sin(2.0f * pi * i / fstacks) * sin(pi * (j + 1.0f) / fslices))));
				// Bottom right
				sphereVertices.push_back(vec3(
					radius * -cos(2.0f * pi * (i + 1.0) / fstacks) * sin(pi * j / fslices),
					radius * -cos(pi * j / fslices),
					radius * sin(2.0f * pi * (i + 1.0) / fstacks) * sin(pi * j / fslices)));
				sphereNormals.push_back(glm::normalize(vec3(
					-cos(2.0f * pi * (i + 1.0) / fstacks) * sin(pi * j / fslices),
					-cos(pi * j / fslices),
					sin(2.0f * pi * (i + 1.0) / fstacks) * sin(pi * j / fslices))));
				// Bottom left
				sphereVertices.push_back(vec3(
					radius * -cos(2.0f * pi * i / fstacks) * sin(pi * j / fslices),
					radius * -cos(pi * j / fslices),
					radius * sin(2.0f * pi * i / fstacks) * sin(pi * j / fslices)));
				sphereNormals.push_back(glm::normalize(vec3(
					-cos(2.0f * pi * i / fstacks) * sin(pi * j / fslices),
					-cos(pi * j / fslices),
					sin(2.0f * pi * i / fstacks) * sin(pi * j / fslices))));

			}
		}
		// Now bind this new vertex data
		glBindVertexArray(VAOs[SPHERE]);

		// Bind vertices
		glBindBuffer(GL_ARRAY_BUFFER, VBOs[SPHERE]);
		glBufferData(GL_ARRAY_BUFFER, sizeof(vec3) * sphereVertices.size(), &sphereVertices[0], GL_STATIC_DRAW);

		// Bind normals
		glBindBuffer(GL_ARRAY_BUFFER, NBOs[SPHERE]);
		glBufferData(GL_ARRAY_BUFFER, sizeof(vec3) * sphereNormals.size(), &sphereNormals[0], GL_STATIC_DRAW);

		// Done updating the buffers
		glBindBuffer(GL_ARRAY_BUFFER, 0);
		glBindVertexArray(0);
	}
	glUniformMatrix4fv(modelviewPos, 1, GL_FALSE, &modelview[0][0]);
	// Draw the sphere regardless of whether it was previously updated or not
	glBindVertexArray(VAOs[SPHERE]);
	glDrawArrays(GL_TRIANGLES, 0, sphereVertices.size());
	glBindVertexArray(0);
}