/*
 *  Original work: Copyright (c) 2014, Oculus VR, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  RakNet License.txt file in the licenses directory of this source tree. An additional grant 
 *  of patent rights can be found in the RakNet Patents.txt file in the same directory.
 *
 *
 *  Modified work: Copyright (c) 2016-2020, SLikeSoft UG (haftungsbeschränkt)
 *
 *  This source code was modified by SLikeSoft. Modifications are licensed under the MIT-style
 *  license found in the license.txt file in the root directory of this source tree.
 */

#include "slikenetstuff.h"

#include "slikenet/NetworkIDManager.h"
#include "CDemo.h"
#include "slikenet/time.h"
#include "slikenet/GetTime.h"
#include "slikenet/SocketLayer.h"
#include "slikenet/linux_adapter.h"
#include "slikenet/osx_adapter.h"

using namespace SLNet;

RakPeerInterface *rakPeer;
NetworkIDManager *networkIDManager;
ReplicaManager3Irrlicht *irrlichtReplicaManager3;
NatPunchthroughClient *natPunchthroughClient;
CloudClient *cloudClient;
SLNet::FullyConnectedMesh2 *fullyConnectedMesh2;
PlayerReplica *playerReplica;

/*
class DebugBoxSceneNode : public scene::ISceneNode 
{
public:
	DebugBoxSceneNode(scene::ISceneNode* parent,
		scene::ISceneManager* mgr,
		s32 id = -1);
	virtual const core::aabbox3d<f32>& getBoundingBox() const;
	virtual void OnRegisterSceneNode();
	virtual void render();

	CDemo *demo;
};
DebugBoxSceneNode::DebugBoxSceneNode(
									 scene::ISceneNode* parent,
									 scene::ISceneManager* mgr,
									 s32 id)
									 : scene::ISceneNode(parent, mgr, id)
{
#ifdef _DEBUG
	setDebugName("DebugBoxSceneNode");
#endif
	setAutomaticCulling(scene::EAC_OFF);
} 
const core::aabbox3d<f32>& DebugBoxSceneNode::getBoundingBox() const
{
	return demo->GetSyndeyBoundingBox();
}
void DebugBoxSceneNode::OnRegisterSceneNode()
{
	if (IsVisible)
		demo->GetSceneManager()->registerNodeForRendering(this, scene::ESNRP_SOLID);
}
void DebugBoxSceneNode::render()
{
	if (DebugDataVisible)
	{ 
		video::IVideoDriver* driver = SceneManager->getVideoDriver();
		driver->setTransform(video::ETS_WORLD, AbsoluteTransformation); 

		video::SMaterial m;
		m.Lighting = false;
		demo->GetDevice()->getVideoDriver()->setMaterial(m);
		demo->GetDevice()->getVideoDriver()->draw3DBox(demo->GetSyndeyBoundingBox());
	}
}
*/

DataStructures::List<PlayerReplica*> PlayerReplica::playerList;

// Take this many milliseconds to move the visible position to the real position
static const float INTERP_TIME_MS=100.0f;

