/*
Basic diffusion and FRAP simulation
 Jonathan Ward and Francois Nedelec, Copyright EMBL Nov. 2007-2009

 
To compile on the mac:
 g++ main.cc -framework GLUT -framework OpenGL -o frap

To compile on Windows using Cygwin:
 g++ main.cc -lopengl32 -lglut32 -o frap
 
To compile on Linux:
 g++ main.cc -lglut -lopengl -o frap
*/

#include "params.h"
#ifdef __APPLE__
#  include <GLUT/glut.h>
#else
#  include "GL/glut.h"
#endif
#include <list>
#include <vector>
#include <iostream>
#include <math.h>
#include <string.h>

#include "TriangleMesh.h"


//link to the global params
extern ParamsClass params;
extern ActiveMesh mesh;

///whether the simulation is paused (=1) or not (=0)
int pauseSimulation=1;
///how big chunks of steps should be taken between drawing
int stepGranularity=1;

///OpenGL window size -> to recalculate from windows coords to scene coords
int windowSizeX=0, windowSizeY=0;

float uhel_x = 0.0, uhel_y = 0.0;
float zoom = 1.0;

bool MouseActive = false;
int last_x = 0, last_y = 0;

bool showAxes=true;
bool showFrame=false;
bool showBox=true;

bool showWholeMesh=true;
bool showWholeMeshFilled=false;
bool showWholeMeshEdges=false;

std::string path;
int ID;
int frameNo=0;
bool LoadNewMesh(const char* path,const int ID,const int fileNo,const bool keepTrajectories=false);
int RenderMesh(int fileNo);

///manage \e count steps of the simulation and then print and draw
void step(int count)
{
	while ((params.currTime < params.stopTime) && (count > 0))
	{
		LoadNewMesh(path.c_str(),ID,frameNo);
		//position the mesh with its centre aligned with the scene centre
		//keep using the same translation (which had been determined in the main() at startup)
		mesh.TranslateMesh(params.sceneCentre - (mesh.meshPlaygroundSize/2.0f - mesh.meshShift));

		params.currTime += params.incrTime;
		--count;
	}
	
	if (params.currTime >= params.stopTime)
	{
		std::cout << "END OF THE SIMULATION HAS BEEN REACHED\n";
		pauseSimulation=1;
	}

	//std::cout << "\nnow at time: " << params.currTime << "\n";

	//ask GLUT to call the display function
	glutPostRedisplay();
}


///draws a flat frame around the playground
void displayFrame(void)
{
    glColor3f(params.sceneBorderColour.r,
	 			  params.sceneBorderColour.g,
	 			  params.sceneBorderColour.b);
    glLineWidth(1);
    glBegin(GL_LINE_LOOP);
    glVertex3f( params.sceneOffset.x , params.sceneOffset.y , params.sceneCentre.z );
    glVertex3f( params.sceneOffset.x + params.sceneSize.x , params.sceneOffset.y , params.sceneCentre.z );
    glVertex3f( params.sceneOffset.x + params.sceneSize.x , params.sceneOffset.y + params.sceneSize.y , params.sceneCentre.z );
    glVertex3f( params.sceneOffset.x , params.sceneOffset.y + params.sceneSize.y , params.sceneCentre.z );
    glEnd();
}

