#include "ClientSynchronizer.h"
#include "streams\ErrorStream.h"

extern ExceptionStream< ostream&, ContinueFunctor  >		KODIAK_WARNING;
extern KODIAK_DEBUG_STREAM KODIAK_DEBUG_COUT;

#include "..\gameSettings.h"


//#################################################
//some auxiliary functions
void setMicroShipPosition( SpaceShip &ship, const Point_3<double> &newPos  )
{
	Point_3<double> scaledShipPos =	GameConstants::MICRO_COORD_CONVERTOR.convertServerToClientPoint( newPos );
	ship.setPos( scaledShipPos );
}
void setMicroShipSpeed( SpaceShip &ship, const double speed  )
{
	double convertedSpeed =	 GameConstants::MICRO_COORD_CONVERTOR.convertServerToClientValue( speed) /1000.0; // speed is in m/s -> convert to km/s
	ship.speed = convertedSpeed;
}
void setMicroShipAcceleration( SpaceShip &ship, const double accleration  )
{
	double convertedAccleration =	 GameConstants::MICRO_COORD_CONVERTOR.convertServerToClientValue( accleration) /1000.0; // acceleration is in m/s^2 -> convert to km/s^2
	ship.accleration = convertedAccleration;
}
void setMicroShipDestinationPosition( SpaceShip &ship, const Point_3<double> &dstPos  )
{
	Point_3<double> scaledShipDstPos =	GameConstants::MICRO_COORD_CONVERTOR.convertServerToClientPoint( dstPos );
	ship.destinationPoint = scaledShipDstPos;
}

Model * getShipModel( OpenGlSharedData &oglData, ShipType shipType )
{
	std::map<string, Model* >::iterator modelIt = oglData.modelManager.find( "ship"+intToString(shipType) );
	//all ship models are stored under string "ship"+[number]
	const string SHIP_MODEL("ship");
	if ( modelIt == oglData.modelManager.end() )
	{
		return NULL;
	}

	return modelIt->second;

}

//#################################################



//-----------Synchronisation interface implementation---------------------
void ClientSynchronizer::warpSynchronize( const ShipWarpInfo &shipWarpInfo  )
{
	//display state change effect
	if ( shipWarpInfo.stateChange )
	{
		Point_3<double> warpEffectPosMicroMap;
		Point_3<double> warpEffectPosMacroMap;

		Vector_3<double> diffVec( shipWarpInfo.dstPosition - shipWarpInfo.srcPosition );
		double totalLen = ( shipWarpInfo.dstPosition - shipWarpInfo.srcPosition ).get_length();

		double lenA = ( shipWarpInfo.srcPosition - shipWarpInfo.position ).get_length();
		double lenB = ( shipWarpInfo.dstPosition - shipWarpInfo.position ).get_length();

		//if ship position is between starting and end point (simplified test )
		if ( abs(lenA + lenB - totalLen) < EPSILON )
		{
			warpEffectPosMicroMap = GameConstants::MICRO_COORD_CONVERTOR.convertServerToClientPoint( shipWarpInfo.position );
			warpEffectPosMacroMap = GameConstants::MACRO_COORD_CONVERTOR.convertServerToClientPoint( shipWarpInfo.position );
			this->mapViewer.activateWarpEffect( warpEffectPosMicroMap, warpEffectPosMacroMap );
		}
		/*
		warpEffectPosMicroMap = GameConstants::MICRO_COORD_CONVERTOR.convertServerToClientPoint( shipWarpInfo.dstPosition );
		warpEffectPosMacroMap = GameConstants::MACRO_COORD_CONVERTOR.convertServerToClientPoint( shipWarpInfo.dstPosition );
		this->mapViewer.activateWarpEffect( warpEffectPosMicroMap, warpEffectPosMacroMap );
		*/

		warpEffectPosMicroMap = GameConstants::MICRO_COORD_CONVERTOR.convertServerToClientPoint( shipWarpInfo.srcPosition );
		warpEffectPosMacroMap = GameConstants::MACRO_COORD_CONVERTOR.convertServerToClientPoint( shipWarpInfo.srcPosition );
		this->mapViewer.activateWarpEffect( warpEffectPosMicroMap, warpEffectPosMacroMap );
	}

	//if ship is in warp, then return, no other modification required
	if ( !shipWarpInfo.inWarp )
	{
		return;
	}

	//set ship data if its in warp ( must be here because no other information are recieved from server when travels in warp )
	ShipDataDetails shipData;
	shipData.acceleration = 0;
	shipData.dstPosition = shipWarpInfo.dstPosition;
	shipData.enemyTarget.targetType = NO_TARGET;
	shipData.enemyTarget.enemyTargetId = false;
	shipData.id = shipWarpInfo.id;
	shipData.inBase = false;
	shipData.inWarp = true;
	shipData.isMining = false;
	shipData.isOrbiting = false;
	shipData.lifePercentage = 0;
	shipData.shieldPercentage = 0;
	shipData.name = shipWarpInfo.name;
	shipData.orbitPivot.set_zero();
	shipData.orbitSpeed = 0;
	shipData.position = shipWarpInfo.position;
	shipData.speed = shipWarpInfo.speed;
	shipData.type = shipWarpInfo.type;

	this->shipSynchronize( shipData );

}
void ClientSynchronizer::shipSynchronize( const ShipDataDetails &shipData )
{
	this->shipSynchronizeMacro( shipData );
	this->shipSynchronizeMicro( shipData );
}

