#include <string>
#include <vector>
#include <iostream>

#include <GL/glew.h>
#include <GL/glut.h>

#include "VObject.h"
#include "GUIelement.h"
#include "VMenuStarter.h"
#include "VTransferFunctionElement.h"
#include "VSliceElement.h"
#include "VMenu.h"
#include "VRenderMode.h"
#include "VAxisAlignElement.h"
#include "VCamera.h"

using namespace std;

GLfloat light_position[] = { 10.0, 10.0, 0.0, 1.0 };

unsigned int fpsCurrent = 0;
unsigned int fpsCount = 0;
unsigned long tick;

int renderMode; // 1..Levoy, 2..MIP, 3..First Hit, 4..Debug

GLuint p, slice, Levoy, MIP, FH, debug, v, f; //shader

bool change = true;

string filename;

VObject * obj;
VMenu * menu;
VCamera * camera;
vector<GUIelement*> elements;

int width = 800;
int height = 600;

bool dragging = false;
bool alt = false;

float modelview[16];

bool slicing = false;

bool operator < (const VTransferFunctionNode& n1, const VTransferFunctionNode& n2)
{
return n1.x < n2.x;
}

void drawcoordinate(){

	//glUseProgram(0);
	glBegin(GL_LINES);
	//x
	glColor3f(1,0,0);
	glVertex3f(0.0f, 0.0f, 0.0f); // origin of the line
	glVertex3f(3.0f, 0.0f, 0.0f); // end point of the line

	//y
	glColor3f(0,1,0);
	glVertex3f(0.0f, 0.0f, 0.0f); // origin of the line
	glVertex3f(0.0f, 3.0f, 0.0f); // end point of the line

	//z
	glColor3f(0,0,1);
	glVertex3f(0.0f, 0.0f, 0.0f); // origin of the line
	glVertex3f(0.0f, 0.0f, 3.0f); // end point of the line

	glEnd( );
}

void draw_cube() {
	//glPushMatrix();
    //glRotatef( 45.0f, 1.0f, 0.0f, 0.0f );
    //glRotatef( 32.0f, 0.0f, 1.0f, 0.0f );
    glBegin(GL_QUADS);
        glColor3f( 1.0f, 0.0f, 0.0f );      // Front side
        glVertex3f(  2.5f, -2.5f, -2.5f );
        glVertex3f( -2.5f, -2.5f, -2.5f );
        glVertex3f( -2.5f,  2.5f, -2.5f );
        glVertex3f(  2.5f,  2.5f, -2.5f );

        glColor3f( 1.0f, 1.0f, 0.0f );      // Back side
        glVertex3f( -2.5f, -2.5f,  2.5f );
        glVertex3f(  2.5f, -2.5f,  2.5f );
        glVertex3f(  2.5f,  2.5f,  2.5f );
        glVertex3f( -2.5f,  2.5f,  2.5f );

        glColor3f( 0.0f, 1.0f, 0.0f );      // Top
        glVertex3f(  2.5f,  2.5f, -2.5f );
        glVertex3f( -2.5f,  2.5f, -2.5f );
        glVertex3f( -2.5f,  2.5f,  2.5f );
        glVertex3f(  2.5f,  2.5f,  2.5f );

        glColor3f( 0.0f, 1.0f, 1.0f );      // Bottom
        glVertex3f( -2.5f, -2.5f, -2.5f );
        glVertex3f(  2.5f, -2.5f, -2.5f );
        glVertex3f(  2.5f, -2.5f,  2.5f );
        glVertex3f( -2.5f, -2.5f,  2.5f );

        glColor3f( 0.0f, 0.0f, 1.0f );      // Left
        glVertex3f( -2.5f, -2.5f, -2.5f );
        glVertex3f( -2.5f, -2.5f,  2.5f );
        glVertex3f( -2.5f,  2.5f,  2.5f );
        glVertex3f( -2.5f,  2.5f, -2.5f );

        glColor3f( 1.0f, 0.0f, 1.0f );      // Right
        glVertex3f(  2.5f, -2.5f,  2.5f );
        glVertex3f(  2.5f, -2.5f, -2.5f );
        glVertex3f(  2.5f,  2.5f, -2.5f );
        glVertex3f(  2.5f,  2.5f,  2.5f );
    glEnd();
	//glPopMatrix();
}

