/* Universidade Federal Fluminense
 * Programa de Pós-Graduação em Computação
 *
 * Tópicos Avançados em Computação Visual e Interfaces II (Técnicas Avançadas de Rendering)
 * Professor: Leandro Augusto Frata Fernandes
 *
 * Version: 2012.05.08-001
 */

// Include the Standard Template Library (STL).
#include <cstdlib>
#include <iostream>
#include <vector>

// Include the OpenGL Extension Wrangler Library (GLEW).
//TODO Include the GLEW's header file here.
#include <GL/glew.h>

// Include the Open-Source OpenGL Utility Toolkit (FreeGLUT).
#include <GL/freeglut.h>

// Include some custom definitions.
#include "../utils/basic.h"
#include "../utils/basic_io.h"
#include "../utils/camera.h"
#include "../utils/object.h"


// --- HARD-CODED OBJECTS ----------------------------------------------------------------------------------------------

void checkGLErrors() {
	GLenum errorCode = glGetError();
	if (errorCode != GL_NO_ERROR) {
		std::cerr << gluErrorString(errorCode) << std::endl;
	}
}

// A simple quad.
class BoringQuad : public Object {
public:

	BoringQuad() :
		Object() {
	}

	BoringQuad(const coord3f& location, const coord3f& forwardDirection, const coord3f& upDirection) :
		Object(location, forwardDirection, upDirection) {
	}

	void draw() {
		glBegin(GL_TRIANGLE_STRIP);

		glColor3f(1.0f, 0.0f, 0.0f);
		glNormal3f(0.0f, 0.0f, 1.0f);
		glVertex3f(-1.0f, -1.0f, 0.0f);

		glColor3f(0.0f, 1.0f, 0.0f);
		glNormal3f(0.0f, 0.0f, 1.0f);
		glVertex3f(1.0f, -1.0f,  0.0f);

		glColor3f(0.0f, 0.0f, 1.0f);
		glNormal3f(0.0f, 0.0f, 1.0f);
		glVertex3f(-1.0f, 1.0f, 0.0f);

		glColor3f(1.0f, 1.0f, 1.0f);
		glNormal3f( 0.0f,  0.0f,  1.0f);
		glVertex3f(1.0f, 1.0f, 0.0f);

		glEnd();
	}
};

// A simple sphere.
class BoringSphere : public Object {
public:

	BoringSphere() :
		Object() {
		initGeometry(180, 180);
	}

	BoringSphere(const coord3f& location, const coord3f& forwardDirection, const coord3f& upDirection) :
		Object(location, forwardDirection, upDirection) {
		initGeometry(180, 180);
	}

private:

	struct MyVertex {
		coord3f vertex;
		coord3f normal;
		color3f color;
	};

	void initGeometry(const GLuint& nRings, const GLuint& nSegments) {
		// Setup data in CPU.
		MyVertex current;

		unsigned short verticeIndex = 0;
		GLfloat deltaRingAngle = (pi<GLfloat>() / nRings);
		GLfloat deltaSegAngle = (2.0f * pi<GLfloat>() / nSegments);
 
		for (GLuint ring=0; ring<=nRings; ++ring) {
			GLfloat r0 = sin(ring * deltaRingAngle);
			GLfloat y0 = cos(ring * deltaRingAngle);
 
			for (GLuint seg=0; seg<=nSegments; ++seg) {
				GLfloat x0 = r0 * sin(seg * deltaSegAngle);
				GLfloat z0 = r0 * cos(seg * deltaSegAngle);
 
				current.vertex.set(x0, y0, z0);
				current.normal.set(x0, y0, z0);
				current.color.set(0.5f * (current.normal.x + 1.0f), 0.5f * (current.normal.y + 1.0f), 0.5f * (current.normal.z + 1.0f));
				m_vertices.push_back(current);
 
				if (ring != nRings) {
					m_indices.push_back(verticeIndex + nSegments + 1);
					m_indices.push_back(verticeIndex);
					m_indices.push_back(verticeIndex + nSegments);
					m_indices.push_back(verticeIndex + nSegments + 1);
					m_indices.push_back(verticeIndex + 1);
					m_indices.push_back(verticeIndex);
					verticeIndex++;
				}
			}
		}
	}

public:

	void draw() {
		glBegin(GL_TRIANGLES);

		for (size_t i=0; i!=m_indices.size(); ++i) {
			const MyVertex& current = m_vertices[m_indices[i]];

			glNormal3fv(current.normal.data());
			glColor3fv(current.color.data());
			glVertex3fv(current.vertex.data());
		}

		glEnd();
	}

private:

	std::vector<MyVertex> m_vertices;
	std::vector<unsigned short> m_indices;
};

// My own vertex.
struct MyVertex {
	coord4f vertex;
	coord3f normal;
	texcoord2f texCoord0;
	coord3f tangent;
	coord3f bitangent;
};

// A simple quad.
class Quad : public Object {
public:

	Quad() :
		m_vertexArrayVBO(0),
		m_indexArrayVBO(0),
		Object() {
	}

	Quad(const coord3f& location, const coord3f& forwardDirection, const coord3f& upDirection) :
		m_vertexArrayVBO(0),
		m_indexArrayVBO(0),
		Object(location, forwardDirection, upDirection) {
	}

public:

	void initVertexBufferObject() {
		// Setup data in CPU.
		MyVertex vertices[] = {
			{ coord4f(-1.0f, -1.0f, 0.0f, 1.0f), coord3f(0.0f, 0.0f, 1.0f), texcoord2f(0.0f, 0.0f), coord3f(1.0f, 0.0f, 0.0f), coord3f(0.0f, 1.0f, 0.0f) },
			{ coord4f(1.0f, -1.0f,  0.0f, 1.0f), coord3f(0.0f, 0.0f, 1.0f), texcoord2f(1.0f, 0.0f), coord3f(1.0f, 0.0f, 0.0f), coord3f(0.0f, 1.0f, 0.0f) },
			{ coord4f(-1.0f, 1.0f, 0.0f, 1.0f), coord3f(0.0f, 0.0f, 1.0f), texcoord2f(0.0f, 1.0f), coord3f(1.0f, 0.0f, 0.0f), coord3f(0.0f, 1.0f, 0.0f) },
			{ coord4f(1.0f, 1.0f, 0.0f, 1.0f), coord3f( 0.0f,  0.0f,  1.0f), texcoord2f(1.0f, 1.0f), coord3f(1.0f, 0.0f, 0.0f), coord3f(0.0f, 1.0f, 0.0f) } };

		unsigned short indices[] = { 0, 1, 2, 2, 1, 3 };

		// Create two buffer objects, one for vertices ans other for indices.
		//TODO Make it work.
		//TODO Use m_vertexArrayVBO and m_indexArrayVBO to keep VBO's ids.

		glGenBuffers(1, &m_vertexArrayVBO);
		glGenBuffers(1, &m_indexArrayVBO);

		glBindBuffer(GL_ARRAY_BUFFER, m_vertexArrayVBO);
		glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexArrayVBO);
		glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
		
		
	}

	void draw() {
		//TODO Make it work using Vertex Buffer Object.
		glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT);

		//Set custom attributes
		glBindBuffer(GL_ARRAY_BUFFER, m_vertexArrayVBO);
		//Vertex
		glEnableVertexAttribArray(0);
																							//Offset
		glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, sizeof(MyVertex), (void*)0);
		//Normal
		glEnableVertexAttribArray(1);
		glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(MyVertex), (void*)(0 + sizeof(coord4f)));
		//Texcoord
		glEnableVertexAttribArray(2);
		glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(MyVertex), (void*)(0 + sizeof(coord4f) + sizeof(coord3f)));

		//Draw Elements
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexArrayVBO);
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (void*)0);

		glPopClientAttrib();

	}

	void freeVertexBufferObject() {
		//TODO Make it work.
		glDeleteBuffers(1, &m_vertexArrayVBO);
		glDeleteBuffers(1, &m_indexArrayVBO);
	}

