#include "nGraph.h"
#include "glObjects.h"
#include "vector.h"
#include "camera.h"
#include "point.h"
#include "edge.h"
#include "input.h"
#include "menu.h"

// Global Variables - this stuff is fine for now but change it later
int window_width = DEFAULT_WINDOW_WIDTH;
int window_height = DEFAULT_WINDOW_HEIGHT;
int bits_per_pixel = DEFAULT_BITS_PER_PIXEL;
int number_graph_points = NUMBER_GRAPH_POINTS;
bool fullscreen = DEFAULT_FULLSCREEN;
int max_fps = MAX_FPS;
std::string start_letter;
int field_of_view = 90;
int currentPoint = 0;
bool foundItem = false;

// Time storage
static Uint32 lastFrameTime;
static Uint32 searchFinishTime;

// Global Program Objects
ngObjects::Input user(window_height, window_width, field_of_view, 24);
ngObjects::Menu menu;
ngObjects::Font infoFont();
std::vector <ngObjects::Point> points;
std::vector <ngObjects::Edge> edges;
std::queue <int> pointList; // Create list, start at startPoint

int minUnknownVertex()
{
	int minVertex = -1;
	int minDistance = INT_MAX;
	for (int i = 0; i < (int)points.size(); i++)
	{
		if (!points.at(i).getMarker() && points.at(i).getDistance() < minDistance)
		{
			minVertex = i;
			minDistance = points.at(i).getDistance();
		}
	}
	return minVertex;
}

void dijkstra()
{
	int v;

	for (int i = 0; i < (int)points.size(); i++) // Initalization
	{
		points.at(i).setDistance(INT_MAX);
		points.at(i).setPath(-1);
		points.at(i).setMarker(false);
	}
	points.at(currentPoint).setDistance(0); // Set distance of the starting point to 0
	for (int i = 0; i < (int)points.size(); i++)
	{
		v = minUnknownVertex(); // Find an unknown vertex with the least distance
		points.at(v).setMarker(true); // Set distance known to be true
		std::vector <int> e(points.at(v).getConnections()); // Get the connected edges for this point
		for (int j = 0; j < (int)e.size(); j++)
		{
			std::vector <int> endPoints(edges.at(e.at(j)).getPoints());
			int endEdgePoint;
			if (endPoints.at(0) == v)
				endEdgePoint = endPoints.at(1);
			else if (endPoints.at(1) == v)
				endEdgePoint = endPoints.at(0);
			else
				endEdgePoint = 0;
			if (points.at(endEdgePoint).getDistance() > (points.at(v).getDistance() + edges.at(e.at(j)).getWeight()))
			{
				points.at(endEdgePoint).setDistance(points.at(v).getDistance() + edges.at(e.at(j)).getWeight());
				points.at(endEdgePoint).setPath(v);
			}
		}

	}
}

void initSDL(int w, int h, int bpp) // Inits SDL and creates OpenGL window
{
	if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER) < 0)
	{
		std::cerr << "SDL: Could not initialize" << std::endl;
		exit(1);
	}

	// Clears out SDL on program termination
	atexit(SDL_Quit);

	// Set our :cool: window title and icon
	SDL_WM_SetCaption(PROGRAM_TITLE, "");
	SDL_ShowCursor(SDL_DISABLE);

	if (fullscreen)
	{
		putenv("SDL_VIDEO_CENTERED=1");
		if (SDL_SetVideoMode(w, h, bpp, SDL_OPENGL | SDL_FULLSCREEN) == 0)
		{
			std::cerr << "SDL: Could not set Video Mode" << std::endl;
			exit(1);
		}
	}
	else
	{
		if (SDL_SetVideoMode(w, h, bpp, SDL_OPENGL) == 0)
		{
			std::cerr << "SDL: Could not set Video Mode" << std::endl;
			exit(1);
		}
	}
}

void initGL(int w, int h, int bpp) // Sets OpenGL settings and creates global lighting
{
	SDL_GL_SetAttribute(SDL_GL_RED_SIZE, bpp / 3);
	SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, bpp / 3);
	SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, bpp / 3);
	SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, bpp);
	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

	glClearColor (0.5, 0.5, 0.5, 0.0);

	glEnable(GL_TEXTURE_2D);
	glShadeModel(GL_SMOOTH);

	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

	glEnable(GL_CULL_FACE);
	glFrontFace(GL_CCW);

	glEnable(GL_LIGHTING);
	glDepthFunc(GL_LEQUAL);
	glEnable(GL_DEPTH_TEST);

	glObjects::glLight globalLightModel;
	globalLightModel.ambient[0] = 0.5f;
	globalLightModel.ambient[1] = 0.5f;
	globalLightModel.ambient[2] = 0.5f;
	globalLightModel.ambient[3] = 1.0f;
	glLightModelfv(GL_LIGHT_MODEL_AMBIENT, globalLightModel.ambient);

	glObjects::glLight point_light;
	point_light.ambient[0] = 0.0f;
	point_light.ambient[1] = 0.0f;
	point_light.ambient[2] = 0.0f;
	point_light.ambient[3] = 1.0f;
	point_light.diffuse[0] = 1.0f;
	point_light.diffuse[1] = 1.0f;
	point_light.diffuse[2] = 1.0f;
	point_light.diffuse[3] = 1.0f;
	point_light.position[0] = -10.0f;
	point_light.position[1] = 1.0f;
	point_light.position[2] = 10.0f;
	point_light.position[3] = 0.0f;
	point_light.specular[0] = 1.0f;
	point_light.specular[1] = 1.0f;
	point_light.specular[2] = 1.0f;
	point_light.specular[3] = 1.0f;
	glEnable(GL_LIGHT0);
}