///draws a frame box around the playground
void displayBox(void)
{
    glColor3f(params.sceneBorderColour.r,
	 			  params.sceneBorderColour.g,
	 			  params.sceneBorderColour.b);
    glLineWidth(1);
	 //front square/plane
    glBegin(GL_LINE_LOOP);
    glVertex3f( params.sceneOffset.x                      , params.sceneOffset.y                      , params.sceneOffset.z );
    glVertex3f( params.sceneOffset.x + params.sceneSize.x , params.sceneOffset.y                      , params.sceneOffset.z );
    glVertex3f( params.sceneOffset.x + params.sceneSize.x , params.sceneOffset.y + params.sceneSize.y , params.sceneOffset.z );
    glVertex3f( params.sceneOffset.x                      , params.sceneOffset.y + params.sceneSize.y , params.sceneOffset.z );
    glEnd();

	 //rear square/plane
    glBegin(GL_LINE_LOOP);
    glVertex3f( params.sceneOffset.x                      , params.sceneOffset.y                      , params.sceneOffset.z + params.sceneSize.z );
    glVertex3f( params.sceneOffset.x + params.sceneSize.x , params.sceneOffset.y                      , params.sceneOffset.z + params.sceneSize.z );
    glVertex3f( params.sceneOffset.x + params.sceneSize.x , params.sceneOffset.y + params.sceneSize.y , params.sceneOffset.z + params.sceneSize.z );
    glVertex3f( params.sceneOffset.x                      , params.sceneOffset.y + params.sceneSize.y , params.sceneOffset.z + params.sceneSize.z );
    glEnd();

	 //sideways "connectors"
	 glBegin(GL_LINES);
    glVertex3f( params.sceneOffset.x                      , params.sceneOffset.y                      , params.sceneOffset.z );
    glVertex3f( params.sceneOffset.x                      , params.sceneOffset.y                      , params.sceneOffset.z + params.sceneSize.z );

    glVertex3f( params.sceneOffset.x + params.sceneSize.x , params.sceneOffset.y                      , params.sceneOffset.z );
    glVertex3f( params.sceneOffset.x + params.sceneSize.x , params.sceneOffset.y                      , params.sceneOffset.z + params.sceneSize.z );

    glVertex3f( params.sceneOffset.x + params.sceneSize.x , params.sceneOffset.y + params.sceneSize.y , params.sceneOffset.z );
    glVertex3f( params.sceneOffset.x + params.sceneSize.x , params.sceneOffset.y + params.sceneSize.y , params.sceneOffset.z + params.sceneSize.z );

    glVertex3f( params.sceneOffset.x                      , params.sceneOffset.y + params.sceneSize.y , params.sceneOffset.z );
    glVertex3f( params.sceneOffset.x                      , params.sceneOffset.y + params.sceneSize.y , params.sceneOffset.z + params.sceneSize.z );
    glEnd();
}

void displayAxes(void)
{
	glLineWidth(2);
	glBegin(GL_LINES);
	glColor3f(1.0,0.0,0.0); //red   -- x axis
	glVertex3f(params.sceneCentre.x,params.sceneCentre.y,params.sceneCentre.z);
	glVertex3f(params.sceneCentre.x+params.sceneSize.x/4.f,params.sceneCentre.y,params.sceneCentre.z);

	glColor3f(0.0,1.0,0.0); //green -- y axis
	glVertex3f(params.sceneCentre.x,params.sceneCentre.y,params.sceneCentre.z);
	glVertex3f(params.sceneCentre.x,params.sceneCentre.y+params.sceneSize.y/4.f,params.sceneCentre.z);

	glColor3f(0.0,0.0,1.0); //blue  -- z axis
	glVertex3f(params.sceneCentre.x,params.sceneCentre.y,params.sceneCentre.z);
	glVertex3f(params.sceneCentre.x,params.sceneCentre.y,params.sceneCentre.z+params.sceneSize.z/4.f);
	glEnd();
}