private:

	GLuint m_vertexArrayVBO;
	GLuint m_indexArrayVBO;
};

// A simple sphere.
class Sphere : public Object {
public:

	Sphere() :
		m_vertexArrayVBO(0),
		m_indexArrayVBO(0),
		m_indicesCount(0),
		Object() {
	}

	Sphere(const coord3f& location, const coord3f& forwardDirection, const coord3f& upDirection) :
		m_vertexArrayVBO(0),
		m_indexArrayVBO(0),
		m_indicesCount(0),
		Object(location, forwardDirection, upDirection) {
	}

public:

	void initVertexBufferObject(const GLuint& nRings, const GLuint& nSegments) {
		// Setup data in CPU.
		MyVertex current;
		std::vector<MyVertex> vertices;
		std::vector<unsigned short> indices;

		unsigned short verticeIndex = 0;
		GLfloat deltaRingAngle = (pi<GLfloat>() / nRings);
		GLfloat deltaSegAngle = (2.0f * pi<GLfloat>() / nSegments);
 
		for (GLuint ring=0; ring<=nRings; ++ring) {
			GLfloat r0 = sin(ring * deltaRingAngle);
			GLfloat y0 = cos(ring * deltaRingAngle);
 
			for (GLuint seg=0; seg<=nSegments; ++seg) {
				GLfloat x0 = r0 * sin(seg * deltaSegAngle);
				GLfloat z0 = r0 * cos(seg * deltaSegAngle);
 
				current.vertex.set(x0, y0, z0, 1.0f);
				current.normal.set(x0, y0, z0);
				current.texCoord0.set((GLfloat)seg / (GLfloat)nSegments, (GLfloat)ring / (GLfloat)nRings);
				vertices.push_back(current);
 
				if (ring != nRings) {
					indices.push_back(verticeIndex + nSegments + 1);
					indices.push_back(verticeIndex);
					indices.push_back(verticeIndex + nSegments);
					indices.push_back(verticeIndex + nSegments + 1);
					indices.push_back(verticeIndex + 1);
					indices.push_back(verticeIndex);
					verticeIndex++;
				}
			}
		}

		m_indicesCount = indices.size();

		for (size_t i=0; i<indices.size(); i+=3) {
			MyVertex& v1 = vertices[indices[i + 0]];
			MyVertex& v2 = vertices[indices[i + 1]];
			MyVertex& v3 = vertices[indices[i + 2]];
        
			const coord4f& deltaCoord1 = v2.vertex - v1.vertex;
			const coord4f& deltaCoord2 = v3.vertex - v1.vertex;
        
			const texcoord2f& deltaTexCoord1 = v2.texCoord0 - v1.texCoord0;
			const texcoord2f& deltaTexCoord2 = v3.texCoord0 - v1.texCoord0;
        
			const GLfloat r = 1.0f / (deltaTexCoord1.s * deltaTexCoord2.t - deltaTexCoord2.s * deltaTexCoord1.t);
			const coord4f& tangent = r * (deltaCoord1 * deltaTexCoord2.t - deltaCoord2 * deltaTexCoord1.t);
			const coord4f& bitangent = r * (deltaCoord2 * deltaTexCoord1.s - deltaCoord1 * deltaTexCoord2.s);

			v1.tangent.set(tangent.x, tangent.y, tangent.z);
			v1.bitangent.set(bitangent.x, bitangent.y, bitangent.z);

			v2.tangent.set(tangent.x, tangent.y, tangent.z);
			v2.bitangent.set(bitangent.x, bitangent.y, bitangent.z);

			v3.tangent.set(tangent.x, tangent.y, tangent.z);
			v3.bitangent.set(bitangent.x, bitangent.y, bitangent.z);
		}
    
		// Create two buffer objects, one for vertices ans other for indices.
		//TODO Make it work.
		//TODO Use m_vertexArrayVBO and m_indexArrayVBO to keep VBO's ids.
		glGenBuffers(1, &m_vertexArrayVBO);
		glGenBuffers(1, &m_indexArrayVBO);

		glBindBuffer(GL_ARRAY_BUFFER, m_vertexArrayVBO);
		glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(MyVertex), &vertices.front(), GL_STATIC_DRAW);

		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexArrayVBO);
		glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned short), &indices.front(), GL_STATIC_DRAW);
	}

	void draw() {
		//TODO Make it work using Vertex Buffer Object.
		glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT);

		//Set custom attributes
		glBindBuffer(GL_ARRAY_BUFFER, m_vertexArrayVBO);
		//Vertex
		glEnableVertexAttribArray(0);
																							//Offset
		glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, sizeof(MyVertex), (void*)0);
		//Normal
		glEnableVertexAttribArray(1);
		glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(MyVertex), (void*)(0 + sizeof(coord4f)));
		//Texcoord
		glEnableVertexAttribArray(2);
		glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(MyVertex), (void*)(0 + sizeof(coord4f) + sizeof(coord3f)));

		//Draw Elements
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_indexArrayVBO);
		glDrawElements(GL_TRIANGLES, m_indicesCount, GL_UNSIGNED_SHORT, (void*)0);

		glPopClientAttrib();
	}

	void freeVertexBufferObject() {
		//TODO Make it work.
		glDeleteBuffers(1, &m_vertexArrayVBO);
		glDeleteBuffers(1, &m_indexArrayVBO);
	}

