// This file is part of ThriceBall.
//
// Author: Kai Hillenbrand (AnTeevY)
// © Copyright, 2009 by Kai Hillenbrand, http://www.anteevy.de

// --------------------------------------------------

#include "player.h"

CPlayer::CPlayer(CRenderer *renderer, OGRE3DRenderSystem *rendersys)
	: m_pRenderer(renderer), m_pAudio(renderer->getAudio()), m_pCamera(m_pSceneMgr->getCamera("MainCamera")),
	m_pRenderSystem(rendersys), m_pSceneMgr(renderer->getSceneManager()), m_pInterface(renderer->getInterface()),
	m_bBallShadows(true), m_bCharging(false), m_bCanChangeType(true), m_bBraking(false),m_iCurBallNum(1), 
	m_fEnergy(100.f), m_fMaxShotPower(5.f), m_fMinShotPower(0.5f), m_fIncreasePercentage(2.f), 
	m_fCurPower(0.5f), m_fPowerMult(100.f), m_fSpeedEdge(5.f), m_fBrakePower(0.9f), m_fShotEnergyCost(5.f), 
	m_fPossibleEnergy(100.f), m_fRedPowerMult(4.5f), m_fGreenPowerMult(4.5f), m_fBluePowerMult(27.f), 
	m_vCurBallPos(0,0,0),  m_vRespawnPos(0,0,0), m_fDeathEnergyCost(10.f), m_fBrakeEnergyCost(0.05f), 
	m_fTimeEnergyCost(0.01f), m_iGameState(0)
	
{	
	createBalls();	// create the balls
	createArrow();	// create the arrow

	std::cout << "ThriceBall Log: Created Player" << std::endl;	// for the log
}

void CPlayer::update(float winWidth, float winHeight)
{
	// called each frame
	updateArrow(winWidth, winHeight);	// calculate arrow direction based on mouse input
	updateEnergy();						// update energy
	updateCurPower();					// update current charge power
	updateBrake();						// update brake
}

void CPlayer::updateArrow(float winWidth, float winHeight)
{
	if (isInShotMode())
	{		
		calcMouseDir(winWidth, winHeight);	// calculate the XZ-direction the mouse points at
		m_pArrowNode->setVisible(true);		// arrow is visible because of shot mode

		// set arrow node's position, direction and scale based on mouse direction and current power
		m_pArrowNode->setPosition(Vector3(	getCurBall()->getGlobalPosition().x + (getCurPower() * 0.13f + 1.f) * m_vMouseDir.x, 
											getCurBall()->getGlobalPosition().y,
											getCurBall()->getGlobalPosition().z + (getCurPower() * 0.13f + 1.f) * m_vMouseDir.z));
		m_pArrowNode->setDirection(Vector3(m_vMouseDir.x, 0, m_vMouseDir.z), Ogre::Node::TS_WORLD);
		m_pArrowNode->setScale(Vector3(0.3,1,getCurPower() * 0.15f + 0.3f));
	}
	else
		m_pArrowNode->setVisible(false);	// hide arrow

	// if won or lost game, hide the arrow, too
	if (m_iGameState != 0)
		m_pArrowNode->setVisible(false);
}

void CPlayer::updateEnergy()
{
	// if still playing
	if (m_iGameState == 0)
	{
		// reduce energy by time
		reduceEnergy(m_fTimeEnergyCost);

		// check for game over
		if (m_fEnergy <= 0)
			gameOver();

		// check for heartbeat
		if (m_fEnergy <= 20)
			m_pAudio->playSound(4);

		// give the energy to the interface and let it update its HUD
		m_pInterface->updateEnergy(m_fEnergy, m_fPossibleEnergy);
	}
}

void CPlayer::gameWin()
{
	if (m_iGameState == 0)	// can only win if still playing -> can't win twice
	{
		m_iGameState = 2;	// won game

		// write the text information when game won
		m_pInterface->hideInfo();
		// the true parameter means that the info will be drawn at all costs!
		m_pInterface->drawInfo("Gratulations! You've made it to the last checkpoint. Now press the quit key/button to see the highscore.", true);
	}
}