void ActiveMesh::displayMesh(void)
{
	const float gray=0.6f;
	glColor4f(gray,gray,gray,1.0f);
	glLineWidth(1);

	long unsigned int* id=ID.data();
	for (unsigned int i=0; i < ID.size(); i+=3)
	{
		glBegin(GL_TRIANGLES);
		glNormal3f(norm[i/3].x,norm[i/3].y,norm[i/3].z);
		glVertex3f(Pos[*id].x,Pos[*id].y,Pos[*id].z); ++id;
		glVertex3f(Pos[*id].x,Pos[*id].y,Pos[*id].z); ++id;
		glVertex3f(Pos[*id].x,Pos[*id].y,Pos[*id].z); ++id;
		glEnd();
	}

/**/
	//show filopodia line segments
	glLineWidth(4);
	glBegin(GL_LINES);
	for (size_t f=0; f < Ftree.size(); ++f)
	{
		glColor3f(((f+0)%4)*0.25f,((f+2)%4)*0.25f,((f+1)%4)*0.25f);
		mesh_t& filoMesh=Ftree[f];
		for (unsigned int i=0; i < filoMesh.segFromPoint.size(); ++i)
		{
			glVertex3f(filoMesh.fPoints[filoMesh.segFromPoint[i]].x,
						  filoMesh.fPoints[filoMesh.segFromPoint[i]].y,
						  filoMesh.fPoints[filoMesh.segFromPoint[i]].z);
			glVertex3f(filoMesh.fPoints[filoMesh.segToPoint[i]].x,
						  filoMesh.fPoints[filoMesh.segToPoint[i]].y,
						  filoMesh.fPoints[filoMesh.segToPoint[i]].z);
		}
	}
	glEnd();
	glLineWidth(1);
/**/

	//display filopodia
	for (size_t f=0; f < Ftree.size(); ++f)
	{
		glColor3f(((f+0)%4)*0.25f,((f+2)%4)*0.25f,((f+1)%4)*0.25f);
		//glColor4f(1.f,1.f,1.f,1.0f);
		const mesh_t& filoMesh=Ftree[f];

		const long unsigned int* id=filoMesh.ID.data();
		for (unsigned int i=0; i < filoMesh.ID.size(); i+=3)
		{
			glBegin(GL_TRIANGLES);
			glNormal3f(filoMesh.norm[i/3].x,filoMesh.norm[i/3].y,filoMesh.norm[i/3].z);
			glVertex3f(filoMesh.Pos[*id].x,filoMesh.Pos[*id].y,filoMesh.Pos[*id].z); ++id;
			glVertex3f(filoMesh.Pos[*id].x,filoMesh.Pos[*id].y,filoMesh.Pos[*id].z); ++id;
			glVertex3f(filoMesh.Pos[*id].x,filoMesh.Pos[*id].y,filoMesh.Pos[*id].z); ++id;
			glEnd();
		}
	}
}

void ActiveMesh::displayMeshEdges(void)
{
	const float gray=0.8f;
	glColor4f(gray,gray,gray,1.0f);
	glLineWidth(3);

	long unsigned int* id=ID.data();
	for (unsigned int i=0; i < ID.size(); i+=3)
	{
		glBegin(GL_LINE_LOOP);
		glVertex3f(Pos[*id].x,Pos[*id].y,Pos[*id].z); ++id;
		glVertex3f(Pos[*id].x,Pos[*id].y,Pos[*id].z); ++id;
		glVertex3f(Pos[*id].x,Pos[*id].y,Pos[*id].z); ++id;
		glEnd();
	}
}


void display(void)
{
	glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();
	glTranslatef(params.sceneCentre.x,params.sceneCentre.y,0.f); //to prevent camera entering the object
	glScalef(zoom,zoom,zoom);
	Vector3d<float> eye=params.sceneCentre;
	eye.x+=params.sceneSize.z*cosf(uhel_y)*sinf(uhel_x); //according to some tutorial
	eye.y+=params.sceneSize.z*sinf(uhel_y);
	eye.z+=params.sceneSize.z*cosf(uhel_y)*cosf(uhel_x);
	gluLookAt(eye.x,eye.y,eye.z, //eye
				 params.sceneCentre.x,params.sceneCentre.y,params.sceneCentre.z, //center
	          //0.0, cosf(-uhel_y), sinf(-uhel_y)); // up
	          0.0, 1.0, 0.0); // up

	glEnable(GL_FOG);
	if (showBox) displayBox();
	if (showFrame) displayFrame();
	glDisable(GL_FOG);

	if (showWholeMesh) mesh.displayMesh();
	if (showWholeMeshEdges) mesh.displayMeshEdges();

	if (showAxes) displayAxes();

	glFlush();
	glutSwapBuffers();
}


// called automatically after a certain amount of time
void timer(int)
{
	 //do simulation and then draw
    step(stepGranularity);

    //ask GLUT to call 'timer' again in 'delay' milli-seconds:
	 //in the meantime, you can inspect the drawing :-)
    if (!pauseSimulation) glutTimerFunc(500, timer, 0);
}

