#include <GL/glew.h>

#include <GL/freeglut.h>
#include <iostream>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

#include "..\..\src\GLSLShader.h"

#define GL_CHECK_ERRORS assert(glGetError()== GL_NO_ERROR);

#pragma comment(lib, "glew32.lib")

using namespace std;

//rozmiar okna
const int WIDTH  = 1280;
const int HEIGHT = 960;

//niezbędne shadery
GLSLShader shader;
//shader renderujący punkty
GLSLShader pointShader;

//tablica i bufory wierzchołków
GLuint vaoID;
GLuint vboVerticesID;
GLuint vboIndicesID;

//tablica i bufory wierzchołków bryły widzenia
GLuint vaoFrustumID;
GLuint vboFrustumVerticesID;
GLuint vboFrustumIndicesID;

const int NUM_X = 40; //liczba czworokątów wzdłuż osi X
const int NUM_Z = 40; //liczba czworokątów wzdłuż osi Z

const float SIZE_X = 100; //rozmiar płaszczyzny w przestrzeni świata
const float SIZE_Z = 100;
const float HALF_SIZE_X = SIZE_X/2.0f;
const float HALF_SIZE_Z = SIZE_Z/2.0f;

//całkowita liczba wierzchołków i indeksów
glm::vec3 vertices[(NUM_X+1)*(NUM_Z+1)];
const int TOTAL_INDICES = NUM_X*NUM_Z*2*3;
GLushort indices[TOTAL_INDICES];

//do ustalania dokładności obliczeń
const float EPSILON = 0.001f;
const float EPSILON2 = EPSILON*EPSILON;

//zmienne transformacyjne kamery
int state = 0, oldX=0, oldY=0;
float rX=-135, rY=45, fov = 45;

#include "..\..\src\FreeCamera.h"
 
//kody klawiszy
const int VK_W = 0x57;
const int VK_S = 0x53;
const int VK_A = 0x41;
const int VK_D = 0x44;
const int VK_Q = 0x51;
const int VK_Z = 0x5a;

//przyrost czasu
float dt = 0;

//zmienne związane z upływem czasu
float last_time=0, current_time =0;


//dwie kamery swobodne i wskaźnik do aktywnej
CFreeCamera cam;
CFreeCamera world;
CFreeCamera* pCurrentCam;

//zmienne związane z wygładzaniem ruchów myszy
const float MOUSE_FILTER_WEIGHT=0.75f;
const int MOUSE_HISTORY_BUFFER_SIZE = 10;

//bufor historii położeń myszy
glm::vec2 mouseHistory[MOUSE_HISTORY_BUFFER_SIZE];

float mouseX=0, mouseY=0; //filtered mouse values

//wyłącznik wygładzania ruchów myszy
bool useFiltering = true;

//wierzchołki bryły widzenia
glm::vec3 frustum_vertices[8];

//stałe kolory
GLfloat white[4] = {1,1,1,1};
GLfloat red[4] = {1,0,0,0.5};
GLfloat cyan[4] = {0,1,1,0.5};

//punkty
const int PX = 100;
const int PZ = 100;
const int MAX_POINTS=PX*PZ;

//wierzchołki punktów, tablica wierzchołków obiekty bufora wierzchołków
glm::vec3 pointVertices[MAX_POINTS];
GLuint pointVAOID, pointVBOID;


//liczba punktów widocznych
int total_visible=0;

//zapytanie  
GLuint query;

//zmienne związane z szybkością animacji
float start_time = 0;
float fps=0;
int total_frames;
float last_time_fps=0;

//bufor tekstowy do wyświetlania komunikatów
char buffer[MAX_PATH]={'\0'};

