/////////////////////////////////////////////////////////////////////////////////////
//
// This code is used to teach the course "game engine foundations" in Seneca college
// Developed by Alireza Moghaddam on Sep. 2020 
//
////////////////////////////////////////////////////////////////////////////////////

using namespace std;

#include "vgl.h"
#include "LoadShaders.h"
#include "glm\glm.hpp"
#include "glm\gtc\matrix_transform.hpp"
#include "glm\gtx\rotate_vector.hpp"
#include <iostream>

enum VAO_IDs { Triangles, NumVAOs };
enum Buffer_IDs { ArrayBuffer, NumBuffers };
enum Attrib_IDs { vPosition = 0 };

GLuint VAOs[NumVAOs];
GLuint Buffers[NumBuffers];
GLuint location;
GLuint cam_mat_location;
GLuint proj_mat_location;

const GLuint NumVertices = 16;

//Player motion speed and key controls
float height = 0.8f;
float yaw_speed = 0.1f;
float travel_speed = 60.0f;
float mouse_sensitivity = 0.01f;

//Used for tracking mouse cursor position on screen
int x0 = 0;	
int y_0 = 0;

//Transformation matrices and camera vectors
glm::mat4 model_view;
glm::vec3 cam_pos = glm::vec3(1.0f, 1.0f, 5.0);
glm::vec3 forward_vector = glm::vec3(0, 0, -1);
glm::vec3 up_vector = glm::vec3(0, 1, 0);
glm::vec3 side_vector = glm::cross(up_vector, forward_vector);

//Used to measure time between two frames
int oldTimeSinceStart = 0;
int deltaTime;

//Creating and rendering bunch of objects on the scene to interact with
const int Num_Obstacles = 5;
float obstacle_data[Num_Obstacles][3];


//Helper function to generate a random float number within a range
float randomFloat(float a, float b)
{
	float random = ((float)rand()) / (float)RAND_MAX;
	float diff = b - a;
	float r = random * diff;
	return a + r;
}