void ClientSynchronizer::shipSynchronizeMacro(const ShipDataBasic &shipData, bool useAlphaOffset )
{
	if (this->mapViewer.starMap == NULL) {
		KODIAK_WARNING<< "ERROR: Null pointer clientSynchronizer->mapViewer.starMap in ClientSynchronizer::shipSynchronizeMacro. Skipped";
		return;
	}

	//search in database for given ship
	ShipRadarObject &shipRef = this->mapViewer.starMap->RadarData.ships[ shipData.id ]; //create or query

	shipRef.setDstPosition ( GameConstants::MACRO_COORD_CONVERTOR.convertServerToClientPoint( shipData.dstPosition ) );
	shipRef.setPosition ( GameConstants::MACRO_COORD_CONVERTOR.convertServerToClientPoint( shipData.position ) );			
	shipRef.setSpeed ( GameConstants::MACRO_COORD_CONVERTOR.convertServerToClientValue( shipData.speed) / 1000.0 ) ; // speed is in m/s -> convert to km/s
	shipRef.setAcceleration ( GameConstants::MACRO_COORD_CONVERTOR.convertServerToClientValue( shipData.acceleration) / 1000.0 ); // acceleration is in m/s^2 -> convert to km/s^2
	shipRef.setAlpha( 1.0 );
	shipRef.inBase = shipData.inBase;
	shipRef.inWarp = shipData.inWarp;
	

	if ( useAlphaOffset )
	{
		shipRef.setAlphaOffset( 0.001 );
	}
	else
	{
		shipRef.setAlphaOffset( 0.0 );
	}
}






void ClientSynchronizer::starSynchronize(  const StarData &starData )
{
	if (this->mapViewer.starMap == NULL) {
		KODIAK_WARNING<< "ERROR: Null pointer clientSynchronizer->mapViewer.starMap in ClientSynchronizer::starSynchronize. Skipped";
		return;
	}

	//search in database for given star
	StarRadarObject &starRef =this->mapViewer.starMap->RadarData.stars[ starData.id ]; //create or query
	starRef.setPosition ( GameConstants::MACRO_COORD_CONVERTOR.convertServerToClientPoint( GEOMETRY::CONVERT_TO_DOUBLE( starData.position )) );							
}

void ClientSynchronizer::shipSynchronizeCorrector( const ShipDataDetails &serverShipData, SpaceShip& clientShipData )
{
	//convert by scaling factor from long long to float
	Point_3<double> scaledServerShipPos =	GameConstants::MICRO_COORD_CONVERTOR.convertServerToClientPoint( serverShipData.position );
	Vector_3<double> diffVec = scaledServerShipPos - clientShipData.getPos();
	float diff = diffVec.get_length();

	if ( diff > GameConstants::MAX_TOLERABLE_POSTION_DISTANCE_DIFFERENCE_CORRECTOR || clientShipData.lastSynchronizationTime < EPSILON )
	{
		clientShipData.setPos( scaledServerShipPos );
		clientShipData.setExternForce( Vector_3<double>(0,0,0) );
	}
	else
	{
		// extern force is used as correction vector calculated according
		// to the simulatiom time between last 2 synchronisations and the 
		// difference error between client and server position

		Vector_3<double> correctionForce = (diffVec / clientShipData.lastSynchronizationTime) * GameConstants::ATTENUATION_FACTOR_CORRECTOR;

		if ( correctionForce.get_length() > EPSILON )
		{
			//clamp to interval <0, 0.1>
			correctionForce.clampLengthTo(0, 0.1 );
			clientShipData.setExternForce( (clientShipData.getExternForce() + correctionForce) *0.5 );
		}
		else
		{
			clientShipData.setExternForce( Vector_3<double>(0,0,0) );
		}
	}
}