//funkcja wygładzająca ruchy myszy
void filterMouseMoves(float dx, float dy) {
    for (int i = MOUSE_HISTORY_BUFFER_SIZE - 1; i > 0; --i) {
        mouseHistory[i] = mouseHistory[i - 1];
    }

    // zapisywanie bieżącego położenia myszy do bufora historii.
    mouseHistory[0] = glm::vec2(dx, dy);

    float averageX = 0.0f;
    float averageY = 0.0f;
    float averageTotal = 0.0f;
    float currentWeight = 1.0f;

    // wygładzanie
    for (int i = 0; i < MOUSE_HISTORY_BUFFER_SIZE; ++i)
    {
		glm::vec2 tmp=mouseHistory[i];
        averageX += tmp.x * currentWeight;
        averageY += tmp.y * currentWeight;
        averageTotal += 1.0f * currentWeight;
        currentWeight *= MOUSE_FILTER_WEIGHT;
    }

    mouseX = averageX / averageTotal;
    mouseY = averageY / averageTotal;

}

//obsługa kliknięcia przyciskiem myszy
void OnMouseDown(int button, int s, int x, int y)
{
	if (s == GLUT_DOWN)
	{
		oldX = x;
		oldY = y;
	}

	if(button == GLUT_MIDDLE_BUTTON)
		state = 0;
	else if(button == GLUT_RIGHT_BUTTON)
		state = 2;
	else
		state = 1;
}