// inititializing buffers, coordinates, setting up pipeline, etc.
void init(void)
{
	glEnable(GL_DEPTH_TEST);

	//Randomizing the position and scale of obstacles
	for (int i = 0; i < Num_Obstacles; i++)
	{

		switch (i)
		{
			case 0:
				obstacle_data[i][0] = 1.0;//randomFloat(-50, 50); //X
				obstacle_data[i][1] = 1.0;//randomFloat(-50, 50); //Y
				obstacle_data[i][2] = 1.0;//randomFloat(0.1, 10.0); //Scale
				break;
			case 1:
				obstacle_data[i][0] = 1.5;//randomFloat(-50, 50); //X
				obstacle_data[i][1] = 1.5;//randomFloat(-50, 50); //Y
				obstacle_data[i][2] = 0.5;//randomFloat(0.1, 10.0); //Scale
				break;
			case 2:
				obstacle_data[i][0] = 0.5;//randomFloat(-50, 50); //X
				obstacle_data[i][1] = 0.5;//randomFloat(-50, 50); //Y
				obstacle_data[i][2] = 0.5;//randomFloat(0.1, 10.0); //Scale
				break;
			case 3:
				obstacle_data[i][0] = 1.5;//randomFloat(-50, 50); //X
				obstacle_data[i][1] = 0.5;//randomFloat(-50, 50); //Y
				obstacle_data[i][2] = 0.5;//randomFloat(0.1, 10.0); //Scale
				break;
			case 4:
				obstacle_data[i][0] = 0.5;//randomFloat(-50, 50); //X
				obstacle_data[i][1] = 1.5;//randomFloat(-50, 50); //Y
				obstacle_data[i][2] = 0.5;//randomFloat(0.1, 10.0); //Scale
				break;
		}


	}

	ShaderInfo shaders[] = {
		{ GL_VERTEX_SHADER, "triangles.vert" },
		{ GL_FRAGMENT_SHADER, "triangles.frag" },
		{ GL_NONE, NULL }
	};

	GLuint program = LoadShaders(shaders);
	glUseProgram(program);	//My Pipeline is set up

	GLfloat vertices[NumVertices][3] = {
		{ -0.45, -0.45 ,0.45 }, // Cube
		{ 0.45, -0.45 ,0.45 },
		{ 0.45, 0.45 ,0.45 },
		{ -0.45, 0.45 ,0.45 },
		{ -0.45, -0.45 ,-0.45 },
		{ 0.45, -0.45 ,-0.45 },
		{ 0.45, 0.45 ,-0.45 },
		{ -0.45, 0.45 ,-0.45 },

		{ -100.0, -100.0, 0.0 }, //Plane to walk on and a sky
		{ 100.0, -100.0, 0.0 },
		{ 100.0, 100.0, 0.0 },
		{ -100.0, 100.0, 0.0 },
		{ -100.0, -100.0, 10.0 },
		{ 100.0, -100.0, 10.0 },
		{ 100.0, 100.0, 10.0 },
		{ -100.0, 100.0, 10.0 }

	};

	GLfloat colorData[NumVertices][3] = {
		{ 0,1,1 }, // color for cube vertices
		{ 1,1,0 },
		{ 1,0,0 },
		{ 0,1,0 },
		{ 0,1,0 },
		{ 0,0,1 },
		{ 1,1,1 },
		{ 1,0,1 },

		{ 0,1,0 }, // color for plane vertices
		{ 0,1,0 },
		{ 0,1,0 },
		{ 0,1,0 },
		{ 0,0,1 },
		{ 0,0,1 },
		{ 0,0,1 },
		{ 0,0,1 }

	};

	glGenBuffers(2, Buffers);
	glBindBuffer(GL_ARRAY_BUFFER, Buffers[0]);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
	glBindAttribLocation(program, 0, "vPosition");
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
	glEnableVertexAttribArray(0);

	glBindBuffer(GL_ARRAY_BUFFER, Buffers[1]);
	glBufferData(GL_ARRAY_BUFFER, sizeof(colorData), colorData, GL_STATIC_DRAW);
	glBindAttribLocation(program, 1, "vertexColor");
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
	glEnableVertexAttribArray(1);

	location = glGetUniformLocation(program, "model_matrix");
	cam_mat_location = glGetUniformLocation(program, "camera_matrix");
	proj_mat_location = glGetUniformLocation(program, "projection_matrix");
}

//Helper function to draw a cube
void drawCube(float scale)
{
	GLubyte top_face[] = { 0, 1, 2, 3 };
	GLubyte bottom_face[] = { 4, 5, 6, 7 };
	GLubyte left_face[] = { 0, 4, 7, 3 };
	GLubyte right_face[] = { 1, 5, 6, 2 };
	GLubyte front_face[] = { 2, 3, 7, 6 };
	GLubyte back_face[] = { 2, 3, 7, 6 };
	model_view = glm::scale(model_view, glm::vec3(scale, scale, scale));
	glUniformMatrix4fv(location, 1, GL_FALSE, &model_view[0][0]);
	glDrawElements(GL_QUADS, 4, GL_UNSIGNED_BYTE, top_face);
	glDrawElements(GL_QUADS, 4, GL_UNSIGNED_BYTE, bottom_face);
	glDrawElements(GL_QUADS, 4, GL_UNSIGNED_BYTE, left_face);
	glDrawElements(GL_QUADS, 4, GL_UNSIGNED_BYTE, right_face);
	glDrawElements(GL_QUADS, 4, GL_UNSIGNED_BYTE, front_face);
	glDrawElements(GL_QUADS, 4, GL_UNSIGNED_BYTE, back_face);
}

