/* World.cpp
 *
 * Definition of the World class.
 */

#include "World.h"
#include "Path.h"
#include <cassert>
#include <GL/glut.h>



using namespace std;

#define PI 3.14159

const double World::MAX_DT = 0.01;
const double World::DEF_GRAVITY = -60;
const double World::METER_INC = 1.0;
const double World::METER_MULT = 100.0;

World::World(const Tuple& gravity, bool light)
: gravity_(gravity),
  ball_(),
  club_(),
  meter_(),
  meterActive_(false),
  meterIncreasing_(true),
  gameOver_(false),
  light_(light),
  ballDebug_(false),
  strokes_(0),
  obstructions_(),
  camera()
{
	// nothing else to do
}


World::~World()
{
	destroyWorld();
}

void World::destroyWorld()
{
	clearObstructions();
	ball_.clearAll();
	ball_.accelTo(Tuple());
	strokes_ = 0;
	Triangle::setShrinkFactor();
	Triangle::unsetCrazyTris();
	gameOver_ = false;
}

/* clearObstructions
 *
 * Iterates through the obstruction vector and deletes each element before
 * clearing the vector.
 */
void World::clearObstructions()
{
	for (vector<Obstruction*>::const_iterator i = obstructions_.begin();
		 i != obstructions_.end(); ++i)
	{
		delete *i;
	}

	obstructions_.clear();
}

void World::init()
{
	camera.init();
	camera.track(pBallPosition()); // default: Camera follow Ball 
	setupLight();
	club_.initializeGraphics();

	
}

/* draw
 *
 * Draw the ball and all the triangles.
 */
void World::draw()
{
	camera.display(); // must be called first!

	ball_.draw(light_);

	// Only draw club if ball is stopped
	// and we are focused on the ball.
	if (ball_.stopped() && camera.tracking())
	{
		glPushMatrix();
		Tuple dP = ball_.position();
		glTranslatef(dP.x(), dP.y(), dP.z());
		glRotatef(-180.0 * camera.phi() / PI, 0.0, 1.0, 0.0);
		dP = Tuple(-2.5 * ball_.radius(), -1 * ball_.radius(), 2 * ball_.radius());
		glTranslatef(dP.x(), dP.y(), dP.z());
		club_.draw();
		glPopMatrix();
	}

	for (vector<Obstruction*>::const_iterator i = obstructions_.begin();
		 i != obstructions_.end(); ++i)
	{
		(*i)->draw(light_);
	}

	meter_.draw(light_);
}

/* timeStep
 *
 * Do the whole movement and collision shebang.
 */
void World::timeStep(double dt)
{
	if (meterActive_)
	{
		if (meterIncreasing_)
		{
			meter_.incValue(dt * METER_INC);
			if (meter_.value() > 1.0)
			{
				meter_.setValue(1.0);
				meterIncreasing_ = false;
			}
		}
		else
		{
			meter_.decValue(dt * METER_INC);
			if (meter_.value() < 0.0)
			{
				meter_.setValue(0.0);
				meterIncreasing_ = true;
			}
		}
	}

	// Prevent crazy performance on slow computers
	if (dt > MAX_DT)
		dt = MAX_DT;

	// Check whether the ball is stopped
	if (!ball_.checkStopped())
	{
		// Check the ball's rolling status
		ball_.checkRolling();

		// Add half the effect of gravity and rolling
		ball_.impulse(dt / 2, gravity_);
		ball_.doFriction(dt / 2);

		// Loop until dt has elapsed
		while (dt > 0.0) {
			// Calculate the ball's expected path
			Path path = ball_.calcPath(dt);

			// Test the ball's path on all obstructions, which return beta values
			//   indicating where/whether there are any collisions
			bool hole = false;
			double minBeta = 1.0;
			vector<Obstruction*> collisions;
			for (vector<Obstruction*>::const_iterator i = obstructions_.begin();
				i != obstructions_.end(); ++i)
			{
				double beta = (*i)->pathIntersect(path, ball_.radius());
				if (beta < minBeta)
				{
					// We've spotted a new closest collision, so clear the others
					minBeta = beta;
					collisions.clear();
					hole = (*i)->isHole();
					collisions.push_back(*i);
				}
				else if (beta == minBeta)
				{
					// We've spotted a rare simultaneous collision; add it
					hole = hole || (*i)->isHole();
					collisions.push_back(*i);
				}
			}

			// Move the ball to the nearest collision (or end of path)
			ball_.moveTo(path.begPos() + minBeta * (path.endPos() - path.begPos()));

			// Check if we've gotten to the hole
			if (!hole)
			{
				// Respond to the collisions, if there are any
				ball_.doCollision(collisions);

				// Update time
				dt *= 1.0 - minBeta;
			}
			else
			{
				// Print the score
				cout << "Kudos! You finished the course in " << strokes() << endl;
				cout << "GAME OVER! ... now get a life." << endl;

				// Prep the grand finale
				Triangle::setCrazyTris();

				// End the game
				gameOver_ = true;
				dt = 0.0;
			}

			// Report Ball's position and velocity at each time step
			if (ballDebug_)
				cout << "P:" << ball_.position() << " V:" << ball_.velocity() << endl;

		}

		// Add the rest of the effect of gravity and rolling
		ball_.impulse(dt / 2, gravity_);
		ball_.doFriction(dt / 2);
	}

	// Make sure we haven't fallen off of the course
	if (ball_.checkBottomedOut())
		++strokes_;
}