//obsługa ruchów myszy
void OnMouseMove(int x, int y)
{
	bool changed = false;
	if (state == 0) {
		fov += (y - oldY)/5.0f;
		pCurrentCam->SetFOV(fov);
		changed = true;
	} else if(state == 1) {
		rY += (y - oldY)/5.0f;
		rX += (oldX-x)/5.0f;
		if(useFiltering)
			filterMouseMoves(rX, rY);
		else {
			mouseX = rX;
			mouseY = rY;
		}
		if(pCurrentCam == &world) {
			cam.Rotate(mouseX, mouseY,0);
			cam.CalcFrustumPlanes();
		} else {
			pCurrentCam->Rotate(mouseX,mouseY, 0);
		}
		changed = true;
	}
	oldX = x;
	oldY = y;

	if(changed) {
		pCurrentCam->CalcFrustumPlanes();
		frustum_vertices[0] = cam.farPts[0];
		frustum_vertices[1] = cam.farPts[1];
		frustum_vertices[2] = cam.farPts[2];
		frustum_vertices[3] = cam.farPts[3];

		frustum_vertices[4] = cam.nearPts[0];
		frustum_vertices[5] = cam.nearPts[1];
		frustum_vertices[6] = cam.nearPts[2];
		frustum_vertices[7] = cam.nearPts[3];

		//aktualizacja wierzchołków bryły widzenia
		glBindVertexArray(vaoFrustumID);
			glBindBuffer (GL_ARRAY_BUFFER, vboFrustumVerticesID);
			glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(frustum_vertices), &frustum_vertices[0]);
		glBindVertexArray(0);
	}

	glutPostRedisplay();
}
void OnInit() {
	//generowanie zapytania
	glGenQueries(1, &query);

	//włączenie trybu rysowania konturów wielokątów
	glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
	//włączenie testu głębi
	glEnable(GL_DEPTH_TEST);

	//rozmiar punktu
	glPointSize(10);

	GL_CHECK_ERRORS

	//wczytanie shaderów
	shader.LoadFromFile(GL_VERTEX_SHADER, "shadery/shader.vert");
	shader.LoadFromFile(GL_FRAGMENT_SHADER, "shadery/shader.frag");
	//kompilacja i konsolidacja programu shaderowego
	shader.CreateAndLinkProgram();
	shader.Use();
		//atrybuty i uniformy
		shader.AddAttribute("vVertex");
		shader.AddUniform("MVP");
		shader.AddUniform("color");
		//inicjalizacja uniformów
		glUniform4fv(shader("color"),1,white);
	shader.UnUse();

	GL_CHECK_ERRORS

	//wczytanie schaderów dla punktów
	pointShader.LoadFromFile(GL_VERTEX_SHADER, "shadery/points.vert");
	pointShader.LoadFromFile(GL_GEOMETRY_SHADER, "shadery/points.geom");
	pointShader.LoadFromFile(GL_FRAGMENT_SHADER, "shadery/points.frag");
	//kompilacja i konsolidacja programu shaderowego
	pointShader.CreateAndLinkProgram();
	pointShader.Use();
		//atrybuty i uniformy
		pointShader.AddAttribute("vVertex");
		pointShader.AddUniform("MVP");
		pointShader.AddUniform("t");
		pointShader.AddUniform("FrustumPlanes");
	pointShader.UnUse();

	GL_CHECK_ERRORS

	//przygotowanie geometrii podłoża
	//przygotowanie wierzchołków
	int count = 0;
	int i=0, j=0;
	for( j=0;j<=NUM_Z;j++) {
		for( i=0;i<=NUM_X;i++) {
			vertices[count++] = glm::vec3( ((float(i)/(NUM_X-1)) *2-1)* HALF_SIZE_X, 0, ((float(j)/(NUM_Z-1))*2-1)*HALF_SIZE_Z);
		}
	}

	//wypełnienie tablicy indeksów
	GLushort* id=&indices[0];
	for (i = 0; i < NUM_Z; i++) {
		for (j = 0; j < NUM_X; j++) {
			int i0 = i * (NUM_X+1) + j;
			int i1 = i0 + 1;
			int i2 = i0 + (NUM_X+1);
			int i3 = i2 + 1;
			if ((j+i)%2) {
				*id++ = i0; *id++ = i2; *id++ = i1;
				*id++ = i1; *id++ = i2; *id++ = i3;
			} else {
				*id++ = i0; *id++ = i2; *id++ = i3;
				*id++ = i0; *id++ = i3; *id++ = i1;
			}
		}
	}

	GL_CHECK_ERRORS

	//VAO iVBO dla podłoża
	glGenVertexArrays(1, &vaoID);
	glGenBuffers(1, &vboVerticesID);
	glGenBuffers(1, &vboIndicesID);

	glBindVertexArray(vaoID);

		glBindBuffer (GL_ARRAY_BUFFER, vboVerticesID);
		//przekazanie wierzchołków do obiektu bufora
		glBufferData (GL_ARRAY_BUFFER, sizeof(vertices), &vertices[0], GL_STATIC_DRAW);
		GL_CHECK_ERRORS
		//tablica atrybutów wierzchołka dla położenia
		glEnableVertexAttribArray(shader["vVertex"]);
		glVertexAttribPointer(shader["vVertex"], 3, GL_FLOAT, GL_FALSE,0,0);
		GL_CHECK_ERRORS
		//przekazanie indeksów do bufora tablicy elementów
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndicesID);
		glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), &indices[0], GL_STATIC_DRAW);
		GL_CHECK_ERRORS

	//ustalenie położenia i orientacji kamery
	cam.SetPosition(glm::vec3(2,2,2));
	cam.Rotate(rX,rY,0);
	//ustawienie parametrów rzutowania dla kamery i aktualizacja macierzy
	cam.SetupProjection(fov, ((GLfloat)WIDTH)/HEIGHT,1.f,10);
	cam.Update();

	//wyznaczenie ścian bryły widzenia dla kamery
	cam.CalcFrustumPlanes();

	//ustalenie położenia i orientacji kamery w przestrzeni świata
	world.SetPosition(glm::vec3(10,10,10));
	world.Rotate(rX,rY,0);
	//ustawienie rzutowania i aktualizacja parametrów kamery
	world.SetupProjection(fov,(GLfloat)WIDTH/HEIGHT, 0.1f, 100.0f);
	world.Update();

	//obiekt cam kamerą aktywną
	pCurrentCam = &cam;

	//przygotowanie geometrii bryły widzenia
	glGenVertexArrays(1, &vaoFrustumID);
	glGenBuffers(1, &vboFrustumVerticesID);
	glGenBuffers(1, &vboFrustumIndicesID);

	//zapisywanie wierzchołków bryły
	frustum_vertices[0] = cam.farPts[0];
	frustum_vertices[1] = cam.farPts[1];
	frustum_vertices[2] = cam.farPts[2];
	frustum_vertices[3] = cam.farPts[3];

	frustum_vertices[4] = cam.nearPts[0];
	frustum_vertices[5] = cam.nearPts[1];
	frustum_vertices[6] = cam.nearPts[2];
	frustum_vertices[7] = cam.nearPts[3];

	GLushort frustum_indices[36]={0,4,3,3,4,7, //górna
								  6,5,1,6,1,2, //dolna
								  0,1,4,4,1,5, //lewa
								  7,6,3,3,6,2, //prawa
								  4,5,6,4,6,7, //bliska
								  3,2,0,0,2,1, //daleka
								  };
	glBindVertexArray(vaoFrustumID);

		glBindBuffer (GL_ARRAY_BUFFER, vboFrustumVerticesID);
		//przekazanie wierzchołków bryły do obiektu bufora
		glBufferData (GL_ARRAY_BUFFER, sizeof(frustum_vertices), &frustum_vertices[0], GL_DYNAMIC_DRAW);
		GL_CHECK_ERRORS
		//tablica atrybutów wierzchołka dla położenia
		glEnableVertexAttribArray(shader["vVertex"]);
		glVertexAttribPointer(shader["vVertex"], 3, GL_FLOAT, GL_FALSE,0,0);
		GL_CHECK_ERRORS
		//przekazanie indeksów do bufora tablicy elementów
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboFrustumIndicesID);
		glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(frustum_indices), &frustum_indices[0], GL_STATIC_DRAW);
		GL_CHECK_ERRORS
			 

	//przygotowanie zestawu punktów
	for(int j=0;j<PZ;j++) {
		for(int i=0;i<PX;i++) {
			float   x = i/(PX-1.0f);
			float   z = j/(PZ-1.0f); 
			pointVertices[j*PX+i] = glm::vec3(x,0,z);
		}
	}
	//VAO i VBO dla punktów
	glGenVertexArrays(1, &pointVAOID);
	glGenBuffers(1, &pointVBOID);
	glBindVertexArray(pointVAOID);
	glBindBuffer (GL_ARRAY_BUFFER, pointVBOID);
	//przekazanie wierzchołków do obiektu bufora
	glBufferData (GL_ARRAY_BUFFER, sizeof(pointVertices), pointVertices, GL_STATIC_DRAW);

	//tablica atrybutów wierzchołka dla położenia
	glEnableVertexAttribArray(pointShader["vVertex"]);
	glVertexAttribPointer(pointShader["vVertex"], 3, GL_FLOAT, GL_FALSE,0,0);

	// pobranie kierunku patrzenia kamery w celu określenia wartości parametrów yaw i pitch
	glm::vec3 look =  glm::normalize(cam.GetPosition());
	float yaw = glm::degrees(float(atan2(look.z, look.x)+M_PI));
	float pitch = glm::degrees(asin(look.y));
	rX = yaw;
	rY = pitch;

	//wypełnianie bufora historii położeń myszy, jeśli wygładzanie jest włączone 
	if(useFiltering) {
		for (int i = 0; i < MOUSE_HISTORY_BUFFER_SIZE ; ++i) {
			mouseHistory[i] = glm::vec2(rX, rY);
		}
	}

	cout<<"Inicjalizacja powiodla sie"<<endl;

	//czas początkowy
	start_time = glutGet(GLUT_ELAPSED_TIME)/1000.0f;
}