void CPlayer::gameOver()
{
	if (m_iGameState == 0)	// can only lose if still playing -> can't lose twice
	{
		m_iGameState = 1;	// lost game

		// write the text information when game over
		m_pInterface->hideInfo();
		// the true parameter means that the info will be drawn at all costs!
		m_pInterface->drawInfo("Sorry, no energy left. Press the exit key/button and restart the level to try again.", true);
	}
}

void CPlayer::restart()
{
	std::cout << "ThriceBall Log: Restart Player" << std::endl;	// for the log

	// reset changing variables
	m_bCharging = m_bBraking = false;
	m_bCanChangeType = true;
	m_iCurBallNum = 1;
	m_fEnergy = m_fPossibleEnergy = 100.f;
	m_fCurPower = 0.5f;
	m_iGameState = 0;
}

// ball functions

void CPlayer::updateCurPower()
{
	// while charging and in shot mode and current power is below maximum power and still playing
	if ((m_bCharging) && (m_fCurPower < m_fMaxShotPower) && (isInShotMode()) && (m_iGameState == 0))
	{
		// increase the power by a specified percentage of the maximum power
		m_fCurPower += (m_fIncreasePercentage / 100 * m_fMaxShotPower);
		// decrease the possible energy
		m_fPossibleEnergy = m_fEnergy - (m_fCurPower / m_fMaxShotPower * m_fShotEnergyCost);
	}
}

void CPlayer::updateBrake()
{
	// if not charging for a shot, then the right-click would cancel it and if braking
	// and if ball is really moving (0.1 is veeery slow speed, so actual speed has to be higher)
	if((!m_bCharging) && (getCurBall()->getLinearVelocity().magnitude() > 0.1) && (m_bBraking))
	{
		// reduce the linear velocity smoothly to stop the ball rolling
		getCurBall()->setLinearVelocity(NxOgre::Real3(getCurBall()->getLinearVelocity().x * m_fBrakePower,
			getCurBall()->getLinearVelocity().y, getCurBall()->getLinearVelocity().z * m_fBrakePower));
	
		// reduce energy while braking
		reduceEnergy(m_fBrakeEnergyCost);
	}
	else
		m_bBraking = false;
}

void CPlayer::switchBall()
{
	// if still playing
	if (m_iGameState == 0)
	{
		// next ball is current ball; adjust height (y-pos) based on ball size
		switch (m_iCurBallNum)
		{
		case 1:
			m_vCurBallPos = getCurBall()->getGlobalPosition() + NxOgre::Vec3(0, -0.2, 0);
			setCurBall(2);
			break;
		case 2:
			m_vCurBallPos = getCurBall()->getGlobalPosition() + NxOgre::Vec3(0, 0.4, 0);
			setCurBall(3);
			break;
		case 3:
			m_vCurBallPos = getCurBall()->getGlobalPosition() + NxOgre::Vec3(0, -0.2, 0);
			setCurBall(1);
			break;
		default:break;
		}
	}
}

void CPlayer::applyMove()
{
	// while in shot mode and still playing
	if(isInShotMode() && (m_iGameState == 0))
	{
		Real xPower = 0, zPower = 0;
		xPower = m_vMouseDir.x * m_fPowerMult * getCurPower();
		zPower = m_vMouseDir.z * m_fPowerMult * getCurPower();

		// prevent a power error by giving the power an upper limit
		if (xPower > (m_fPowerMult * m_fMaxShotPower))
			xPower = m_fPowerMult * m_fMaxShotPower;
		if (zPower > (m_fPowerMult * m_fMaxShotPower))
			zPower = m_fPowerMult * m_fMaxShotPower;

		// multiply the powers by the current power multiplicator, depending on selected ball
		// because balls have different weight, they need more or less power then others
		xPower *= getCurPowerMult();
		zPower *= getCurPowerMult();

		// add force to the ball in the calculated direction with current shot power modification
		getCurBall()->addForce(NxOgre::Real3(xPower, 0, zPower), NxOgre::Enums::ForceMode_Force, true);

		// reduce the energy after the shot
		reduceEnergy(m_fCurPower / m_fMaxShotPower * m_fShotEnergyCost);
		m_fPossibleEnergy = m_fEnergy;

		m_bCharging = false;	// no longer charging
		m_pAudio->playSound(0);	// play shot sound
	}

	resetCurPower();	// shot power is again m_fMinShotPower
}