void World::swing()
{
	if (ball_.stopped() && camera.tracking())
	{
		if (meterActive_)
		{
			// Stop the ball from rolling and erase it's history
			//   (so it doesn't just stop immediately)
			ball_.accelTo(Tuple());
			ball_.clearAll();

			// Find the initial direction of the ball's trajectory
			Tuple camDir = camera.direction();
			Tuple strokeDir =
				(Tuple(camDir.x(), 0.0, camDir.z()).norm() +
				Tuple(0.0, club_.slope(), 0.0)).norm();

			// Hit the ball
			ball_.accelBy(strokeDir * meter_.value() * METER_MULT);

			meterActive_ = false;
			meter_.setValue(0.0);
		
			// Player hit the ball, increment strokes
			++strokes_;
			cout << "Strokes so far: " << strokes << endl;
		}
		else
			meterActive_ = true;
	}
}

void World::ballDebugToggle()
{
	ballDebug_ = !ballDebug_;
	cout << "Ball Reporting: " << (ballDebug_ ? "On" : "Off") << endl;
}

void World::rollDebugToggle()
{
	ball_.rollDebugToggle();
	cout << "Collision Reporting: " << (ball_.rollDebug() ? "On" : "Off") << endl;
}

void World::lightOn()
{
	light_ = true;
	glEnable(GL_LIGHTING);
}

void World::lightOff()
{
	light_ = false;
	glDisable(GL_LIGHTING);
}

void World::setupLight()
{
	// Function should only be run once.
	static int sentinel = 0;
	if (sentinel++ > 0)
	{
		cerr << "***ERROR: Ran setupLight() twice!\n";
		return;
	}

	// Define light RGBA intensity parameters.
	GLfloat glfLightAmbient[]  = {0.5f,0.5f,0.5f,1.0f};
	GLfloat glfLightDiffuse[]  = {1.2f,1.2f,1.2f,1.0f};
	GLfloat glfLightSpecular[] = {0.9f,0.9f,0.9f,1.0f};

	// Define a directional light source (infinite distance away from the objects in the scene).
	GLfloat glfLightPosition[] = {0.0f,0.0f,2.0f,0.0f};

	// Set light source parameters.
	glLightfv(GL_LIGHT0,GL_AMBIENT,glfLightAmbient);
	//glLightfv(GL_LIGHT0,GL_DIFFUSE,glfLightDiffuse);
	//glLightfv(GL_LIGHT0,GL_SPECULAR,glfLightSpecular);
	glLightfv(GL_LIGHT0,GL_POSITION,glfLightPosition);

	// Enable the defined light source.
	glEnable(GL_LIGHT0);
	// Enable back face culling of polygons based upon their window coordinates.
	//glEnable(GL_CULL_FACE); // turns off front face draw
	// Enable depth comparisons. Enabled in initOpenGL().
	//glEnable(GL_DEPTH_TEST);

	// Select smooth shading.
	glShadeModel(GL_SMOOTH);

	// Turn on/off lights as required
	light_ ? lightOn() : lightOff();
}

/* addTriangle
 *
 * Adds a triangle.
 */
Triangle* World::addTriangle(Vertex* v1, Vertex* v2, Vertex* v3)
{
	assert(v1 != NULL && v2 != NULL && v3 != NULL);

	Triangle* temp = new Triangle(v1, v2, v3);
	obstructions_.push_back(temp);

	return temp;
}

/* addEdge
 *
 * Adds an edge.
 */
Edge* World::addEdge(Vertex* v1, Vertex* v2)
{
	assert(v1 != NULL && v2 != NULL);

	Edge* temp = new Edge(v1, v2);
	obstructions_.push_back(temp);

	return temp;
}

/* addVertex
 *
 * Adds a vertex.
 */
Vertex* World::addVertex(Tuple v1, Tuple color)
{
	Vertex* temp = new Vertex(v1, color);
	obstructions_.push_back(temp);

	return temp;
}