//zwalnianie wszystkich alokowanych zasobów
void OnShutdown() {
	glDeleteQueries(1, &query);

	//likwidacja programu shaderowego
	shader.DeleteShaderProgram();
	pointShader.DeleteShaderProgram();

	//likwidacja vao i vbo dla wierzchołków
	glDeleteBuffers(1, &vboVerticesID);
	glDeleteBuffers(1, &vboIndicesID);
	glDeleteVertexArrays(1, &vaoID);

	//likwidacja vao i vbo dla bryły widzenia
	glDeleteVertexArrays(1, &vaoFrustumID);
	glDeleteBuffers(1, &vboFrustumVerticesID);
	glDeleteBuffers(1, &vboFrustumIndicesID);

	//likwidacja vao i vbo dla punktów
	glDeleteVertexArrays(1, &pointVAOID);
	glDeleteBuffers(1, &pointVBOID);

	cout<<"Zamkniecie powiodlo sie"<<endl;
}

//obsługa zmiany wymiarów okna
void OnResize(int w, int h) {
	//wymiary okna widokowego
	glViewport (0, 0, (GLsizei) w, (GLsizei) h);
}

//reakcja na sygnał bezczynności procesora
void OnIdle() {

	//obsługa klawiszy W,S,A,D,Q i Z służących do poruszania kamerą
	if( GetAsyncKeyState(VK_W) & 0x8000) {
		pCurrentCam->Walk(dt);
	}

	if( GetAsyncKeyState(VK_S) & 0x8000) {
		pCurrentCam->Walk(-dt);
	}
	if( GetAsyncKeyState(VK_A) & 0x8000) {
		pCurrentCam->Strafe(-dt);
	}

	if( GetAsyncKeyState(VK_D) & 0x8000) {
		pCurrentCam->Strafe(dt);
	}

	if( GetAsyncKeyState(VK_Q) & 0x8000) {
		pCurrentCam->Lift(dt);
	}

	if( GetAsyncKeyState(VK_Z) & 0x8000) {
		pCurrentCam->Lift(-dt);
	}

	glm::vec3 t = pCurrentCam->GetTranslation(); 

	if(glm::dot(t,t)>EPSILON2) {
		pCurrentCam->SetTranslation(t*0.95f);
	}

	//aktualizacja bryły widzenia
	if(pCurrentCam == &cam) {
		//ustalenie nowych wierzchołków bryły widzenia
		pCurrentCam->CalcFrustumPlanes();
		frustum_vertices[0] = cam.farPts[0];
		frustum_vertices[1] = cam.farPts[1];
		frustum_vertices[2] = cam.farPts[2];
		frustum_vertices[3] = cam.farPts[3];

		frustum_vertices[4] = cam.nearPts[0];
		frustum_vertices[5] = cam.nearPts[1];
		frustum_vertices[6] = cam.nearPts[2];
		frustum_vertices[7] = cam.nearPts[3];

		//przekazanie wierzchołków bryły widzenia do GPU
		glBindVertexArray(vaoFrustumID);
			glBindBuffer (GL_ARRAY_BUFFER, vboFrustumVerticesID);
			glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(frustum_vertices), &frustum_vertices[0]);
		glBindVertexArray(0);
	}

	//wywołanie funkcji wyświetlającej
	glutPostRedisplay();
}