void CPlayer::cancelShot()
{
	m_fCurPower = m_fMinShotPower;	// reduce shot power again to the minimum
	m_fPossibleEnergy = m_fEnergy;	// no energy lost -> reset the energy-after-shot to the actual energy
	m_bCharging = false;			// no longer charging
}

void CPlayer::respawn()
{
	// withdraw energy and reset ball to last checkpoint
	reduceEnergy(m_fDeathEnergyCost);
	setBallPos(m_vRespawnPos);
	m_pAudio->playSound(1);	// play death sound
}

// get functions

String CPlayer::getQuitMsg()
{
	String msg = "";	// return message

	// convert float to string
	String sScore = "";
	float fScore = m_fEnergy * 10;
	sScore = StringConverter::toString(fScore, 3); 

	// if in debug mode, write debug message
	if (m_pRenderer->getDebugMode())
	{
		msg = "DEBUG_MODE";
	}
	else
	{
		switch (m_iGameState)
		{
		case 0:	// still playing
			msg = "EXIT_QUIT";
			break;
		case 1:	// lost game
			msg = "EXIT_LOSS";
			break;
		case 2:	// won game		
			msg =  "_SCORE_" + sScore + "_SCORE_";
			break;
		default:
			msg = "EXIT_QUIT";
			break;
		}
	}

	return msg;
}

float CPlayer::getCurPowerMult()
{
	switch (m_iCurBallNum)
	{
	case 1: return m_fRedPowerMult;		break;
	case 2: return m_fGreenPowerMult;	break;
	case 3: return m_fBluePowerMult;	break;
	default: return 1;					break;
	}
}

OGRE3DBody* CPlayer::getCurBall()
{
	switch (m_iCurBallNum)
	{
	case 1: return m_pBallRed; break;
	case 2: return m_pBallGreen; break;
	case 3: return m_pBallBlue; break;
	default:return m_pBallRed; break;
	}
}

Vector3 CPlayer::getCurBallPos()
{
	Vector3 returnVec;
	switch (m_iCurBallNum)
	{
	case 1: returnVec.x = m_pBallRed->getGlobalPosition().x;
			returnVec.y = m_pBallRed->getGlobalPosition().y;
			returnVec.z = m_pBallRed->getGlobalPosition().z;
			break;
	case 2: returnVec.x = m_pBallGreen->getGlobalPosition().x;
			returnVec.y = m_pBallGreen->getGlobalPosition().y;
			returnVec.z = m_pBallGreen->getGlobalPosition().z;
			break;
	case 3: returnVec.x = m_pBallBlue->getGlobalPosition().x;
			returnVec.y = m_pBallBlue->getGlobalPosition().y;
			returnVec.z = m_pBallBlue->getGlobalPosition().z;
			break;
	default:returnVec.x = m_pBallRed->getGlobalPosition().x;
			returnVec.y = m_pBallRed->getGlobalPosition().y;
			returnVec.z = m_pBallRed->getGlobalPosition().z;
			break;
	}
	return returnVec;
}

// set functions

void CPlayer::reduceEnergy(float amount)
{
	m_fEnergy -= amount;
	m_fPossibleEnergy -= amount;
}

void CPlayer::setEnergies(float shot, float brake, float death, float time)
{
	m_fShotEnergyCost = shot;
	m_fBrakeEnergyCost = brake;
	m_fDeathEnergyCost = death;
	m_fTimeEnergyCost = time;
}