void loadVolume(string _file){
	menu->showMenu(false);
	menu->loadFile(false);
	filename = _file;
	obj->load(filename);
	VTransferFunctionElement * vtfe = new VTransferFunctionElement(&width,&height,&fpsCount,obj->getHistogram());
	obj->setTexture(vtfe->getTexture());
	VSliceElement * vse = new VSliceElement(&width,&height,&fpsCount, obj->getsliceNo());
	obj->setSliderValue(vse->getSliderValue());
	elements.clear();
	elements.push_back(vtfe);
	elements.push_back(new VRenderMode(&width,&height,&fpsCount,&renderMode));
	elements.push_back(new VAxisAlignElement(&width,&height,&fpsCount,camera));
	elements.push_back(vse);
	elements.push_back(new VMenuStarter(&width,&height,&fpsCount,elements));
}

string textFileRead(string _file){
	FILE * pFile;
	pFile = fopen(_file.c_str(),"r");
	string Data;
	char temp;
	while((temp = fgetc(pFile)) != EOF){
		Data+= temp;
	}
	fclose(pFile);
	return Data;
}

void printInfoLog(GLuint obj)
{
	GLint infologLength = 0;
	GLint charsWritten  = 0;
	char *infoLog;
	
	glGetObjectParameterivARB(obj, GL_OBJECT_INFO_LOG_LENGTH_ARB,
							  &infologLength);
	
	if (infologLength > 0)
	{
		infoLog = (char *)malloc(infologLength);
		glGetInfoLogARB(obj, infologLength, &charsWritten, infoLog);
		printf("%s\n",infoLog);
		free(infoLog);
	}
}

void setFragmentShader(string Filename){
	string vs,fs;

	v = glCreateShader(GL_VERTEX_SHADER);
	f = glCreateShader(GL_FRAGMENT_SHADER);	

	vs = textFileRead("shader/vertex.txt");
	fs = textFileRead("shader/"+Filename);

	const char * vv = vs.c_str();
	const char * ff = fs.c_str();

	glShaderSource(v, 1, &vv,NULL);
	glShaderSource(f, 1, &ff,NULL);

	glCompileShader(v);
	glCompileShader(f);

}

void setShaders(void){
	
	setFragmentShader("frag_Levoy.txt");

	Levoy = glCreateProgram();

	glAttachShader(Levoy,v);
	glAttachShader(Levoy,f);

	glLinkProgram(Levoy);
	glUseProgram(Levoy);
	printInfoLog(v);
	printInfoLog(f);
	printInfoLog(Levoy);


	setFragmentShader("frag_MIP.txt");

	MIP = glCreateProgram();

	glAttachShader(MIP,v);
	glAttachShader(MIP,f);

	glLinkProgram(MIP);
	glUseProgram(MIP);
	printInfoLog(v);
	printInfoLog(f);
	printInfoLog(MIP);

	setFragmentShader("frag_Slice.txt");

	slice = glCreateProgram();

	glAttachShader(slice,v);
	glAttachShader(slice,f);

	glLinkProgram(slice);
	glUseProgram(slice);
	printInfoLog(v);
	printInfoLog(f);
	printInfoLog(slice);

	setFragmentShader("frag_FH.txt");

	FH = glCreateProgram();

	glAttachShader(FH,v);
	glAttachShader(FH,f);

	glLinkProgram(FH);
	glUseProgram(FH);
	printInfoLog(v);
	printInfoLog(f);
	printInfoLog(FH);

	setFragmentShader("frag_debug.txt");

	debug = glCreateProgram();

	glAttachShader(debug,v);
	glAttachShader(debug,f);

	glLinkProgram(debug);
	glUseProgram(debug);
	printInfoLog(v);
	printInfoLog(f);
	printInfoLog(debug);

	glUseProgram(0);
}

void countFPS(int value) {
	char title[120];
	fpsCount = fpsCurrent;
	fpsCurrent = 0;

	_snprintf_s(title, 120, "%s. FPS: %d", "VISLU", fpsCount);
	glutSetWindowTitle(title);
	glutTimerFunc(1000, countFPS, 1);
}

void inputKey(unsigned char key, int x, int y){
	switch (key) {
		case '1': obj->setDirection(0);
			obj->setsliceNo(-1);
			break;
		case '2': obj->setDirection(1); break;
		case '3': obj->setDirection(2); break;
		case '4': obj->setDirection(3); break;
		case '+': obj->changesliceNo(1); break;
		case '-': obj->changesliceNo(-1); break;
		case 'i': cout << "renderMode: " << renderMode << endl; break;
		case 27: if (menu->helpMenu()){
			menu->helpMenu(false);
			menu->menuPos(0);
				 }
				 else if (menu->showMenu()){
					 exit(0);
				 }
				 else {
					 menu->showMenu(true);
					 menu->menuPos(0);
				 }
				 break;
		case 13: 
			if (menu->loadFile()){
				switch (menu->menuPos()){
					case 0: loadVolume("data/lobster.dat");
							break;
					case 1: loadVolume("data/skewed_head.dat");
							break;			
				}
			}
			if (menu->helpMenu()){		//RETURN
				menu->helpMenu(false);
				menu->menuPos(0);
			}
			else if (menu->showMenu()){
				switch (menu->menuPos()){
					case 0: menu->loadFile(true);
							break;
					case 1: menu->helpMenu(true);
							break;
					case 2: exit(0);
							break;				
				}
			}
			break;
	}	
}