//funkcja zwrotna wyświetlania
void OnRender() {
	//obliczenia związane z szybkością animacji
	++total_frames;
	last_time = current_time;
	current_time = glutGet(GLUT_ELAPSED_TIME)/1000.0f;
	dt = current_time-last_time;
	if( (current_time-last_time_fps) >1) {
		fps = total_frames/(current_time-last_time_fps);
		last_time_fps = current_time;
		total_frames = 0;
	}

	//czyszczenie buforów koloru i głębi
	glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

	//transformacje kamery
	glm::mat4 MV	= pCurrentCam->GetViewMatrix();
	glm::mat4 P     = pCurrentCam->GetProjectionMatrix();
    glm::mat4 MVP	= P*MV;

	//pobieranie ścian bryły widzenia
	glm::vec4 p[6];
	pCurrentCam->GetFrustumPlanes(p);

	//początek zapytania
	glBeginQuery(GL_PRIMITIVES_GENERATED, query);

	//wiązanie shadera punktów
	pointShader.Use();
		//ustalanie uniformów shadera
		glUniform1f(pointShader("t"), current_time);
		glUniformMatrix4fv(pointShader("MVP"), 1, GL_FALSE, glm::value_ptr(MVP));
		glUniform4fv(pointShader("FrustumPlanes"), 6, glm::value_ptr(p[0]));

		//wiązanie obiektu tablicy wierzchołków dla punktów
		glBindVertexArray(pointVAOID);
			//rysowanie punktów
			glDrawArrays(GL_POINTS,0,MAX_POINTS);

	//odwiązywanie shadera punktów
	pointShader.UnUse();

	//koniec zapytania
	glEndQuery(GL_PRIMITIVES_GENERATED);

	//ustalenie liczby widocznych punktów na podstawie wyniku zapytania
	GLuint res;
	glGetQueryObjectuiv(query, GL_QUERY_RESULT, &res);
	sprintf_s(buffer, "FPS: %3.3f :: Liczba widocznych punktów: %3d",fps, res);
	glutSetWindowTitle(buffer);

	//shader podłoża
	shader.Use();
	//wiązanie obiektu tablicy wierzchołków dla podłoża
	glBindVertexArray(vaoID);
		//ustalanie uniformów shadera
		glUniformMatrix4fv(shader("MVP"), 1, GL_FALSE, glm::value_ptr(MVP));
		glUniform4fv(shader("color"),1,white);
			//rysowanie trójkątów
			glDrawElements(GL_TRIANGLES, TOTAL_INDICES, GL_UNSIGNED_SHORT, 0);
			
	//rysowanie bryły widzenia kamery lokalnej, gdy aktywna jest kamera globalna
	if(pCurrentCam == &world) {
		//ustalanie uniformów shadera
		glUniform4fv(shader("color"),1,red);

		//wiązanie obiektu tablicy wierzchołków dla bryły widzenia
	 	glBindVertexArray(vaoFrustumID);

		//włączenie mieszania alfa
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

		//włączenie trybu rysowania pełnych wielokątów
		glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
			//rysowanie trójkątów
	 		glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_SHORT, 0);
		//przywrócenie trybu rysowania konturów
		glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

		//wyłączenie mieszania
		glDisable(GL_BLEND);
	}

	//odwiązanie shadera
	shader.UnUse();

	//zamiana buforów w celu wyświetlenia obrazu
	glutSwapBuffers();
}