void GetSceneViewSize(const int w,const int h,
							float& xVisF, float& xVisT,
							float& yVisF, float& yVisT)
{
	 //what is the desired size of the scene
	 const float xBound=params.sceneSize.x
	 				+ 2.f*params.sceneOuterBorder.x;
	 const float yBound=params.sceneSize.y
	 				+ 2.f*params.sceneOuterBorder.y;

	 xVisF = params.sceneOffset.x - params.sceneOuterBorder.x;
	 yVisF = params.sceneOffset.y - params.sceneOuterBorder.y;
	 xVisT = xBound + xVisF;
	 yVisT = yBound + yVisF;

    if ( (float)w * yBound < (float)h * xBound )		//equals to  w/h < x/y
	 {
	 	//window is closer to the rectangle than the scene shape
		  yVisF *= ((float)h / float(w)) * (xBound/yBound);
        yVisT *= ((float)h / float(w)) * (xBound/yBound);
	 }
	 else
	 {
		  xVisF *= ((float)w / float(h)) * (yBound/xBound);
        xVisT *= ((float)w / float(h)) * (yBound/xBound);
	 }
}


void printKeysHelp(void)
{
	std::cout << "\nKeys:\n";
	std::cout << "ESC or 'q': quit        ENTER: enters empty line on a console\n";
	std::cout << "' ','n','z': simulation goes/stop at various steps\n";
	std::cout << "'r': resets the view\n";
	std::cout << "'o','O': zoom out,in\n";
	std::cout << "'h': displays this help\n";
	std::cout << "'d': displays enabled features,displays status\n";
	std::cout << "'m': toggle display of the mesh\n";
	std::cout << "'M': toggle display of the mesh as wireframe or filled\n";
	std::cout << "'e': toggle display of the mesh wireframe (triangle edges)'\n";
	std::cout << "'v': toggle display of the vertex supporting region\n";
	std::cout << "'V': chooses another vertex\n";
	std::cout << "'s': toggle display of the fitted quadratic surface \n";
	std::cout << "'t': toggle display of the test/debug function\n";
	std::cout << "'a': toggle display of the R,G,B <-> x,y,z axes \n";
	std::cout << "'f': toggle display of the frame/plane az z=0\n";
	std::cout << "'b': toggle display of the bounding box\n";
	std::cout << "'c','C': turns off,on z-buffer (off = render in the order of drawing)\n";
	std::cout << "'x','X': controls for some testing\n";
	std::cout << "'i': not used\n";
}


void printDispayedFeauters(void)
{
	if (pauseSimulation) std::cout << "\nsimulation is now paused\n";
	else std::cout << "\nsimulation is now in progress\n";
	std::cout << "simulation time progress " << params.currTime/params.stopTime*100.f
		<< "%, delta time is " << params.incrTime << " minutes\n";

	std::cout << "viewing: x_ang=" << uhel_x << ", y_ang=" << uhel_y
	          << ", zoom=" << zoom << "\n";

	std::cout << "Currently displayed features:\n";
}


