/* 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.13-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 ----------------------------------------------------------------------------------------------

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


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

// The main camera.
PinholeCamera camera;

// Some geometry.
BoringQuad quad;
BoringSphere sphere;

// The objects related to the custom render target.
//TODO Declare some handles for a framebuffer object, a color texture object, and a depth renderbuffer object.
GLuint fb01;
GLuint mottoTexture;
GLuint depthbuffer;
GLuint framebuffer;

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

// Use this procedure to draw the scene.
void drawScene() {
	glPushMatrix();
	glMultMatrixf(quad.getModelMatrix().data());
	quad.draw();
	glPopMatrix();

	glPushMatrix();
	glMultMatrixf(sphere.getModelMatrix().data());
	sphere.draw();
	glPopMatrix();
}

// Use this procedure for setting up a framebuffer object.
void initFramebufferObject() {
	GLsizei width = 512;
	GLsizei height = 512;

	// Create and initialize a color texture.
	//TODO Make it work.
	

	//TODO Use loadImageFromFile() to load "../media/motto.png" and "../media/wood.jpg".

	glGenTextures(1, &mottoTexture);
	glBindTexture(GL_TEXTURE_2D, mottoTexture);

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

	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
	
	
	// Create and initialize a depth renderbuffer object.
	glGenRenderbuffers(1, &depthbuffer);

	glBindRenderbuffer(GL_RENDERBUFFER, depthbuffer);

	glRenderbufferStorage(GL_RENDERBUFFER,GL_DEPTH_COMPONENT32F, width, height);

	// Create and initialize a framebuffer object.
	//TODO Make it work.
	glGenFramebuffers(1, &framebuffer);

	glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);

	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mottoTexture, 0);
	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthbuffer);


	// Render to the window.
	//TODO Set the window as the current target.
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

// Use this procedure for deleting a framebuffer object.
void freeFramebufferObject() {
	//TODO Make it work.
	glDeleteFramebuffers(1, &framebuffer);
	glDeleteRenderbuffers(1, &depthbuffer);
	glDeleteTextures(1, &mottoTexture);
}


// Use this procedure to show the target texture (this is not the best way to do that).
void drawTexture(GLuint texture) {
	GLubyte* pixels;
	GLfloat x, scale = 0.25f;
	GLint width, height, level;

	glPushAttrib(GL_ALL_ATTRIB_BITS);

	glDisable(GL_DEPTH_TEST);
	
	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();
	gluOrtho2D(0.0f, glutGet(GLUT_WINDOW_WIDTH), 0.0f, glutGet(GLUT_WINDOW_HEIGHT));

	glMatrixMode(GL_MODELVIEW);
	glPushMatrix();
	glLoadIdentity();

	glBindTexture(GL_TEXTURE_2D, texture);
	
	glPixelZoom(scale, scale);

	glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, &level);
	glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &width);
	glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_HEIGHT, &height);

	x = 0.0f;
	pixels = new GLubyte[width * height * 3];

	while ((width > 0) && (height > 0)) {
		glGetTexImage(GL_TEXTURE_2D, level, GL_BGR, GL_UNSIGNED_BYTE, pixels);
	
		glRasterPos2f(x, 0.0f);
		glDrawPixels(width, height, GL_BGR, GL_UNSIGNED_BYTE, pixels);

		glColor3f(1.0f, 1.0f, 1.0f);
		glBegin(GL_LINE_LOOP);
		glVertex2f(x + (width * scale), height * scale);
		glVertex2f(x, height * scale);
		glVertex2f(x, 0.0f);
		glVertex2f(x + (width * scale), 0.0f);
		glEnd();

		level += 1;
		x += (scale * width);
		glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &width);
		glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_HEIGHT, &height);
	}

	delete [] pixels;

	glPopMatrix();
	glPopMatrix();
	glPopAttrib();
}



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

// The callback function for handling the display of the current window.
void myDiplayCallback() {
	//TODO Render the scene twice: one in the render texture and another one in the window.

	//pass 1
	{
		//bind 1
		glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);

		GLint width, height;

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

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

		// Update viewport.
		
		glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width);
		glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height);

		glViewport(0, 0, width, height);

		// Clear the back buffer.
		glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		// Draw the scene.
		drawScene();
	}

	//pass 2
	{
		//bind 0
		glBindFramebuffer(GL_FRAMEBUFFER, 0);

		GLint width, height;

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

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

		// Update viewport.
		width = glutGet(GLUT_WINDOW_WIDTH);
		height = glutGet(GLUT_WINDOW_HEIGHT);
		glViewport(0, 0, width, height);

		

		// Clear the back buffer.
		glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		// Draw the scene.
		drawScene();
		//glGenerateMipmapEXT(GL_TEXTURE_2D);
	}
	//glBindFramebuffer(GL_FRAMEBUFFER, 0);
	// Display the rendered texture.
	//TODO Call drawTexture(...);
	drawTexture(mottoTexture);

	// 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 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 3");

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

	// Initialize GLEW.
	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 the objects related to a curstom render target.
	initFramebufferObject();

	// Start the main loop.
	glutMainLoop();

	// Free the objects related to the custom renter target.
	freeFramebufferObject();

	// Job done!
	return EXIT_SUCCESS;
}
