#include <iostream>
#include <cmath>
#include <cstdio>
#include "gl/glew.h"
#include "gl/freeglut.h"

using namespace std;


const int RENDER_WIDTH = 1024;
const int RENDER_HEIGHT = 768;
const int ShadowMapSize = 512;

float p_camera[3] = {35,25,5};
float l_camera[3] = {0,-7,-25};

float p_light[3] = {3,16,0};
float l_light[3] = {0,0,-5};

float light_mvnt = 30.0f;

GLuint fboId;
GLuint depthTextureId;

GLuint shadowShaderId;
GLuint shadowMapUniform;
GLuint shadowMapStepXUniform;
GLuint shadowMapStepYUniform;


char *textFileRead(char *fn) 
{
	FILE *fp;
	char *content = NULL;
	size_t count=0;

	if (fn != NULL) {
		fopen_s(&fp, fn,"rt");

		if (fp != NULL) {
			fseek(fp, 0, SEEK_END);
			count = ftell(fp);
			rewind(fp);
			if (count > 0) {
				content = (char *)malloc(sizeof(char) * (count+1));
				count = fread(content,sizeof(char),count,fp);
				content[count] = '\0';
			}
			fclose(fp);
		}
	}
	return content;
}

void loadShadowShader()
{
	GLuint v, f;
	char *vs = NULL, *fs = NULL;
	v = glCreateShader(GL_VERTEX_SHADER);
	f = glCreateShader(GL_FRAGMENT_SHADER);

	vs = textFileRead("VertexShader.c");
	fs = textFileRead("FragmentShader.c");

	const char * vv = vs;
	const char * ff = fs;

	glShaderSource(v, 1, &vv,NULL);
	glShaderSource(f, 1, &ff,NULL);

	delete vs;
	delete fs;

	glCompileShader(v);
	glCompileShader(f);
	shadowShaderId = glCreateProgram();

	glAttachShader(shadowShaderId, v);
	glAttachShader(shadowShaderId, f);
	glLinkProgram(shadowShaderId);
	glUseProgram(shadowShaderId);

	shadowMapUniform = glGetUniformLocation(shadowShaderId, "ShadowMap");
	shadowMapStepXUniform = glGetUniformLocation(shadowShaderId, "xPixelOffset");
	shadowMapStepYUniform = glGetUniformLocation(shadowShaderId, "yPixelOffset");
}

void generateShadowFBO()
{
	glGenTextures(1, &depthTextureId);
	glBindTexture(GL_TEXTURE_2D, depthTextureId);
	glTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, ShadowMapSize, ShadowMapSize, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, 0);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	float cp[] = {1,1,1,1};
	glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, cp);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
	glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_INTENSITY);
	glBindTexture(GL_TEXTURE_2D, 0);

	glGenFramebuffers(1, &fboId);
	glBindFramebuffer(GL_FRAMEBUFFER, fboId);
	glDrawBuffer(GL_NONE);
	glReadBuffer(GL_NONE);
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTextureId, 0);
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

void setupMatrices(float position_x,float position_y,float position_z,float lookAt_x,float lookAt_y,float lookAt_z, bool mode)
{
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	if (mode)
		gluPerspective(45, 1.0, 1.0, 1000.0);
	else
		gluPerspective(45, (double)RENDER_WIDTH/RENDER_HEIGHT, 1.0, 1000.0);

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	gluLookAt(position_x,position_y,position_z,lookAt_x,lookAt_y,lookAt_z,0,1,0);
}

void update(void)
{
	p_light[0] = light_mvnt * cos(glutGet(GLUT_ELAPSED_TIME)/1000.0f);
	p_light[2] = light_mvnt * sin(glutGet(GLUT_ELAPSED_TIME)/1000.0f);
}

void setTextureMatrix(void)
{
	static double modelView[16];
	static double projection[16];
	const GLdouble bias[16] = {	
		0.5, 0.0, 0.0, 0.0, 
		0.0, 0.5, 0.0, 0.0,
		0.0, 0.0, 0.5, 0.0,
		0.5, 0.5, 0.5, 1.0};

	// Grab modelview and transformation matrices
	glGetDoublev(GL_MODELVIEW_MATRIX, modelView);
	glGetDoublev(GL_PROJECTION_MATRIX, projection);

	glMatrixMode(GL_TEXTURE);
	glActiveTexture(GL_TEXTURE7);

	glLoadIdentity();	
	glLoadMatrixd(bias);

	// concatating all matrice into one.
	glMultMatrixd (projection);
	glMultMatrixd (modelView);

	// Go back to normal matrix mode
	glMatrixMode(GL_MODELVIEW);
}