void resizeWindow(int w, int h, int fov) // Resizes our OpenGL window
{
	glViewport(0, 0, w, h);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	if(h<1) h=1;
	gluPerspective(fov, (double)w/(double)h, 1.0, 500.0);
	glMatrixMode(GL_MODELVIEW);
}

int processEvents(int windowW, int windowH)
{
    SDL_Event event;
    while(SDL_PollEvent(&event)) 
	{
		switch(event.type) 
		{
			case SDL_KEYDOWN:
				if (menu.isActive())
					return menu.processEvent(event);
				else if (event.key.keysym.sym == SDLK_ESCAPE)
					menu.activate(); // Give all event control to menu
				else
					return user.keyDown(&event.key.keysym);
				break;

			case SDL_KEYUP:
				if (menu.isActive())
					return menu.processEvent(event);
				else
					return user.keyUp(&event.key.keysym);

			case SDL_MOUSEBUTTONDOWN:
				if (!menu.isActive())
					user.mouseButton(&event.button);					
				break;
			case SDL_MOUSEBUTTONUP:
				if (!menu.isActive())
					user.mouseButton(&event.button);
				break;

			case SDL_QUIT:
				return NGRAPH_QUIT;
        }
    }

	return 1;
}

int main (int argc, char **argv)
{
	// Initalize Program -----------------------------------------------------------------------------------------------------
	GLfloat rotateX = 0.0, rotateY = -45.0;
	std::string pointLabel;
	int retVal, direction, rowPointNumber = 0, weightStart; // Storage for the return value in process events loop
	
	// Read in configuration file and set settings
	std::fstream inputFile("config.txt", std::ios::in);
	std::string garbage;
	inputFile >> garbage >> number_graph_points;
	inputFile >> garbage >> fullscreen;
	inputFile >> garbage >> max_fps;
	inputFile >> window_width >> garbage >> window_height;
	// Check input from file
	/*if (number_graph_points > 26)
	{
		MessageBox(NULL, "Invalid Resolution in config file!", "ERROR", MB_OK);
		exit(1);
	}
	if (window_width != 640 && window_width != 800 && window_width != 1024 && window_width != 1280)
	{
		MessageBox(NULL, "Invalid Resolution in config file!", "ERROR", MB_OK);
		exit(1);
	}
	if (window_height != 480 && window_height != 600 && window_height != 768 && window_height != 1024)
	{
		MessageBox(NULL, "Invalid Resolution in config file!", "ERROR", MB_OK);
		exit(1);
	}*/
	weightStart = inputFile.tellg(); // Set start point for reading in weights

	// Set up objects
	menu.setWindowSettings(window_width, window_height);
	ngObjects::Camera camera(0.0, 15.0, 0.0, 0.0, 10.0, 0.0, 30.0);
	pointLabel.append(START_LETTER);

	srand(static_cast<unsigned>(time(0)));

	// Run all the needed initialization stuff
	initSDL(window_width, window_height, bits_per_pixel);
	initGL(window_width, window_height, bits_per_pixel);
	resizeWindow(window_width, window_height, field_of_view);

	// Build the graph -------------------------------------------------------------------------------------------------------
	for (int i = 0; i < number_graph_points; i++)
	{
		// Position vector, used for both wire and box
		ngObjects::vector3D pos(0.0, 0.0, 0.0);
		ngObjects::Point newPoint(2.0, pos, 0.996f, 0.619f, 0.254f);

		if (points.size() != 0) // The first point does not need any edges
		{
			pos = points.at(points.size() - 1).getPosition(); // Get the position of the previos point
			
			if ((points.size() % 3) == 0) // New row
			{
				pos.z -= 30.0;
				pos.x = 0.0;
				rowPointNumber = 0;
			}
			else // Create Horizontal Edge and needed connections
			{
				// Set the direction of the edge based on rather the current row is even or odd
				if (((int)(points.size() / 3) % 2) == 0)
					direction = 0;
				else
					direction = 1;

				pos.angle = 90;
				ngObjects::Edge newEdge(0.5, 30.0, pos, 0.0f, 0.0f, 0.0f, 1.0f, 1 + int(20.0 * rand()/(RAND_MAX+1.0)), direction);
				// Create connections on the point and edge objects
				newEdge.addPoint(i - 1);
				newEdge.addPoint(i);
				points.at(i - 1).addNeighbor((int)points.size());
				newPoint.addNeighbor(i - 1);
				points.at(i - 1).addConnection((int)edges.size());
				newPoint.addConnection((int)edges.size());
				edges.push_back(newEdge);
				pos.x += 30.0;
			}
			
			if (points.size() != 1 && points.size() != 2) // Create vertical edge and needed connections
			{
				if (((int)(points.size() / 3) % 2) == 0 && (rowPointNumber == 0 || rowPointNumber == 1)) // Even row 1 or 2
					direction = 1;
				else if (((int)(points.size() / 3) % 2) == 0 && rowPointNumber == 2) // Even row 2
					direction = 0;
				else if (rowPointNumber == 0 || rowPointNumber == 1) // Odd row 1 or 2
					direction = 0;
				else if (rowPointNumber == 2) // Odd row 2
					direction = 1;

				pos.angle = 0;
				ngObjects::Edge newEdge(0.5, 30.0, pos, 0.0f, 0.0f, 0.0f, 1.0f, 1 + int(20.0 * rand()/(RAND_MAX+1.0)), direction);
				// Create connections on the point and edge objects
				newEdge.addPoint(i - 3);
				newEdge.addPoint(i);
				points.at(i - 3).addNeighbor((int)points.size());
				newPoint.addNeighbor(i - 3);
				points.at(i - 3).addConnection((int)edges.size());
				newPoint.addConnection((int)edges.size());
				edges.push_back(newEdge);
			}
		}

		newPoint.setPosition(pos);
		newPoint.setLabel(pointLabel);
		points.push_back(newPoint);
		// Increment label, rowPointNumber and x position
		pointLabel.at(0) = pointLabel.at(0) + 1;
		pos.x += 30.0;
		rowPointNumber ++;
	}

	// Read in weights from file
	std::vector <std::string> startPoint;
	std::vector <std::string> endPoint;
	std::vector <int> weight;
	for (int i = 0; !inputFile.eof(); i++)
	{
		std::string tmpStartPoint, tmpEndPoint;
       int tmpWeight;
		inputFile >> tmpStartPoint >> tmpEndPoint >> tmpWeight;
		startPoint.push_back(tmpStartPoint);
		endPoint.push_back(tmpEndPoint);
		weight.push_back(tmpWeight);
	}
	// Set weight in edge objects
	for (int i = 0; i < (int)edges.size(); i++)
	{
		inputFile.seekg(weightStart, std::ios::beg);
		std::vector <int> edgePoints(edges.at(i).getPoints());
       for (int j = 0; j < (int)startPoint.size(); j++)
	   {
		   if (points.at(edgePoints.at(0)).getLabel() == startPoint.at(j))
		   {
			   if (points.at(edgePoints.at(1)).getLabel() == endPoint.at(j))
				   edges.at(i).setWeight(weight.at(j));
		   }

	   }
	}

	inputFile.close();

	// Main program loop ------------------------------------------------------------------------------------------------
	while(retVal = processEvents(window_width, window_height))
	{
		// Clear out the previous frame and load a fresh matrix
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		glLoadIdentity();

		// Check the return value from the process events to see if we need to do anything
		if (retVal == NGRAPH_SEARCH)
		{
			for (int i = 0; i < (int)edges.size(); i++) // Reset colors on any tainted edges
				edges.at(i).setColor(0.0, 0.0, 0.0, 1.0);
			for (int i = 0; i < (int)points.size(); i++)
				points.at(i).setColor(0.996f, 0.619f, 0.254f);

			for (int i = 0; i < (int)points.size(); i++) // Unmark all points
				points.at(i).setMarker(false);

			pointList.push(currentPoint);
		}
		else if (retVal == NGRAPH_SHORTEST_PATH)
		{
			for (int i = 0; i < (int)edges.size(); i++) // Reset colors on any tainted edges
				edges.at(i).setColor(0.0, 0.0, 0.0, 1.0);
			for (int i = 0; i < (int)points.size(); i++)
				points.at(i).setColor(0.996f, 0.619f, 0.254f);

			dijkstra(); // Build path distance table

			// Start at end point and find path to current point
			char key = menu.getEnteredKey();
			int endPoint;
			for (int i = 0; i < (int)points.size(); i++) // Find the end point based on the label
			{
				std::string label = points.at(i).getLabel();
				if (key == label.at(0))
					endPoint = i;
			}

			while (points.at(endPoint).getLabel() != points.at(currentPoint).getLabel())
			{
				points.at(endPoint).setColor(0.996f, 0.619f, 0.254f);
				std::vector <int> e(points.at(endPoint).getConnections());
				for (int i = 0; i < (int)e.size(); i++)
				{
					std::vector <int> endPoints(edges.at(e.at(i)).getPoints());
					if (endPoints.at(0) == endPoint)
					{
						if (endPoints.at(1) == points.at(endPoint).getPath())
							edges.at(e.at(i)).setColor(0.996f, 0.619f, 0.254f, 1.0);
					}
					else if (endPoints.at(1) == endPoint)
					{
						if (endPoints.at(0) == points.at(endPoint).getPath())
							edges.at(e.at(i)).setColor(0.996f, 0.619f, 0.254f, 1.0);
					}
				}
				endPoint = points.at(endPoint).getPath();
			}
		}

		// Check if we need to visit some points to search
		if (!pointList.empty())
		{
			std::string currentLabel = points.at(pointList.front()).getLabel(); // Grab the label from first item in queue
			if (currentLabel.at(0) != menu.getEnteredKey()) // Check if this is the item we are searching for
			{
				std::vector <int> neighbors(points.at(pointList.front()).getNeighbors());// Get front points neighbors
				for (int i = 0; i < (int)neighbors.size(); i++)	// for all unmarked neighbors of front item in queue
				{													// add them into the queue to be checked
					if (!points.at(neighbors.at(i)).getMarker())	// find the edge number and add to to the highlight edges vector
					{
						points.at(neighbors.at(i)).setMarker(true);
						points.at(neighbors.at(i)).setColor(0.267, 0.396, 0.9412);
						pointList.push(neighbors.at(i));
						int neighbor = neighbors.at(i);
						int frontItem = pointList.front();

						//std::cout << points.at(frontItem).getLabel() << " Added Neighbor " << points.at(neighbor).getLabel() << std::endl; // CONSOLE DEBUG

						for (int j = 0; j < edges.size(); j++) // Find the edge number to highlight
						{
							std::vector <int> edgePoints(edges.at(j).getPoints());
							int leftPoint = edgePoints.at(0);
							if (leftPoint == frontItem) 
							{
								int rightPoint = edgePoints.at(1);
								if (rightPoint == neighbor)
									edges.at(j).setColor(0.267, 0.396, 0.9412, 1.0);
							}
							if (edgePoints.at(1) == frontItem) // Check for reversed points on the edge
							{
								if (leftPoint == neighbor)
									edges.at(j).setColor(0.267, 0.396, 0.9412, 1.0);
							}
						}
					}
				}
				pointList.pop(); // Remove point at the front of the queue
			}
			else // It was the item we were searching for, now change our current item and clear the queue
			{
				foundItem = true;
				currentPoint = pointList.front();
				points.at(currentPoint).setColor(0.9647, 0.3137, 0.1843);
				camera.position = points.at(currentPoint).getPosition();
				camera.position.y = 15.0;
				// Record when the search finished so we can remove the red lines after a little while
				searchFinishTime = SDL_GetTicks();
				int listSize = pointList.size();
				for (int i = 0; i < listSize; i++)
					pointList.pop();
			}
			if (!foundItem)
				searchFinishTime = SDL_GetTicks();
		}

		// Calculate rotation angles based off mouse movement
		user.mouseMotion(&rotateX, &rotateY);

		// Rotate camera based on the calculated angles
		camera.rotateAxisX(rotateX); //DISABLED, things look nicer this way
		camera.rotateAxisY(rotateY);
		camera.update();

		/* REMOVED because I hate it when the graph goes back to how it was
		if (searchFinishTime != 0 && (SDL_GetTicks() - searchFinishTime) >= 10000) // reset the colors after a minute
		{
			for (int i = 0; i < (int)edges.size(); i++)
				edges.at(i).setColor(0.0, 0.0, 0.0, 1.0);
			for (int i = 0; i < (int)points.size(); i++)
				points.at(i).setColor(0.996f, 0.619f, 0.254f);

			searchFinishTime = 0;
		}*/

		// Draw objects on the frame
		for (int i = 0; i < (int)points.size(); i++)
		{
			if (i == currentPoint)
				points.at(i).render();
			else
				points.at(i).render(rotateY);
		}
		for (int i = 0; i < (int)edges.size(); i++)
			edges.at(i).render();

		// Draw the menu if we need to, do this after objects otherwise the matricies get skewed
		if (menu.isActive())
			menu.render();

		// Write frame out to display buffer
		SDL_GL_SwapBuffers();
		lastFrameTime = SDL_GetTicks();
		// Wait before drawing our next frame (CAP AT MAX_FPS)
		SDL_Delay(float(1000 / max_fps));
	}

	return 0;
}