void CPlayer::setCurBall(int num)
{
	// if valid ball number and is able to change ball (is on top of a checkpoint)
	if ((num > 0) && (num < 4) && (m_bCanChangeType)) 
	{
		OGRE3DBody *oldBall = getCurBall();
		m_iCurBallNum = num;

		// set the new ball's position to the actual ball position
		OGRE3DBody *curBall = getCurBall();
		setBallPos(m_vCurBallPos);

		// apply the old velocity
		curBall->setLinearVelocity(oldBall->getLinearVelocity());

		// set the arrow's color
		if (num == 1)
			m_pSceneMgr->getEntity("Arrow")->setMaterialName("red");
		else if (num == 2)
			m_pSceneMgr->getEntity("Arrow")->setMaterialName("green");
		else if (num == 3)
			m_pSceneMgr->getEntity("Arrow")->setMaterialName("blue");

		// set the positions of the other balls out of range
		if (curBall != m_pBallRed)
			m_pBallRed->setGlobalPosition(NxOgre::Vec3(1000,0,1000));
		if (curBall != m_pBallGreen)
			m_pBallGreen->setGlobalPosition(NxOgre::Vec3(1000,0,1000));
		if (curBall != m_pBallBlue)
			m_pBallBlue->setGlobalPosition(NxOgre::Vec3(1000,0,1000));
	}
}

void CPlayer::setBallPos(NxOgre::Vec3 pos)
{
	m_vCurBallPos = pos;
	getCurBall()->setGlobalPosition(m_vCurBallPos);
	getCurBall()->setLinearVelocity(NxOgre::Vec3(0,0,0));
}

void CPlayer::setRespawnPos(NxOgre::Vec3 pos, int color)
{
	// if respawn position should be changed
	if (pos != m_vRespawnPos)
	{
		m_pAudio->playSound(2);	// play checkpoint sound

		m_vRespawnPos = pos;
		ParticleSystem* pSys = m_pSceneMgr->getParticleSystem("CPParticles");

		// set the checkpoint particle effect to the current respawn position
		SceneNode *cbpartNode = m_pSceneMgr->getSceneNode("CPParticlesNode");
		cbpartNode->setPosition(Vector3(m_vRespawnPos.x, m_vRespawnPos.y + 0.5, m_vRespawnPos.z));

		if (color == 0)	// start checkpoint is red
		{		
			pSys->getEmitter(0)->setColourRangeStart(ColourValue(0.8,0.1,0.1));
			pSys->getEmitter(0)->setColourRangeEnd(ColourValue(0.7,0.4,0.4));
		}
		else if (color == 1) // finish checkpoint is green
		{
			pSys->getEmitter(0)->setColourRangeStart(ColourValue(0.1,0.8,0.1));
			pSys->getEmitter(0)->setColourRangeEnd(ColourValue(0.4,0.7,0.4));
		}
		else if (color == 2) // normal checkpoint is orange
		{
			pSys->getEmitter(0)->setColourRangeStart(ColourValue(1,0.9,0.2));
			pSys->getEmitter(0)->setColourRangeEnd(ColourValue(1,0.5,0.2));
		}
	}
}

void CPlayer::setCharging(bool charge)
{
	// while in shot mode and current power is below maximum power and still playing
	if ((m_fCurPower < m_fMaxShotPower) && (isInShotMode()) && (m_iGameState == 0))
	{
		m_bCharging = true;
	}
	else
		m_bCharging = false;
}

void CPlayer::setBraking(bool brake)
{
	// if not charging for a shot, and if ball is really moving and not currently braking
	if((!m_bCharging) && (getCurBall()->getLinearVelocity().magnitude() > 0.1) && (!m_bBraking))
	{
		m_bBraking = true;
	}
	else
		m_bBraking = false;
}

// private functions