private:

	GLuint m_vertexArrayVBO;
	GLuint m_indexArrayVBO;

	GLsizei m_indicesCount;
};


// --- SOME USEFUL GLOBAL VARIABLES ------------------------------------------------------------------------------------

// The main camera.
PinholeCamera camera;

// Some geometry.
//BoringQuad quad;     //TODO Replace the BoringQuad class by the Quad class.
Quad quad;
//BoringSphere sphere; //TODO BoringSphere the BoringQuad class by the Sphere class.
Sphere sphere;

GLuint mottoTexture;
GLuint woodTexture;

GLuint vertShader;
GLuint fragShader;
GLuint shaderProgram;

GLint materialDiffuseMapLocation;
GLint modelViewProjectionMatrixLocation;

// --- SCENE RELATED FUNCTIONS -----------------------------------------------------------------------------------------

// Use this procedure for making the scene.
void initScene() {
	camera.setPosition(
		coord3f(0.0f, 0.0f, 1.0f),  // Location.
		coord3f(0.0f, 0.0f, -1.0f), // Forward vector.
		coord3f(0.0f, 1.0f, 0.0f)); // Up vector.

	quad.setPosition(
		coord3f(0.0f, 0.0f, -2.0f), // Location.
		coord3f(0.0f, 0.0f, 1.0f),  // Forward vector.
		coord3f(0.0f, 1.0f, 0.0f)); // Up vector.

	sphere.setPosition(
		coord3f(0.0f, 0.0f, 0.0f),  // Location.
		coord3f(0.0f, 0.0f, 1.0f),  // Forward vector.
		coord3f(0.0f, 1.0f, 0.0f)); // Up vector.

	sphere.translate(coord3f(0.0f, 0.0f, -1.0f));
	sphere.scale(coord3f(0.5f, 0.5f, 0.5f));

	//TODO Call quad.initVertexBufferObject();
	quad.initVertexBufferObject();
	//TODO Call sphere.initVertexBufferObject(180, 180);
	sphere.initVertexBufferObject(180, 180);
}

// Use this procedure for deleting the Vertex Buffer Objects.
void freeScene() {
	//TODO Call quad.freeVertexBufferObject();
	quad.freeVertexBufferObject();
	//TODO Call sphere.freeVertexBufferObject();
	sphere.freeVertexBufferObject();
}