//obsługa klawiszy służących do przełączania kamer 
void OnKey(unsigned char key, int x, int y) {
	switch(key) {
		case '1':
			pCurrentCam = &cam;
		break;

		case '2':
			pCurrentCam = &world;
		break;
	}
	glutPostRedisplay();
}


int main(int argc, char** argv) {
	//inicjalizacja freeglut
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
	glutInitContextVersion (3, 3);
	glutInitContextFlags (GLUT_CORE_PROFILE | GLUT_DEBUG);
	glutInitWindowSize(WIDTH, HEIGHT);
	glutCreateWindow("Free Camera - OpenGL 3.3");

	//inicjalizacja glew
	glewExperimental = GL_TRUE;
	GLenum err = glewInit();
	if (GLEW_OK != err)	{
		cerr<<"Blad: "<<glewGetErrorString(err)<<endl;
	} else {
		if (GLEW_VERSION_3_3)
		{
			cout<<"Sterownik obsluguje OpenGL 3.3\nDetails:"<<endl;
		}
	}
	err = glGetError(); //w celu ignorowania błędu 1282 INVALID ENUM
	GL_CHECK_ERRORS

	//wyprowadzanie informacji na ekran
	cout<<"\tWersja GLEW "<<glewGetString(GLEW_VERSION)<<endl;
	cout<<"\tProducent: "<<glGetString (GL_VENDOR)<<endl;
	cout<<"\tRenderer: "<<glGetString (GL_RENDERER)<<endl;
	cout<<"\tWersja OpenGL: "<<glGetString (GL_VERSION)<<endl;
	cout<<"\tGLSL: "<<glGetString (GL_SHADING_LANGUAGE_VERSION)<<endl;

	GL_CHECK_ERRORS

	//inicjalizacja OpenGL
	OnInit();
	
	//callback hooks
	glutCloseFunc(OnShutdown);
	glutDisplayFunc(OnRender);
	glutReshapeFunc(OnResize);
	glutMouseFunc(OnMouseDown);
	glutMotionFunc(OnMouseMove);
	glutKeyboardFunc(OnKey);
	glutIdleFunc(OnIdle);

	//wywołanie pętli głównej
	glutMainLoop();

	return 0;
}