void ClientSynchronizer::shipSynchronizeMicro(const ShipDataDetails &shipData )
{
	if ( this->mapViewer.getGalaxyPart() == NULL ) 
	{
		return;
	}

	GalaxyPart *galaxyPart = this->mapViewer.getGalaxyPart();
	Model*  shipModel = getShipModel( this->mapViewer.sharedData, shipData.type );

	
	//all ship models are stored under string "ship"+[number]
	const string SHIP_MODEL("ship");
	if ( shipModel == NULL )
	{
		throw exception( (string("Model with name: ")+ SHIP_MODEL +intToString(shipData.type) + string(" was not found")).c_str() );
	}

	bool newShip = false;
	SpaceShip *shipPtr = NULL;
	shipPtr = galaxyPart->findShip( shipData.id ); 	//search in database for given ship

	if ( shipPtr == NULL )
	{
		//ship not found -> create new ship
		newShip = true;
		shipPtr = galaxyPart->insertShip( shipData.id, SpaceShip(*shipModel) );
	}

							
	if ( ! newShip )
	{
		setMicroShipPosition( *shipPtr, shipData.position );
		shipPtr->setExternForce( Vector_3<double>(0,0,0) );

		//do not use correcto for now
		//this->shipSynchronizeCorrector( shipData, *shipIt->second );
	}
	else
	{
		//convert by scaling factor from long long to float
		setMicroShipPosition( *shipPtr, shipData.position );
		shipPtr->setExternForce( Vector_3<double>(0,0,0) );
	}


	setMicroShipDestinationPosition( *shipPtr, shipData.dstPosition );

	shipPtr->name = shipData.name;
	setMicroShipSpeed( *shipPtr, shipData.speed  );
	setMicroShipAcceleration( *shipPtr, shipData.acceleration  );
	
	shipPtr->type = shipData.type;
	shipPtr->lifePercentage = shipData.lifePercentage;
	shipPtr->shieldPercentage = shipData.shieldPercentage;
	shipPtr->isOrbiting = shipData.isOrbiting;
	shipPtr->isMiningFromPlanet = shipData.isMining;
	shipPtr->orbitPivot = GameConstants::MICRO_COORD_CONVERTOR.convertServerToClientPoint( shipData.orbitPivot );
	shipPtr->orbitSpeed = shipData.orbitSpeed / 1000000000.0; //convert angular speed from nano degrees to degrees
	shipPtr->rateOfFire = 10.0; //TODO, FIXME : add this atributte to server message 
	shipPtr->enemyTarget = shipData.enemyTarget;
	shipPtr->inWarp = shipData.inWarp;
	shipPtr->inBase = shipData.inBase;
	


	//--------------------------------------------
	//sets where is the front side of the ship if new ship
	if ( newShip )
	{
		shipPtr->setFront( MAKE_VECTOR<double>(0,0,1) );
	}
	//--------------------------------------------

	shipPtr->setOrientation( shipPtr->getDir() );
	shipPtr->lastSynchronizationTime = 0; //synchronization done, clear counter

}