void InstantiateRakNetClasses(void)
{
	static const int MAX_PLAYERS=32;
	static const unsigned short TCP_PORT=0;
	static const SLNet::TimeMS UDP_SLEEP_TIMER=30;

	// Basis of all UDP communications
	rakPeer= SLNet::RakPeerInterface::GetInstance();
	// Using fixed port so we can use AdvertiseSystem and connect on the LAN if the server is not available.
	SLNet::SocketDescriptor sd(1234,0);
	sd.socketFamily=AF_INET; // Only IPV4 supports broadcast on 255.255.255.255
	while (IRNS2_Berkley::IsPortInUse(sd.port, sd.hostAddress, sd.socketFamily, SOCK_DGRAM)==true)
		sd.port++;
	// +1 is for the connection to the NAT punchthrough server
	SLNET_VERIFY(rakPeer->Startup(MAX_PLAYERS+1,&sd,1) == SLNet::RAKNET_STARTED);
	rakPeer->SetMaximumIncomingConnections(MAX_PLAYERS);
	// Fast disconnect for easier testing of host migration
	rakPeer->SetTimeoutTime(5000,UNASSIGNED_SYSTEM_ADDRESS);
	// ReplicaManager3 replies on NetworkIDManager. It assigns numbers to objects so they can be looked up over the network
	// It's a class in case you wanted to have multiple worlds, then you could have multiple instances of NetworkIDManager
	networkIDManager=new NetworkIDManager;
	// Automatically sends around new / deleted / changed game objects
	irrlichtReplicaManager3=new ReplicaManager3Irrlicht;
	irrlichtReplicaManager3->SetNetworkIDManager(networkIDManager);
	rakPeer->AttachPlugin(irrlichtReplicaManager3);
	// Automatically destroy connections, but don't create them so we have more control over when a system is considered ready to play
	irrlichtReplicaManager3->SetAutoManageConnections(false,true);
	// Create and register the network object that represents the player
	playerReplica = new PlayerReplica;
	irrlichtReplicaManager3->Reference(playerReplica);
	// Lets you connect through routers
	natPunchthroughClient=new NatPunchthroughClient;
	rakPeer->AttachPlugin(natPunchthroughClient);
	// Uploads game instance, basically client half of a directory server
	// Server code is in NATCompleteServer sample
	cloudClient=new CloudClient;
	rakPeer->AttachPlugin(cloudClient);
	fullyConnectedMesh2=new FullyConnectedMesh2;
	fullyConnectedMesh2->SetAutoparticipateConnections(false);
	fullyConnectedMesh2->SetConnectOnNewRemoteConnection(false, "");
	rakPeer->AttachPlugin(fullyConnectedMesh2);
	// Connect to the NAT punchthrough server
	SLNET_VERIFY(rakPeer->Connect(DEFAULT_NAT_PUNCHTHROUGH_FACILITATOR_IP, DEFAULT_NAT_PUNCHTHROUGH_FACILITATOR_PORT,0,0) == CONNECTION_ATTEMPT_STARTED);

	// Advertise ourselves on the lAN if the NAT punchthrough server is not available
 	//for (int i=0; i < 8; i++)
 	//	rakPeer->AdvertiseSystem("255.255.255.255", 1234+i, 0,0,0);
}
void DeinitializeRakNetClasses(void)
{
	// Shutdown so the server knows we stopped
	rakPeer->Shutdown(100,0);
	SLNet::RakPeerInterface::DestroyInstance(rakPeer);
	delete networkIDManager;
	delete irrlichtReplicaManager3;
	delete natPunchthroughClient;
	delete cloudClient;
	delete fullyConnectedMesh2;
	// ReplicaManager3 deletes all referenced objects, including this one
	//playerReplica->PreDestruction(0);
	//delete playerReplica;
}
BaseIrrlichtReplica::BaseIrrlichtReplica()
{
}
BaseIrrlichtReplica::~BaseIrrlichtReplica()
{

}
void BaseIrrlichtReplica::SerializeConstruction(SLNet::BitStream *constructionBitstream, SLNet::Connection_RM3 *destinationConnection)
{
	// unused parameters
	(void)destinationConnection;

	constructionBitstream->Write(position);
}
bool BaseIrrlichtReplica::DeserializeConstruction(SLNet::BitStream *constructionBitstream, SLNet::Connection_RM3 *sourceConnection)
{
	// unused parameters
	(void)sourceConnection;

	constructionBitstream->Read(position);
	return true;
}
RM3SerializationResult BaseIrrlichtReplica::Serialize(SLNet::SerializeParameters *serializeParameters)
{
	// unused parameters
	(void)serializeParameters;

	return RM3SR_BROADCAST_IDENTICALLY;
}
void BaseIrrlichtReplica::Deserialize(SLNet::DeserializeParameters *deserializeParameters)
{
	// unused parameters
	(void)deserializeParameters;
}
void BaseIrrlichtReplica::Update(SLNet::TimeMS curTime)
{
	// unused parameters
	(void)curTime;
}
PlayerReplica::PlayerReplica()
{
	model=0;
	rotationDeltaPerMS=0.0f;
	isMoving=false;
	deathTimeout=0;
	lastUpdate= SLNet::GetTimeMS();
	playerList.Push(this,_FILE_AND_LINE_);
}
PlayerReplica::~PlayerReplica()
{
	unsigned int index = playerList.GetIndexOf(this);
	if (index != (unsigned int) -1)
		playerList.RemoveAtIndexFast(index);
}
void PlayerReplica::WriteAllocationID(SLNet::Connection_RM3 *destinationConnection, SLNet::BitStream *allocationIdBitstream) const
{
	// unused parameters
	(void)destinationConnection;

	allocationIdBitstream->Write(SLNet::RakString("PlayerReplica"));
}
void PlayerReplica::SerializeConstruction(SLNet::BitStream *constructionBitstream, SLNet::Connection_RM3 *destinationConnection)
{
	BaseIrrlichtReplica::SerializeConstruction(constructionBitstream, destinationConnection);
	constructionBitstream->Write(rotationAroundYAxis);
	constructionBitstream->Write(playerName);
	constructionBitstream->Write(IsDead());
}
bool PlayerReplica::DeserializeConstruction(SLNet::BitStream *constructionBitstream, SLNet::Connection_RM3 *sourceConnection)
{
	if (!BaseIrrlichtReplica::DeserializeConstruction(constructionBitstream, sourceConnection))
		return false;
	constructionBitstream->Read(rotationAroundYAxis);
	constructionBitstream->Read(playerName);
	constructionBitstream->Read(isDead);
	return true;
}
void PlayerReplica::PostDeserializeConstruction(SLNet::BitStream *constructionBitstream, SLNet::Connection_RM3 *destinationConnection)
{
	// unused parameters
	(void)constructionBitstream;
	(void)destinationConnection;

	// Object was remotely created and all data loaded. Now we can make the object visible
	scene::IAnimatedMesh* mesh = 0;
	scene::ISceneManager *sm = demo->GetSceneManager();
	mesh = sm->getMesh(IRRLICHT_MEDIA_PATH "sydney.md2");
	model = sm->addAnimatedMeshSceneNode(mesh, 0);

//	DebugBoxSceneNode * debugBox = new DebugBoxSceneNode(model,sm);
//	debugBox->demo=demo;
//	debugBox->setDebugDataVisible(true); 

	model->setPosition(position);
	model->setRotation(core::vector3df(0, rotationAroundYAxis, 0));
	model->setScale(core::vector3df(2,2,2));
	model->setMD2Animation(scene::EMAT_STAND);
	curAnim=scene::EMAT_STAND;
	model->setMaterialTexture(0, demo->GetDevice()->getVideoDriver()->getTexture(IRRLICHT_MEDIA_PATH "sydney.bmp"));
	model->setMaterialFlag(video::EMF_LIGHTING, true);
	model->addShadowVolumeSceneNode();
	model->setAutomaticCulling ( scene::EAC_BOX );
	model->setVisible(true);
	model->setAnimationEndCallback(this);
	wchar_t playerNameWChar[1024];
	mbstowcs_s(nullptr, playerNameWChar, playerName.C_String(), 1023);
	scene::IBillboardSceneNode *bb = sm->addBillboardTextSceneNode(0, playerNameWChar, model);
	bb->setSize(core::dimension2df(40,20));
	bb->setPosition(core::vector3df(0,model->getBoundingBox().MaxEdge.Y+bb->getBoundingBox().MaxEdge.Y-bb->getBoundingBox().MinEdge.Y+5.0f,0));
	bb->setColor(video::SColor(255,255,128,128), video::SColor(255,255,128,128));
}
void PlayerReplica::PreDestruction(SLNet::Connection_RM3 *sourceConnection)
{
	// unused parameters
	(void)sourceConnection;

	if (model)
		model->remove();
}
RM3SerializationResult PlayerReplica::Serialize(SLNet::SerializeParameters *serializeParameters)
{
	BaseIrrlichtReplica::Serialize(serializeParameters);
	serializeParameters->outputBitstream[0].Write(position);
	serializeParameters->outputBitstream[0].Write(rotationAroundYAxis);
	serializeParameters->outputBitstream[0].Write(isMoving);
	serializeParameters->outputBitstream[0].Write(IsDead());
	return RM3SR_BROADCAST_IDENTICALLY;
}
void PlayerReplica::Deserialize(SLNet::DeserializeParameters *deserializeParameters)
{
	BaseIrrlichtReplica::Deserialize(deserializeParameters);
	deserializeParameters->serializationBitstream[0].Read(position);
	deserializeParameters->serializationBitstream[0].Read(rotationAroundYAxis);
	deserializeParameters->serializationBitstream[0].Read(isMoving);
	bool wasDead=isDead;
	deserializeParameters->serializationBitstream[0].Read(isDead);
	if (isDead==true && wasDead==false)
	{
		demo->PlayDeathSound(position);
	}

	core::vector3df positionOffset;
	positionOffset=position-model->getPosition();
	positionDeltaPerMS = positionOffset / INTERP_TIME_MS;
	float rotationOffset;
	rotationOffset=GetRotationDifference(rotationAroundYAxis,model->getRotation().Y);
	rotationDeltaPerMS = rotationOffset / INTERP_TIME_MS;
	interpEndTime = SLNet::GetTimeMS() + (SLNet::TimeMS) INTERP_TIME_MS;
}
void PlayerReplica::Update(SLNet::TimeMS curTime)
{
	// Is a locally created object?
	if (creatingSystemGUID==rakPeer->GetGuidFromSystemAddress(SLNet::UNASSIGNED_SYSTEM_ADDRESS))
	{
		// Local player has no mesh to interpolate
		// Input our camera position as our player position
		playerReplica->position=demo->GetSceneManager()->getActiveCamera()->getPosition()-irr::core::vector3df(0,CAMERA_HEIGHT,0);
		playerReplica->rotationAroundYAxis=demo->GetSceneManager()->getActiveCamera()->getRotation().Y-90.0f;
		isMoving=demo->IsMovementKeyDown();

		// Ack, makes the screen messed up and the mouse move off the window
		// Find another way to keep the dead player from moving
	//	demo->EnableInput(IsDead()==false);

		return;
	}

	// Update interpolation
	SLNet::TimeMS elapsed = curTime-lastUpdate;
	if (elapsed<=1)
		return;
	if (elapsed>100)
		elapsed=100;

	lastUpdate=curTime;
	irr::core::vector3df curPositionDelta = position-model->getPosition();
	irr::core::vector3df interpThisTick = positionDeltaPerMS*(float) elapsed;
	if (curTime < interpEndTime && interpThisTick.getLengthSQ() < curPositionDelta.getLengthSQ())
	{
		model->setPosition(model->getPosition()+positionDeltaPerMS*(float) elapsed);
	}
	else
	{
		model->setPosition(position);
	}

	float curRotationDelta = GetRotationDifference(rotationAroundYAxis,model->getRotation().Y);
	float interpThisTickRotation = rotationDeltaPerMS*(float)elapsed;
	if (curTime < interpEndTime && fabs(interpThisTickRotation) < fabs(curRotationDelta))
	{
		model->setRotation(model->getRotation()+core::vector3df(0,interpThisTickRotation,0));
	}
	else
	{
		model->setRotation(core::vector3df(0,rotationAroundYAxis,0));
	}

	if (isDead)
	{
		UpdateAnimation(scene::EMAT_DEATH_FALLBACK);
		model->setLoopMode(false);		
	}
	else if (curAnim!=scene::EMAT_ATTACK)
	{
		if (isMoving)
		{
			UpdateAnimation(scene::EMAT_RUN);
			model->setLoopMode(true);
		}
		else
		{
			UpdateAnimation(scene::EMAT_STAND);
			model->setLoopMode(true);
		}
	}	
}
void PlayerReplica::UpdateAnimation(irr::scene::EMD2_ANIMATION_TYPE anim)
{
	if (anim!=curAnim)
		model->setMD2Animation(anim);
	curAnim=anim;
}
float PlayerReplica::GetRotationDifference(float r1, float r2)
{
	float diff = r1-r2;
	while (diff>180.0f)
		diff-=360.0f;
	while (diff<-180.0f)
		diff+=360.0f;
	return diff;
}
void PlayerReplica::OnAnimationEnd(scene::IAnimatedMeshSceneNode* node)
{
	// unused parameters
	(void)node;

	if (curAnim==scene::EMAT_ATTACK)
	{
		if (isMoving)
		{
			UpdateAnimation(scene::EMAT_RUN);
			model->setLoopMode(true);
		}
		else
		{
			UpdateAnimation(scene::EMAT_STAND);
			model->setLoopMode(true);
		}
	}
}
void PlayerReplica::PlayAttackAnimation(void)
{
	if (isDead==false)
	{
		UpdateAnimation(scene::EMAT_ATTACK);
		model->setLoopMode(false);		
	}
}
bool PlayerReplica::IsDead(void) const
{
	return deathTimeout > SLNet::GetTimeMS();
}
BallReplica::BallReplica()
{
	creationTime= SLNet::GetTimeMS();
}
BallReplica::~BallReplica()
{
}
void BallReplica::WriteAllocationID(SLNet::Connection_RM3 *destinationConnection, SLNet::BitStream *allocationIdBitstream) const
{
	// unused parameters
	(void)destinationConnection;

	allocationIdBitstream->Write(SLNet::RakString("BallReplica"));
}
void BallReplica::SerializeConstruction(SLNet::BitStream *constructionBitstream, SLNet::Connection_RM3 *destinationConnection)
{
	BaseIrrlichtReplica::SerializeConstruction(constructionBitstream, destinationConnection);
	constructionBitstream->Write(shotDirection);
}
bool BallReplica::DeserializeConstruction(SLNet::BitStream *constructionBitstream, SLNet::Connection_RM3 *sourceConnection)
{
	if (!BaseIrrlichtReplica::DeserializeConstruction(constructionBitstream, sourceConnection))
		return false;
	constructionBitstream->Read(shotDirection);
	return true;
}
void BallReplica::PostDeserializeConstruction(SLNet::BitStream *constructionBitstream, SLNet::Connection_RM3 *destinationConnection)
{
	// unused parameters
	(void)constructionBitstream;
	(void)destinationConnection;

	// Shot visible effect and BallReplica classes are not linked, but they update the same way, such that
	// they are in the same spot all the time
	demo->shootFromOrigin(position, shotDirection);

	// Find the owner of this ball, and make them play the attack animation
	unsigned int idx;
	for (idx=0; idx < PlayerReplica::playerList.Size(); idx++)
	{
		if (PlayerReplica::playerList[idx]->creatingSystemGUID==creatingSystemGUID)
		{
			PlayerReplica::playerList[idx]->PlayAttackAnimation();
			break;
		}
	}
}
void BallReplica::PreDestruction(SLNet::Connection_RM3 *sourceConnection)
{
	// unused parameters
	(void)sourceConnection;

	// The system that shot this ball destroyed it, or disconnected
	// Technically we should clear out the node visible effect too, but it's not important for now
}
RM3SerializationResult BallReplica::Serialize(SLNet::SerializeParameters *serializeParameters)
{
	BaseIrrlichtReplica::Serialize(serializeParameters);
	return RM3SR_BROADCAST_IDENTICALLY;
}
void BallReplica::Deserialize(SLNet::DeserializeParameters *deserializeParameters)
{
	BaseIrrlichtReplica::Deserialize(deserializeParameters);
}
void BallReplica::Update(SLNet::TimeMS curTime)
{
	// Is a locally created object?
	if (creatingSystemGUID==rakPeer->GetGuidFromSystemAddress(SLNet::UNASSIGNED_SYSTEM_ADDRESS))
	{
		// Destroy if shot expired
		if (curTime > shotLifetime)
		{
			// Destroy on network
			BroadcastDestruction();
			delete this;
			return;
		}
	}

	// Keep at the same position as the visible effect
	// Deterministic, so no need to actually transmit position
	// The variable position is the origin that the ball was created at. For the player, it is their actual position
	SLNet::TimeMS elapsedTime;
	// Due to ping variances and timestamp miscalculations, it's possible with very low pings to get a slightly negative time, so we have to check
	if (curTime>=creationTime)
		elapsedTime = curTime - creationTime;
	else
		elapsedTime=0;
	irr::core::vector3df updatedPosition = position + shotDirection * (float) elapsedTime * SHOT_SPEED;

	// See if the bullet hit us
	if (creatingSystemGUID!=rakPeer->GetGuidFromSystemAddress(SLNet::UNASSIGNED_SYSTEM_ADDRESS))
	{
		if (playerReplica->IsDead()==false)
		{
			//float playerHalfHeight=demo->GetSyndeyBoundingBox().getExtent().Y/2;
			irr::core::vector3df positionRelativeToCharacter = updatedPosition-playerReplica->position;//+core::vector3df(0,playerHalfHeight,0);
			if (demo->GetSyndeyBoundingBox().isPointInside(positionRelativeToCharacter))
			//if ((playerReplica->position+core::vector3df(0,playerHalfHeight,0)-updatedPosition).getLengthSQ() < BALL_DIAMETER*BALL_DIAMETER/4.0f)
			{
				// We're dead for 3 seconds
				playerReplica->deathTimeout=curTime+3000;
			}
		}
	}
}
SLNet::Replica3 *Connection_RM3Irrlicht::AllocReplica(SLNet::BitStream *allocationId, ReplicaManager3 *replicaManager3)
{
	// unused parameters
	(void)replicaManager3;

	SLNet::RakString typeName; allocationId->Read(typeName);
	if (typeName=="PlayerReplica") {BaseIrrlichtReplica *r = new PlayerReplica; r->demo=demo; return r;}
	if (typeName=="BallReplica") {BaseIrrlichtReplica *r = new BallReplica; r->demo=demo; return r;}
	return 0;
}