void inputKeyUp(unsigned char key, int x, int y){
}

void inputSpecialKey(int key, int x, int y) {
	switch (key) {
		case GLUT_KEY_UP : 
			if (menu->showMenu() || menu->loadFile()){
				menu->decmenuPos();
				if (menu->menuPos() < 0){
					menu->menuPos(0);
				}
			}
			break;
		case GLUT_KEY_DOWN :
			if (menu->showMenu()){
				menu->incmenuPos();
				if (menu->menuPos() > 2){
					menu->menuPos(2);
				}
			}
			if (menu->loadFile()){
				menu->incmenuPos();
				if (menu->menuPos() > 1){
					menu->menuPos(1);
				}
			}
			break;
		case GLUT_KEY_LEFT : 
			break;
		case GLUT_KEY_RIGHT : 
			break;
	}
}

void inputSpecialKeyUp(int key, int x, int y)
{
	switch (key) {
		case GLUT_KEY_UP :
		case GLUT_KEY_DOWN :
			break;
	}
}

void mousepos(int x,int y) {
	for(vector<GUIelement*>::iterator it=elements.begin(); it!=elements.end(); it++) {
		if((*it)->isActive() && (*it)->isInside(x,y))
			(*it)->motion(x,y);
	}
	if (menu->showMenu()){
		menu->setMouse((float)x/width,(float)y/height);
	}
}

void mousedragging(int x,int y) {
	dragging = true;
	bool element = false;
	for(vector<GUIelement*>::iterator it=elements.begin(); it!=elements.end(); it++) {
		if((*it)->isActive() && (*it)->isInside(x,y)) {
			element = true;
			(*it)->drag(x,y);
		}
	}
	if(alt && !element && !slicing) {
		camera->changeDistance(x,y);
		change = true;
	} else if(!alt && !element && !slicing) {
		camera->changeDirection(x,y);
		change = true;
	}
}

void mousebutton(int button, int state, int x, int y) {

	int specialKey = glutGetModifiers();
	if(specialKey == GLUT_ACTIVE_ALT)
		alt = true;
	else alt = false;

    if(button==GLUT_LEFT_BUTTON && state==GLUT_UP) {
		dragging = false;
		bool element = false;
		for(vector<GUIelement*>::iterator it=elements.begin(); it!=elements.end(); it++) {
			if((*it)->isActive() && (*it)->isInside(x,y)) {
				element = true;
				(*it)->released(x,y);
			}
		}
		if(!element)
			camera->deleteFromPoint();
        //cout << "left button release" << endl;
	} else if(button==GLUT_LEFT_BUTTON && state==GLUT_DOWN) {
		bool element = false;
		for(vector<GUIelement*>::iterator it=elements.begin(); it!=elements.end(); it++) {
			if((*it)->isActive() && (*it)->isInside(x,y)) {
				element = true;
				(*it)->pressed(x,y);
			}
		}
		if(!element)
			camera->setFromPoint(x,y);

		//cout << "left button pressed" << endl;
		if(!dragging) { //click

			if (menu->showMenu() || menu->helpMenu() || menu->loadFile()){
				menu->click();
				if (menu->loadFile()){
					switch (menu->menuPos()){
						case 0: loadVolume("data/lobster.dat");
								break;
						case 1: loadVolume("data/skewed_head.dat");
								break;
						case 2: loadVolume("data/beetle.dat");
					}
				}

				if (menu->helpMenu()){		//RETURN
					menu->helpMenu(false);
					menu->menuPos(0);
				}
				else if (menu->showMenu()){
					switch (menu->menuPos()){
						case 0: menu->loadFile(true);
								break;
						case 1: menu->helpMenu(true);
								break;
						case 2: exit(0);
								break;				
					}
				}
			}

		}
	}
}