void ClientSynchronizer::planetSynchronize( const GenericPlanetData &planetData)
{
	if ( this->mapViewer.getGalaxyPart() == NULL ) 
	{
		return;
	}

	BasicPlanet *basicPlanet = NULL;

	GalaxyPart *galaxyPart = this->mapViewer.getGalaxyPart();

	//search in database for given ship
	BasePlanetClass *basePlanet = galaxyPart->findPlanet( planetData.id ); //search in database for given ship

	if ( basePlanet == NULL )
	{
		//planet not found -> create new planet
		basicPlanet = galaxyPart->insertPlanet( planetData.id, BasicPlanet() );
	}
	else
	{
		if (  basePlanet->getPlanetClassType() != BASIC_PLANET_CLASS )
		{
			KODIAK_WARNING<<"basePlanet->getPlanetClassType() != BASIC_PLANET_CLASS ...ignored";
			return;
		}
		else
		{
			basicPlanet =  (BasicPlanet*) basePlanet;
		}
	}


	basicPlanet->axis = planetData.axis;
	basicPlanet->name = planetData.name;
	basicPlanet->center = GameConstants::MICRO_COORD_CONVERTOR.convertServerToClientPoint( planetData.pivot );
	basicPlanet->color.col = planetData.color;
	basicPlanet->color.w = 1.0;
	basicPlanet->objectSize = GameConstants::MICRO_COORD_CONVERTOR.convertServerToClientValue( planetData.objectSize ); //getClamped<float>(0, MAX_PLANET_SIZE, MICRO_COORD_CONVERTOR.convertServerToClientValue( planetData.objectSize ) * PLANET_EXTRA_SCALE_FACTOR );
	basicPlanet->radius = GameConstants::MICRO_COORD_CONVERTOR.convertServerToClientValue( planetData.radius );
	basicPlanet->angle = convertNanoAngleToAngle( planetData.angle );
	basicPlanet->speed = convertNanoAngleToAngle( planetData.speed );
	basicPlanet->coronaTextureId = TextureManager::getInstance().getTexture( planetData.coronaTextureName );
	
	basicPlanet->rateOfFire = 50.0; //TODO, FIXME : add this atributte to server message 
	basicPlanet->enemyTargetIdContainer = planetData.enemyTargetData;

	unsigned int surfaceTexId = TextureManager::getInstance().getTexture( planetData.textureName );
	basicPlanet->textrureId = surfaceTexId;
	if ( surfaceTexId == 0 ) throw exception( (string("Texture with name: ")+ planetData.textureName + string(" was not found")).c_str() );
		
	//font2 texture must be defined in CAL file!!!
	unsigned int fontTexId = TextureManager::getInstance().getTexture( "font2" );
	basicPlanet->fontTexId = fontTexId;
	if ( fontTexId == 0 ) throw exception( (string("Texture with name: font2 was not found")).c_str() );

	/* CONSOLE DEBUG OUTPUT
	KODIAK_DEBUG_COUT<<KODIAK_ENDL<<KODIAK_ENDL;
	KODIAK_DEBUG_COUT<<"########################################"<<KODIAK_ENDL;
	KODIAK_DEBUG_COUT<<"Jmeno planety: "<<basicPlanet->name<<KODIAK_ENDL;
	KODIAK_DEBUG_COUT<<"Id planety: "<<planetData.id <<KODIAK_ENDL;
	KODIAK_DEBUG_COUT<<"Uhel planety: "<<basicPlanet->angle<<KODIAK_ENDL;
	KODIAK_DEBUG_COUT<<"Vzdalenost planety od slunce: "<< basicPlanet->radius <<KODIAK_ENDL;
	KODIAK_DEBUG_COUT<<"Pivot planety (po centrovani): " <<basicPlanet->center.x() << ", " << basicPlanet->center.z() << KODIAK_ENDL;
	KODIAK_DEBUG_COUT<<"Rychlost planety: "<<basicPlanet->speed<<KODIAK_ENDL;
	KODIAK_DEBUG_COUT<<"########################################"<<KODIAK_ENDL;
	*/
}