// called when a key is pressed
void keyboard(unsigned char key, int mx, int my)
{
	//required for calculating the mouse position within the scene coords
	float xVisF, yVisF;
	float xVisT, yVisT;
	float sx,sy;

    switch (key) {
        case 27:   //this corresponds to 'escape'
        case 'q':  //quit
            exit(EXIT_SUCCESS);
            break;
			case 13: //Enter key -- just empty lines
				std::cout << std::endl;
				break;

		  //control execution of the simulation
        case ' ':  //pause/unpause, refresh every 10 cycles
				if (params.currTime < params.stopTime) pauseSimulation^=1;
				if (!pauseSimulation)
				{
					stepGranularity=1; //= 1 min
					timer(0);
				}
            break;
        case 'z':  //pause/unpause, refresh every 600 cycles
				if (params.currTime < params.stopTime) pauseSimulation^=1;
				if (!pauseSimulation)
				{
					stepGranularity=100; //= 10 min
					timer(0);
				}
            break;

        case 'p':  //pause, advance by one frame backward
				if (frameNo > 0) --frameNo;
				pauseSimulation=1;
				step(1);
            break;
        case 'n':  //pause, advance by one frame forward
				++frameNo;
				pauseSimulation=1;
				step(1);
            break;

		  //view params
        case 'r':  //set scale 1:1
				uhel_x = 0.0;
				uhel_y = 0.0;
				zoom = 1.0;
				glutReshapeWindow(int(params.imgSizeX),int(params.imgSizeY));
				glutPostRedisplay();
            break;
			case 'o':
				zoom -= 0.2f;
				if (zoom < 0.2f) zoom=0.2f;
				glutPostRedisplay();
				break;
			case 'O':
				zoom += 0.2f;
				glutPostRedisplay();
				break;

			//control hints/help
			case 'h': //print help
				printKeysHelp();
				break;
			case 'd': //print what is displayed
				printDispayedFeauters();
				break;


			//display objects
			case 'm':
				showWholeMesh^=true;
				glutPostRedisplay();
				break;
			case 'M':
				showWholeMeshFilled^=true;
				if (showWholeMeshFilled)
					glPolygonMode(GL_FRONT,GL_FILL);
				else
					glPolygonMode(GL_FRONT,GL_LINE);
				glutPostRedisplay();
				break;
			case 'e':
				showWholeMeshEdges^=true;
				glutPostRedisplay();
				break;
			case 'c':
				glDisable(GL_DEPTH_TEST);
				std::cout << "Z-buffer is OFF\n";
				glutPostRedisplay();
				break;
			case 'C':
				glEnable(GL_DEPTH_TEST);
				std::cout << "Z-buffer is ON\n";
				glutPostRedisplay();
				break;

			//annotation controls
			case 'a':
				showAxes^=true;
				glutPostRedisplay();
				break;
			case 'f':
				showFrame^=true;
				glutPostRedisplay();
				break;
			case 'b':
				showBox^=true;
				glutPostRedisplay();
				break;

			//some testing controls
			case 'x':
				glFrontFace(GL_CW);
				glutPostRedisplay();
				break;
			case 'X':
				glFrontFace(GL_CCW);
				glutPostRedisplay();
				break;

			case 'i': //inspect a cell
				GetSceneViewSize(windowSizeX,windowSizeY, xVisF,xVisT,yVisF,yVisT);
				sx=(float)mx              /(float)windowSizeX * (xVisT-xVisF) + xVisF;
				sy=(float)(windowSizeY-my)/(float)windowSizeY * (yVisT-yVisF) + yVisF;

				xVisF=sx=sy; //just to avoid warning, TODO REMOVE
			/*
				for (c=agents.begin(); c != agents.end(); c++)
					if ((*c)->IsPointInCell(Vector3d<float>(sx,sy,0)))
					{
						bool wasSelected=(*c)->isSelected;
						(*c)->isSelected=true;
						(*c)->ReportState();
						(*c)->isSelected=wasSelected;
					}
			*/
				break;
    }
}


// called when a special key is pressed
void Skeyboard(int key, int mx, int my)
{
	key=mx=my; //TODO REMOVE
	mx=key;    //TODO REMOVE
/*
	//required for calculating the mouse position within the scene coords
	float xVisF, yVisF;
	float xVisT, yVisT;
	float sx,sy;

    switch (key) {
			case GLUT_KEY_LEFT:
				//rotate cell left by 30deg
				GetSceneViewSize(windowSizeX,windowSizeY, xVisF,xVisT,yVisF,yVisT);
				sx=(float)mx              /(float)windowSizeX * (xVisT-xVisF) + xVisF;
				sy=(float)(windowSizeY-my)/(float)windowSizeY * (yVisT-yVisF) + yVisF;

				for (c=agents.begin(); c != agents.end(); c++)
					if ((*c)->IsPointInCell(Vector3d<float>(sx,sy,0)))
					{
						(*c)->desiredDirection += PI/6.f;
						if ((*c)->desiredDirection > PI) 
							(*c)->desiredDirection -= 2.f*PI;
					}
				break;
			case GLUT_KEY_RIGHT:
				//rotate cell left by 30deg
				GetSceneViewSize(windowSizeX,windowSizeY, xVisF,xVisT,yVisF,yVisT);
				sx=(float)mx              /(float)windowSizeX * (xVisT-xVisF) + xVisF;
				sy=(float)(windowSizeY-my)/(float)windowSizeY * (yVisT-yVisF) + yVisF;

				for (c=agents.begin(); c != agents.end(); c++)
					if ((*c)->IsPointInCell(Vector3d<float>(sx,sy,0)))
					{
						(*c)->desiredDirection -= PI/6.f;
						if ((*c)->desiredDirection < -PI) 
							(*c)->desiredDirection += 2.f*PI;
					}
				break;
    }
*/
}