void CPlayer::createBalls()
{	
	std::cout << "ThriceBall Log: Create the Balls" << std::endl;	// for the log

	// create the 3 balls somewhere in the distance
	// only current ball gets translated to the starting point afterwards
	m_pBallRed = m_pRenderSystem->createBody(new NxOgre::Sphere(0.5), NxOgre::Real3(1000, 1000, 1000), "ball.mesh");
	m_pBallGreen = m_pRenderSystem->createBody(new NxOgre::Sphere(0.3), NxOgre::Real3(1000, 1000, 1000), "ball.mesh");
	m_pBallBlue = m_pRenderSystem->createBody(new NxOgre::Sphere(0.7), NxOgre::Real3(1000, 1000, 1000), "ball.mesh");

	// set entity size
	m_pBallGreen->getEntity()->getParentSceneNode()->scale(0.6,0.6,0.6);
	m_pBallBlue->getEntity()->getParentSceneNode()->scale(1.4,1.4,1.4);

	// set material names
	m_pBallRed->getEntity()->setMaterialName("ball_red");
	m_pBallGreen->getEntity()->setMaterialName("ball_green");
	m_pBallBlue->getEntity()->setMaterialName("ball_blue");

	// specify whether the balls shall cast shadows or not
	m_pBallRed->getEntity()->setCastShadows(m_pRenderer->shadowsEnabled());
	m_pBallGreen->getEntity()->setCastShadows(m_pRenderer->shadowsEnabled());
	m_pBallBlue->getEntity()->setCastShadows(m_pRenderer->shadowsEnabled());

	// set the ball's properties
	m_pBallRed->setAngularDamping(5);
	m_pBallGreen->setAngularDamping(15);
	m_pBallBlue->setAngularDamping(20);

	// set mass
	m_pBallRed->setMass(2);
	m_pBallGreen->setMass(3);
	m_pBallBlue->setMass(20);
}

void CPlayer::createArrow()
{
	std::cout << "ThriceBall Log: Create the Arrow" << std::endl;	// for the log

	Entity *arrow = m_pSceneMgr->createEntity( "Arrow", "arrow.mesh" );
	m_pArrowNode = m_pSceneMgr->getRootSceneNode()->createChildSceneNode( "ArrowNode");
	m_pArrowNode->attachObject(arrow);
}

void CPlayer::calcMouseDir(float winWidth, float winHeight)
{
	Real distance = 0;

	RaySceneQuery *raySceneQuery;
	raySceneQuery = m_pSceneMgr->createRayQuery(Ray());

	// setup ray scene query
	CEGUI::Point mousePos = CEGUI::MouseCursor::getSingleton().getPosition();
	Ray mouseRay = m_pCamera->getCameraToViewportRay(mousePos.d_x / winWidth, mousePos.d_y / winHeight);	
	raySceneQuery->setRay(mouseRay);
	raySceneQuery->setSortByDistance(true);

	// execute query	
	RaySceneQueryResult &result = raySceneQuery->execute();
	RaySceneQueryResult::iterator itr = result.begin( );

	// translate the arrow plane to the y-coord of the ball and check where the ray hits it
	SceneNode *planeNode = m_pSceneMgr->getEntity("ArrowPlaneEnt")->getParentSceneNode();
	planeNode->setPosition(planeNode->getPosition().x, getCurBallPos().y, planeNode->getPosition().z);

	while (itr != result.end())
	{
		distance = itr->distance;

		if (itr->movable->getName() == "ArrowPlaneEnt")
			itr = result.end();
		else
			itr++;
	}

	// get the point in 3D space the mouse points at
	Vector3 vClickPos = mouseRay.getPoint(distance);

	// vMouseDir is the normalised XZ vector from the ball to the mouse position
	// -> the direction the player wants the ball to be shot
	m_vMouseDir = vClickPos - Vector3(getCurBall()->getGlobalPosition().x,
		getCurBall()->getGlobalPosition().y,getCurBall()->getGlobalPosition().z);
	m_vMouseDir.y = 0;
	m_vMouseDir.normalise();
}