void ClientSynchronizer::spaceshipSynchronize( const ShipType shipType )
{
	switch ( shipType % SHIP_TYPE_COUNT )
	{
	case type0:
		this->modelViewer.setModel( SHIP_TYPE_0  );
		break;
	
	case type1:
		this->modelViewer.setModel(SHIP_TYPE_1  );
		break;

	case type2:
		this->modelViewer.setModel( SHIP_TYPE_2 );
		break;

	case type3:
		this->modelViewer.setModel( SHIP_TYPE_3  );
		break;

	case type4:
		this->modelViewer.setModel( SHIP_TYPE_4 );
		break;

	case type5:
		this->modelViewer.setModel( SHIP_TYPE_5  );
		break;

	default:
		this->modelViewer.setModel( SHIP_TYPE_0  );
		KODIAK_WARNING<<"MainWindow::showSmallRenderPanel : unkown ship type, used shipType0";
	}

}
void ClientSynchronizer::baselineSynchronize( const BaselineData &baselineData) 
{
	// Sky initialization----------------------------------------------------------------
	this->planetViewer.spacePlanet->cloudySky.skyTexture = TextureManager::getInstance().getTexture( baselineData.cloudName );
	this->planetViewer.spacePlanet->cloudySky.skyRegion[0] = MAKE_POINT<float>(-500,40,-500);
	this->planetViewer.spacePlanet->cloudySky.skyRegion[1] = MAKE_POINT<float>( 500,40,-500);
	this->planetViewer.spacePlanet->cloudySky.skyRegion[2] = MAKE_POINT<float>( 500,40, 500);
	this->planetViewer.spacePlanet->cloudySky.skyRegion[3] = MAKE_POINT<float>(-500,40, 500);
	this->planetViewer.spacePlanet->cloudySky.offsetDir[0] = 0.0001;
	this->planetViewer.spacePlanet->cloudySky.offsetDir[1] = 0.0001;

	
	//search up in skybox container for skybox with given name
	for ( int i=0; i< this->planetViewer.sharedData.skyboxContainer.size(); ++i)
	{
		if ( this->planetViewer.sharedData.skyboxContainer[i].name == baselineData.skyboxName )
		{
			SKYBOX::getSkyBoxTextures( this->planetViewer.sharedData.skyboxContainer[i], TextureManager::getInstance(), this->planetViewer.spacePlanet->skyBox.skyBoxTex.textures );
			break;
		}
	}


	//------------------------------------------------------------------------------------
	vector< Model*> models;

	// baseline model is stored under string BASELINE_MODEL
	// BASELINE_MODEL='base'+[number] ( base0, base1, base2 )
	const string BASELINE_MODEL("base");

	std::map<string, Model* >::iterator modelIt = this->planetViewer.sharedData.modelManager.find( BASELINE_MODEL +intToString( baselineData.baselineType ) );
	if ( modelIt == this->planetViewer.sharedData.modelManager.end() )
	{
		throw exception( (string("Model with name: ")+ BASELINE_MODEL +intToString( baselineData.baselineType ) + string(" was not found")).c_str() );
	}
	models.push_back( modelIt->second );
	

	// baseDesk model ( the white box between planet surface and baseline)
	// is stored under string BASED_DESK ='baseDesk'
	const string BASED_DESK("baseDesk");


	modelIt = this->planetViewer.sharedData.modelManager.find( BASED_DESK );
	if ( modelIt == this->planetViewer.sharedData.modelManager.end() )
	{
		throw exception( (string("Model with name: ")+ BASED_DESK + string(" was not found")).c_str() );
	}

	modelIt->second->identity();
	modelIt->second->translate( 0, -10, -3);
	modelIt->second->scale(13,10,13);

	

	models.push_back( modelIt->second );


	// every terrain geometry is stored under string 'terrainModel'
	const string TERRAIN_MODEL("terrainModel");

	// terrain materials are loaded from standalone MTL file with name "terrainMaterials"
	const string TERRAIN_MATERIAL( "terrainMaterials");

	// search for old "terrainModel" and delete him if exists in order to avoid memory leaks
	modelIt = this->planetViewer.sharedData.modelManager.find( TERRAIN_MODEL );
	if ( modelIt != this->planetViewer.sharedData.modelManager.end() )
	{

		delete modelIt->second;
		this->planetViewer.sharedData.modelManager.erase( modelIt );
	}

	//----------------------------
	//parse script
	vector<mathParser::Token> outputTokenArray;

	mathParser::parseStringToTokens( baselineData.terrainScript, outputTokenArray);

	cout<<"...adding path prefix to identifier height map tokens"<<endl;
	//add terrainResourcesPath prefix
	for ( int i=0; i<outputTokenArray.size(); ++i)
	{
		if ( outputTokenArray[i].type == mathParser::IDENTIFIER )
		{
			outputTokenArray[i].value = this->planetViewer.sharedData.externalParams.terrainResourcesPath + outputTokenArray[i].value;
		}
	}

	vector<mathParser::Token> prefixTokenArray;
	mathParser::convertToPrefix( outputTokenArray, prefixTokenArray );


	cout<<"...building height map tree"<<endl;
	AbstractNode< TreeTextureNode > *dstTree = NULL;
	buildTree< TreeTextureNode >( prefixTokenArray, &dstTree );
	
	cout<<"...evalueating height map tree"<<endl;

	

	TreeTextureNode resVal;
	dstTree->evaluate( resVal );


	// create new terrain model from height texture 
	// and store it under sring 'terrainModel'
	unsigned int surfTexId = TextureManager::getInstance().getTexture( baselineData.surfaceTexName );
	this->planetViewer.createModelFromHeightMap( resVal.texture, surfTexId, TERRAIN_MODEL );
	modelIt = this->planetViewer.sharedData.modelManager.find( TERRAIN_MODEL );

	
	// materials of terrains are specified in standalone MTL file
	// its set of materilas is stored in materialManager under
	// TERRAIN_MATERIAL="terrainMaterials"
	modelIt->second->getMeshes().back().material = this->planetViewer.sharedData.materialManager[ TERRAIN_MATERIAL ]->getMaterial( baselineData.materialName );


	modelIt->second->translate( 0, 3, 0);
	models.push_back( modelIt->second );
	this->planetViewer.spacePlanet->setModels( models );		
}