//TODO Use this procedure.
// Query and display a shader info log.
void printShaderInfoLog(const char* description, GLuint shader) {
	const int LOG_LENGTH = 1024;
	
	char log[LOG_LENGTH] = {};
	int length = 0;
	glGetShaderInfoLog(shader, LOG_LENGTH, &length, log);

	std::clog << description << std::endl;
	std::clog << log << std::endl;
}


//TODO Use this procedure.
// Query and display a program info log.
void printProgramInfoLog(const char* description, GLuint program) {
	const int LOG_LENGTH = 1024;
	
	char log[LOG_LENGTH] = {};
	int length = 0;
	glGetProgramInfoLog(program, LOG_LENGTH, &length, log);

	std::clog << description << std::endl;
	std::clog << log << std::endl;
}


// Use this procedure for loading, compiling, linking and using the shaders for simple 2-dimensional texture mapping.
void initTexture2DMappingProgram() {
	//TODO Make it work.
	//Source loading

	//TODO Use loadShaderFromFile() to load "texture2d_mapping.frag" and "texture2d_mapping.vert".
	checkGLErrors();
	char *vsSource = loadShaderFromFile("texture2d_mapping.vert");
	char *fsSource = loadShaderFromFile("texture2d_mapping.frag");

	//Create
	vertShader = glCreateShader(GL_VERTEX_SHADER);
	fragShader = glCreateShader(GL_FRAGMENT_SHADER);
	checkGLErrors();
	//Source
	glShaderSource(vertShader, 1, (const char**)&vsSource, (int*)0);
	glShaderSource(fragShader, 1, (const char**)&fsSource, (int*)0);
	checkGLErrors();
	//Compile
	glCompileShader(vertShader);
	glCompileShader(fragShader);
	checkGLErrors();
	//TODO Call printShaderInfoLog() for the vertex shader.
	//TODO Call printShaderInfoLog() for the fragment shader.
	printShaderInfoLog("Vertex PROBLEM", vertShader);
	printShaderInfoLog("Fragment PROBLEM", fragShader);
	checkGLErrors();
	//Free
	free(vsSource);
	free(fsSource);
	
	//Program
	shaderProgram = glCreateProgram();
	glAttachShader(shaderProgram, vertShader);
	glAttachShader(shaderProgram, fragShader);
	
	//Bind
	glBindAttribLocation(shaderProgram, 0, "rm_Vertex");
	glBindAttribLocation(shaderProgram, 1, "rm_Normal");
	glBindAttribLocation(shaderProgram, 2, "rm_TexCoord0");
	checkGLErrors();
	//Uniform Locations
	//Link
	glLinkProgram(shaderProgram);

	materialDiffuseMapLocation = glGetUniformLocation(shaderProgram, "MaterialDiffuseMap");
	modelViewProjectionMatrixLocation = glGetUniformLocation(shaderProgram, "ModelViewProjectionMatrix");
	checkGLErrors();

	//TODO Call printProgramInfoLog() for program.
	printProgramInfoLog("Program PROBLEM", shaderProgram);

}

// Use this procedure for deleting shader and program objects for the simple 2-dimensional texture mapping.
void freeTexture2DMappingProgram() {
	//TODO Make it work.
	glDeleteProgram(shaderProgram);
	glDeleteShader(vertShader);
	glDeleteShader(fragShader);
}

// Use this procedure for texture loading.
void initTextures() {
	//TODO Make it work.
	GLsizei width;
	GLsizei height;
	GLenum format;
	GLenum type;

	//TODO Use loadImageFromFile() to load "../media/motto.png" and "../media/wood.jpg".
	//Motto
	void* pixels = loadImageFromFile("../media/motto.png", width, height, format, type);

	glGenTextures(1, &mottoTexture);
	glBindTexture(GL_TEXTURE_2D, mottoTexture);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, format, type, pixels);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

	glGenerateMipmapEXT(GL_TEXTURE_2D);

	//free(pixels);

	//Wood
	pixels = loadImageFromFile("../media/wood.jpg", width, height, format, type);

	glGenTextures(1, &woodTexture);
	glBindTexture(GL_TEXTURE_2D, woodTexture);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, format, type, pixels);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

	glGenerateMipmapEXT(GL_TEXTURE_2D);

	free(pixels);

}