void startTranslate(float x,float y,float z)
{
	glPushMatrix();
	glTranslatef(x,y,z);
	
	glMatrixMode(GL_TEXTURE);
	glActiveTexture(GL_TEXTURE7);
	glPushMatrix();
	glTranslatef(x,y,z);
}

void endTranslate()
{
	glPopMatrix();
	glMatrixMode(GL_MODELVIEW);
	glPopMatrix();
}

void drawObjects(void)
{
	// Ground
	glColor4f(0.3f,0.3f,0.3f,1);
	glBegin(GL_QUADS);
	glVertex3f(-45,2,-45);
	glVertex3f(-45,2, 55);
	glVertex3f( 55,2, 55);
	glVertex3f( 55,2,-45);
	glEnd();
	
	glColor4f(0.9f,0.9f,0.9f,1);
	
	// Instead of calling glTranslatef, we need a custom function that also maintain the light matrix
	startTranslate(0,4,-16);
	glutSolidCube(4);
	endTranslate();
	
	startTranslate(0,4,-5);
	glutSolidSphere(4,40,40);
	endTranslate();
}

void renderScene(void) 
{
	update();

	glUseProgram(0);

	//Render Scene => FBO
	glBindFramebuffer(GL_FRAMEBUFFER, fboId);
		glClear( GL_DEPTH_BUFFER_BIT);
		glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
		glViewport(0, 0, ShadowMapSize, ShadowMapSize);
	
		setupMatrices(p_light[0],p_light[1],p_light[2],l_light[0],l_light[1],l_light[2], true);
		glCullFace(GL_FRONT);
		drawObjects();

		setTextureMatrix();
	glBindFramebuffer(GL_FRAMEBUFFER,0);

	glViewport(0,0, (int)RENDER_WIDTH, (int)RENDER_HEIGHT);
	glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); 
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	//Using the shadow shader
	glUseProgram(shadowShaderId);
	glActiveTexture(GL_TEXTURE7);
	glUniform1i(shadowMapUniform, 7);
	glBindTexture(GL_TEXTURE_2D,depthTextureId);
	glUniform1f(shadowMapStepXUniform, 1.0/ ShadowMapSize);
	glUniform1f(shadowMapStepYUniform, 1.0/ ShadowMapSize);

	setupMatrices(p_camera[0],p_camera[1],p_camera[2],l_camera[0],l_camera[1],l_camera[2], false);
	glCullFace(GL_BACK);
	drawObjects();


	// DEBUG only. this piece of code draw the depth buffer onscreen
	glUseProgram(0);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(-RENDER_WIDTH/2,RENDER_WIDTH/2,-RENDER_HEIGHT/2,RENDER_HEIGHT/2,1,20);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	glColor4f(1,1,1,1);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D,depthTextureId);
	glEnable(GL_TEXTURE_2D);
	glTranslated(0,0,-1);
	glBegin(GL_QUADS);
		glTexCoord2d(0,0);glVertex3f(0,0,0);
		glTexCoord2d(1,0);glVertex3f(RENDER_WIDTH/2,0,0);
		glTexCoord2d(1,1);glVertex3f(RENDER_WIDTH/2,RENDER_HEIGHT/2,0);
		glTexCoord2d(0,1);glVertex3f(0,RENDER_HEIGHT/2,0);
	glEnd();
	glDisable(GL_TEXTURE_2D);
	
	glUseProgram(shadowShaderId);
	glutSwapBuffers();
	glutPostRedisplay();
}

void processNormalKeys(unsigned char key, int x, int y) {
	
	if (key == 27) 
		exit(0);
}

int main(int argc, char** argv)
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH);
	glutInitWindowPosition(100,100);
	glutInitWindowSize((int)RENDER_WIDTH, (int)RENDER_HEIGHT);
	glutCreateWindow("GLSL Shadow mapping");

	glewInit();

	generateShadowFBO();
	loadShadowShader();
	
	// This is important, if not here, FBO's depthbuffer won't be populated.
	glEnable(GL_DEPTH_TEST);
	glClearDepth(1.0);
	glDepthFunc(GL_LEQUAL);

	glClearColor(0,0,0,1.0f);	
	glEnable(GL_CULL_FACE);

	glHint(GL_PERSPECTIVE_CORRECTION_HINT,GL_NICEST);


	glutDisplayFunc(renderScene);
	glutKeyboardFunc(processNormalKeys);

	glutMainLoop();
	return 0;
}