//Renders level
void draw_level()
{
	//Drawing the floor and the sky
	GLubyte ground[] = { 8, 9, 10, 11 };
	GLubyte sky[] = { 12, 13, 14, 15 };
	glDrawElements(GL_QUADS, 4, GL_UNSIGNED_BYTE, ground);
	glDrawElements(GL_QUADS, 4, GL_UNSIGNED_BYTE, sky);

	//Rendering obstacles obstacles
	for (int i = 0; i < Num_Obstacles; i++)
	{
		model_view = glm::translate(model_view, glm::vec3(obstacle_data[i][0], obstacle_data[i][1], 0.0));
		glUniformMatrix4fv(location, 1, GL_FALSE, &model_view[0][0]);
		drawCube(obstacle_data[i][2]);
		model_view = glm::mat4(1.0);
	}
}

//---------------------------------------------------------------------
//
// display
//
void display(void)
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	model_view = glm::mat4(1.0);

	//We normalize the vectors below. Normalizing means to shrink the length of a vector to 1 (make it a unit vector). 
	//Note: The direction of the vector does not change.

	up_vector = glm::normalize(up_vector);
	forward_vector = glm::normalize(forward_vector);

	glm::vec3 look_at = glm::vec3(cam_pos.x + forward_vector.x, cam_pos.y + forward_vector.y, height);

	glm::mat4 camera_matrix = glm::lookAt(glm::vec3(cam_pos.x, cam_pos.y, cam_pos.z), glm::vec3(look_at.x, look_at.y, look_at.z), up_vector);
	glUniformMatrix4fv(cam_mat_location, 1, GL_FALSE, &camera_matrix[0][0]);

	glm::mat4 proj_matrix = glm::frustum(-0.01f, +0.01f, -0.01f, +0.01f, 0.01f, 100.0f);
	glUniformMatrix4fv(proj_mat_location, 1, GL_FALSE, &proj_matrix[0][0]);

	//Render the level
	draw_level();

	glFlush();
}


void keyboard(unsigned char key, int x, int y)
{
	if (key == 'a')
	{
		//Moving camera along opposit direction of side vector
		cam_pos += glm::cross(up_vector, forward_vector) * travel_speed * ((float)deltaTime) / 1000.0f;
	}
	if (key == 'd')
	{
		//Moving camera along side vector
		cam_pos -= glm::cross(up_vector, forward_vector) * travel_speed * ((float)deltaTime) / 1000.0f;
	}
	if (key == 'w')
	{
		//Moving camera along forward vector. To be more realistic, we use X=V.T equation in physics
		cam_pos += forward_vector * travel_speed * ((float)deltaTime) / 1000.0f;
	}
	if (key == 's')
	{
		//Moving camera along backward vector. To be more realistic, we use X=V.T equation in physics
		cam_pos -= forward_vector * travel_speed * ((float)deltaTime) / 1000.0f;
	}
}

void mouse(int x, int y)
{
	//int delta_x = x - x0;
	//forward_vector = glm::rotate(forward_vector, -delta_x * mouse_sensitivity, up_vector);
	//side_vector = glm::cross(up_vector, forward_vector);
	//cout << x0 << " " << x << " " << delta_x << endl;
	//x0 = x;
	
}

void idle()
{
	//Calculating the delta time between two frames
	//We will use this delta time when moving forward (in keyboard function)
	int timeSinceStart = glutGet(GLUT_ELAPSED_TIME);
	deltaTime = timeSinceStart - oldTimeSinceStart;
	oldTimeSinceStart = timeSinceStart;
	//cout << timeSinceStart << " " << oldTimeSinceStart << " " << deltaTime << endl;
	glutPostRedisplay();
}

//---------------------------------------------------------------------
//
// main
//

int
main(int argc, char** argv)
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA);
	glutInitWindowSize(1024, 1024);
	glutCreateWindow("Camera and Projection");

	glewInit();	//Initializes the glew and prepares the drawing pipeline.

	init();

	glutDisplayFunc(display);

	glutKeyboardFunc(keyboard);

	glutIdleFunc(idle);

	glutPassiveMotionFunc(mouse);

	glutMainLoop();
	
	

}