// Use this procedure for deleting the texture objects.
void freeTextures() {
	//TODO Make it work.
	glDeleteTextures(1, &mottoTexture);
	glDeleteTextures(1, &woodTexture);
}


// --- CUSTOM CALLBACK FUNCTIONS ---------------------------------------------------------------------------------------

// The callback function for handling the display of the current window.
void myDiplayCallback() {
	
	// Clear the back buffer.
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	//TODO Replace the fixed-function pipeline by the programable pipeline.
	glUseProgram(shaderProgram);
	{
		const matrix4x4f& viewProjectionMatrix = multiply(camera.getProjectionMatrix(), camera.getViewMatrix());
		const matrix4x4f& modelMatrix = quad.getModelMatrix(); 
		const matrix4x4f& modelViewProjectionMatrix = multiply(viewProjectionMatrix, modelMatrix);

		glUniform1i(materialDiffuseMapLocation, 0);
		glUniformMatrix4fv(modelViewProjectionMatrixLocation, 1, GL_FALSE, modelViewProjectionMatrix.data());

		glPushAttrib(GL_TEXTURE_BIT);

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, mottoTexture);
	

		quad.draw();
		glPopAttrib();
	}
	{
		const matrix4x4f& viewProjectionMatrix = multiply(camera.getProjectionMatrix(), camera.getViewMatrix());
		const matrix4x4f& modelMatrix = sphere.getModelMatrix(); 
		const matrix4x4f& modelViewProjectionMatrix = multiply(viewProjectionMatrix, modelMatrix);

		glUniform1i(materialDiffuseMapLocation, 0);
		glUniformMatrix4fv(modelViewProjectionMatrixLocation, 1, GL_FALSE, modelViewProjectionMatrix.data());
	
		glPushAttrib(GL_TEXTURE_BIT);

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, woodTexture);

		sphere.draw();
		glPopAttrib();
	}
	

	/*
	//Fixed pipeline
	// Setup projection.
	glMatrixMode(GL_PROJECTION);
	glLoadMatrixf(camera.getProjectionMatrix().data());

	// Setup view space.
	glMatrixMode(GL_MODELVIEW);
	glLoadMatrixf(camera.getViewMatrix().data());

	// Draw the scene.
	glPushMatrix();
	glMultMatrixf(quad.getModelMatrix().data());
	quad.draw();
	glPopMatrix();

	glPushMatrix();
	glMultMatrixf(sphere.getModelMatrix().data());
	sphere.draw();
	glPopMatrix();
	*/

	// Swap buffers.
	glutSwapBuffers();
	
}

// The callback function for handling traditional ASCII keyboard input for the current window.
void myKeyboardCallback(unsigned char key, int x, int y) {
	const GLfloat TRANSLATION_STEP = 0.05f;
	const GLfloat ROTATION_STEP_IN_RADIANS = degreesToRadians(1.0f);

	switch (key) {
		// A or a: Step left.
		case 'A':
		case 'a': camera.translateInViewSpace(coord3f(-TRANSLATION_STEP, 0.0f, 0.0f)); break;

		// D or d: Step right.
		case 'D':
		case 'd': camera.translateInViewSpace(coord3f(TRANSLATION_STEP, 0.0f, 0.0f)); break;

		// W or w: Step forward.
		case 'W':
		case 'w': camera.translateInViewSpace(coord3f(0.0f, 0.0f, -TRANSLATION_STEP)); break;

		// S or s: Step backward.
		case 'S':
		case 's': camera.translateInViewSpace(coord3f(0.0f, 0.0f, TRANSLATION_STEP)); break;

		// Q or q: Rotate left.
		case 'Q':
		case 'q': camera.rotateInViewSpace(coord3f(0.0f, 0.0f, -1.0f), -ROTATION_STEP_IN_RADIANS); break;
			
		// E or e: Rotate right.
		case 'E':
		case 'e': camera.rotateInViewSpace(coord3f(0.0f, 0.0f, -1.0f), ROTATION_STEP_IN_RADIANS); break;

		// ESC: Reset camera position.
		case 27: camera.setPosition(coord3f(0.0f, 0.0f, 1.0f), coord3f(0.0f, 0.0f, -1.0f), coord3f(0.0f, 1.0f, 0.0f)); break;
	};

	glutPostRedisplay();
}