void reshape(int w, int h) {

    if (h==0)
		h=1;

	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();

	glViewport(0,0,w,h);
	//glOrtho(0,width,height,0,1,20);
	gluPerspective(45.0,(double)w/h,1.0,20.0);

	glMatrixMode(GL_MODELVIEW);

	glLoadIdentity();
	
	camera->setCamera();

	width = w;
	height = h;
}

void load() {
    cout << "starting application...." << endl;
    GLenum err = glewInit();
	if (GLEW_OK != err)
	{
		/* Problem: glewInit failed, something is seriously wrong. */
		fprintf(stderr, "Error: %s\n", glewGetErrorString(err));
	}
	fprintf(stdout, "Status: Using GLEW %s\n", glewGetString(GLEW_VERSION));
    glShadeModel (GL_SMOOTH);

	GLfloat light_ambient[] = { 0.1, 0.1, 0.1, 1.0 }; 
	GLfloat light_diffuse[] = { 0.5, 0.5, 0.5, 1.0 }; 
	GLfloat light_specular[] = { 1.0, 1.0, 1.0, 1.0 }; 
	glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient); 
	glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse); 
	glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular); 
	glLightfv(GL_LIGHT0, GL_POSITION, light_position); 

   GLfloat mat_specular[] = { 1.0, 1.0, 1.0, 1.0 }; 
   GLfloat mat_shininess[] = { 50.0 };
   glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular); 
   glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess);

 //  glEnable(GL_LIGHTING);
   glEnable(GL_LIGHT0);

	obj = new VObject(&p, &change, &width, &height, &slicing, modelview, &renderMode);
	menu = new VMenu();
	camera = new VCamera(&width,&height,5.0f);
}

void draw() {

    GLenum err = glGetError();
	if (err != GL_NO_ERROR)
		cout << "GL Error: " << gluErrorString(err) << endl;

    glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	
	glLightfv(GL_LIGHT0, GL_POSITION, light_position);
	
	camera->setCamera();
	glPushMatrix();
	glRotatef(-90.0,1.0,0.0,0.0);
	glGetFloatv(GL_MODELVIEW_MATRIX, modelview);
	switch (renderMode){
		case 1: p = Levoy;
			break;
		case 2: p = MIP;
			break;
		case 3: p = FH;
			break;
		case 4: p = debug;
			break;
		default: p = 0;
	}
	if (slicing){
		p = slice;
	}
	obj->draw();
	glUseProgram(0);
	//glScalef(0.25,0.25,0.25);
	//draw_cube();
	glPopMatrix();


	GLboolean lightingOn = glIsEnabled(GL_LIGHTING);
	if (lightingOn) glDisable(GL_LIGHTING);

	GLboolean depthOn = glIsEnabled(GL_DEPTH_TEST);
	if (depthOn) glDisable(GL_DEPTH_TEST);
	
	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();
	glOrtho(0, width, height, 0, -100, 100);
	glMatrixMode(GL_MODELVIEW);
	glPushMatrix();
	glLoadIdentity();

	glEnable (GL_BLEND); 
	glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	
	for(vector<GUIelement*>::iterator it=elements.begin(); it!=elements.end(); it++) {
		if((*it)->isActive()||(*it)->isBlend()) {
            (*it)->render();
		}
    }
	if(elements[3]->isActive()) slicing = true;
	else slicing = false;

	glPopMatrix();
	glMatrixMode(GL_PROJECTION);
	glPopMatrix();

	glDisable (GL_BLEND); 
	if (depthOn) glEnable(GL_DEPTH_TEST);
	if (lightingOn) glEnable(GL_LIGHTING);
}

void OnDraw(){
	if (menu->showMenu()){
		menu->draw();
	}
	else {
		draw();
	}
	fpsCurrent++;
	glutSwapBuffers();
}

int main(int argc, char** argv)
{

    glutInit(&argc,argv);
    glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH);
    glutInitWindowSize(width, height);
    glutCreateWindow("Volume Ray Caster");

    load();
	setShaders();

    glutIgnoreKeyRepeat(true);
    glutKeyboardFunc(inputKey);
	glutKeyboardUpFunc(inputKeyUp);
	glutSpecialFunc(inputSpecialKey);
	glutSpecialUpFunc(inputSpecialKeyUp);
    glutMotionFunc(mousedragging); //bewegung während taste gedrückt
	glutMouseFunc(mousebutton); //click
	glutPassiveMotionFunc(mousepos); //bewegung

    glutDisplayFunc(OnDraw);
    glutIdleFunc(OnDraw);
    glutReshapeFunc(reshape);

	glutTimerFunc(1000, countFPS, 1);

    glutMainLoop();

    delete camera;
    delete obj;
    return 0;
}