void mouse(int button, int state, int x, int y)
{
	if (state == GLUT_DOWN) MouseActive = true;
	else if (state == GLUT_UP) MouseActive = false;	
	
	last_x=button; //TODO REMOVE
	last_x = x;
	last_y = y;	
}

void MouseMotion(int x, int y) 
{
	if (MouseActive)
	{
		uhel_x -= (float(x - last_x) * 0.01f);
		uhel_y += (float(y - last_y) * 0.01f);
		if (uhel_x > +3.14f) uhel_x=+3.14f;
		if (uhel_x < -3.14f) uhel_x=-3.14f;
		if (uhel_y > +1.57f) uhel_y=+1.57f;
		if (uhel_y < -1.57f) uhel_y=-1.57f;
		
		last_x = x;
		last_y = y;

		glutPostRedisplay();
	}
}


// called when the window is reshaped, arguments are the new window size
void reshape(int w, int h)
{
    glViewport(0, 0, w, h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

	 //remember the new window size...
	 windowSizeX=w; windowSizeY=h;

	 //what will be the final view port
    float xVisF, yVisF;
    float xVisT, yVisT;

	 GetSceneViewSize(w,h,xVisF,xVisT,yVisF,yVisT);
    
    //glOrtho(xVisF, xVisT, yVisF, yVisT, params.sceneCentre.z-params.sceneSize.z,params.sceneCentre.z+params.sceneSize.z);
    glOrtho(xVisF, xVisT, yVisF, yVisT, -1000,1000);
    glMatrixMode(GL_MODELVIEW);
}


void initializeGL(void)
{
	 int Argc=1;
	 char msg[]="meshSurface GUI";
	 char *Argv[10];
	 Argv[0]=msg;

    glutInit(&Argc, Argv);

    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
    glutInitWindowSize(800, 800); 
    glutInitWindowPosition(600, 100);
    glutCreateWindow(Argv[0]);

    glutKeyboardFunc(keyboard);
    glutSpecialFunc(Skeyboard);
    glutDisplayFunc(display);
    glutReshapeFunc(reshape);

    glutMouseFunc(mouse);
    glutMotionFunc(MouseMotion);

    glClearColor(0.0, 0.0, 0.0, 0.0);

	 //enables alfa-blending for every object/drawing
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

	 //filter rendered points (should look nicer)
    glEnable(GL_POINT_SMOOTH);

	 //default displaying mode for triangles
	 //frontfaces are just lines, backfaces are opaque
	 glPolygonMode(GL_BACK, GL_FILL);
	 if (showWholeMeshFilled)
		glPolygonMode(GL_FRONT,GL_FILL);
	 else
		glPolygonMode(GL_FRONT,GL_LINE);

	 //but, well, do not draw backfaces at all
    glEnable(GL_CULL_FACE);
	 glCullFace(GL_BACK);

	 //enable z-buffer;
	 //otherwise it would depend on the order of primitives drawing
	 glEnable(GL_DEPTH_TEST);

	 //set up the fog (is enabled/disabled before/after drawing frames):
	 GLfloat fogColor[4]={0.4f*params.sceneBorderColour.r,
	                      0.4f*params.sceneBorderColour.g,
	                      0.4f*params.sceneBorderColour.b,
								 1.0f}; //has no effect
	 glFogfv(GL_FOG_COLOR,fogColor);
	 //
	 //density is not required in linear mode
	 //glFogf(GL_FOG_DENSITY,0.3f);
	 glFogi(GL_FOG_MODE,GL_LINEAR);
	 //
	 glFogf(GL_FOG_START,params.sceneOffset.z);
	 glFogf(GL_FOG_END,params.sceneOffset.z+params.sceneSize.z);

	 //some OpenGL info, for fun...
#ifndef __APPLE__
	 std::cout << "Vendor  : " << glGetString(GL_VENDOR) << "\n";
	 std::cout << "Renderer: " << glGetString(GL_RENDERER) << "\n";
	 std::cout << "Version : " << glGetString(GL_VERSION) << "\n";
#endif
}

void loopGL(void)
{
	glutMainLoop();
}

void closeGL(void)
{
}