// The callback function for handling mouse click in the current window.
void myMouseButtonCallback(int button, int state, int x, int y) {
	if (state ==  GLUT_DOWN) {
		glutSetCursor(GLUT_CURSOR_NONE);
		glutWarpPointer(glutGet(GLUT_WINDOW_WIDTH) / 2, glutGet(GLUT_WINDOW_HEIGHT) / 2);
	}
	else if (state ==  GLUT_UP) {
		glutSetCursor(GLUT_CURSOR_INHERIT);
	}
}

// The callback function for handling active mouse motion in the current window.
void myMouseMotionCallback(int x, int y) {
	int xCenter = glutGet(GLUT_WINDOW_WIDTH) / 2;
	int yCenter = glutGet(GLUT_WINDOW_HEIGHT) / 2;

	if ((x != xCenter) || (y != yCenter)) {
		const GLfloat ROTATION_STEP = 0.005f;

		coord3f oldZ(0.0f, 0.0f, 1.0f);
		coord3f& newZ = normalize(coord3f(-ROTATION_STEP * (x - xCenter), ROTATION_STEP * (y - yCenter), 1.0f));

		camera.rotateInViewSpace(cross(oldZ, newZ), acos(dot(oldZ, newZ)));

		glutWarpPointer(xCenter, yCenter);
		glutPostRedisplay();
	}
}

// The callback functor that is triggered when the current window is reshaped.
void myWindowReshapeCallback(int width, int height) {
	// Update viewport.
	glViewport(0, 0, width, height);

	// Update projection.
	camera.setProjection(degreesToRadians(60.0f), static_cast<GLfloat>(width) / height, 0.5f, 50.0f);
}


// --- THE MAIN PROGRAM ------------------------------------------------------------------------------------------------

// The main program.
int main(int argc, char* argv[]) {
	// Initialize FreeGLUT.
	glutInit(&argc, argv);

	// Initialize the properties of windows that will be create in the future.
	const int INIT_WINDOW_POSITION_X = 10;
	const int INIT_WINDOW_POSITION_Y = 10;
	const int INIT_WINDOW_WIDTH = 512;
	const int INIT_WINDOW_HEIGHT = 512;

	glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH | GLUT_RGBA); // Double buffering, Depth buffer, RGBA color buffer
	glutInitWindowPosition(INIT_WINDOW_POSITION_X, INIT_WINDOW_POSITION_Y);
	glutInitWindowSize(INIT_WINDOW_WIDTH, INIT_WINDOW_HEIGHT);

	// Create one window and set its callback functions.
	glutCreateWindow("GLSL Project: Lecture 2");

	glutDisplayFunc(myDiplayCallback);
	glutKeyboardFunc(myKeyboardCallback);
	glutMotionFunc(myMouseMotionCallback);
	glutMouseFunc(myMouseButtonCallback);
	glutReshapeFunc(myWindowReshapeCallback);

	// Initialize GLEW.
	//TODO Initialize the GLEW library here.
	glewInit();

	// Set some OpenGL states.
	glEnable(GL_DEPTH_TEST); // Depth test is on.
	glEnable(GL_CULL_FACE);  // Back-face culling is on.

	// Setup scene.
	initScene();

	// Setup shaders.
	initTexture2DMappingProgram();

	// Setup used textures.
	initTextures();

	// Start the main loop.
	glutMainLoop();

	// Free shader and program objects.
	freeTexture2DMappingProgram();

	// Free texture objects.
	freeTextures();

	// Free Vertex Buffer Objects.
	freeScene();

	// Job done!
	return EXIT_SUCCESS;
}
