/*
	Lan Wars.
	Copyright (c) 2009, Darragh Coy.
	All rights reserved.
	Original work(s), source code & author(s) details available at: http://www.darraghcoy.com

	Redistribution and use in source and binary forms, with or without modification, are permitted 
	provided that the following conditions are met:

		* 	Redistributions of source code must retain the above copyright notice, this list of 
			conditions and the following disclaimer.
		* 	Redistributions in binary form must reproduce the above copyright notice, this list of 
			conditions and the following disclaimer in the documentation.
		* 	Neither the name of the author nor the names of contributors may be used to endorse or 
			promote products derived from this software without specific prior written permission.
		* 	The use of any third party libraries, modules or data files provided with the source 
			form of the program must follow and respect the individual licensing terms of these 
			components. You do not have permission to modify or circumvent the licensing terms for 
			provided third party components unless the third party licenses specifically allow this. 
			If you do not agree to the terms of provided third party components then you do not have 
			permission to use them in your derived work(s).
		* 	The terms of this license do not apply to separate (but related) distributions if supplied 
			by the author(s), only to the contents of this source distribution, derived works, and the 
			binaries produced by either modified or unmodified versions of the source code. The terms 
			of this license do not also apply to any third party libraries or components provided with 
			the source distribution. Such third party elements are licensed under their own separate 
			agreements.

	THIS SOFTWARE IS PROVIDED BY DARRAGH COY ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, 
	BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
	ARE DISCLAIMED. IN NO EVENT SHALL DARRAGH COY BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
	EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 
	OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 
	OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 
	IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

//=====================================================================================================================
/// @brief		This helper class handles sending and receiving of data for the client.
///				It is contained within the GameSession class itself.
//=====================================================================================================================

#include "Precompiled.h"
#include "Game/GameSessionDirector_Client.h"
#include "Game/GameSession.h"
#include "Game/GameSession.h"
#include "Game/GameCore.h"
#include "Game/GameMenus.h"
#include "Game/GameObjectManager.h"
#include "Game/GamePacketCenter.h"
#include "Game/GameNetSerializer.h"
#include "GameObjects/GameObjectBase.h"

using namespace Game;

//=====================================================================================================================
/// @brief		Creates this helper object
/// @param		session - session we belong to
/// @pre		We must be connected
/// @pre		We must be a client connection
//=====================================================================================================================

GameSessionDirector_Client::GameSessionDirector_Client( GameSession & session ) : m_session(session)
{
	// Make sure ok:
	
	ENSURE( m_session.IsConnected()	, "We must be connected"			)
	ENSURE( m_session.IsClient()	, "We must be a client connection"	)
}

//=====================================================================================================================
/// @brief		Does an update of the client session
/// @pre		We must be connected
/// @pre		We must be a client
/// @todo		Determine scope (in or out) for non-client controlled objects from seeing whether other non-client 
///				controlled objects got updated.
//=====================================================================================================================

void GameSessionDirector_Client::Update()
{
	// Create a reference to this in case we get deleted (due to disconnect) and our object gets invalidated:

	GameSession & session = m_session;

	// Make sure ok:

	ENSURE( session.IsConnected()	,	"We must be connected"	)
	ENSURE( session.IsClient()	,	"We must be a client"	)
	
	// This is our outgoing packet to the server
	
	GamePacket out; out.SetAddress( session.GetServerAddress() );
	
	// If this is true then we send a packet to the server:
	
	bool send = false;
	
	if ( GetTickCount() - session.GetServerPeer().GetLastSendTime() > session.m_settings.m_client_cpg_interval )
	{
		send = true;
	}
	
	// Do sends for the client
	
	if ( send ) Send(out);
	
	// Now do receives for the client:
	
	if ( session.IsConnected() )
	{
		// This is the number of packets to receive:
		
		unsigned received_packets = session.m_packet_center->GetIncomingCount();
		
		// If there is nothing then do no more:
		
		if ( received_packets != 0 )
		{		
			// Read all of the incoming packets into this list:
			
			std::vector<GamePacket> incoming(received_packets);
			
			// Read all of the incoming packets:
			
			for ( unsigned i = 0 ; i < received_packets ; ++i ) session.m_packet_center->ReadPacket(incoming[i]);
			
			// Sort all of the incoming packets: oldest first
			
			std::sort(incoming.begin(),incoming.end());
			
			// Do the receiving: continue reading until all are read or until we are disconnected
			
			for ( unsigned i = 0 ; i < incoming.size() && session.IsConnected() ; ++i )
			{
				// This should never fail:
				
				ENSURE( session.m_packet_center != 0 , "Game sesion must have a packet center" )
				
				// Ignore the packet if it is not from the server:
				
				if ( incoming[i].GetAddress() == session.GetServerAddress() ) Receive(incoming[i],out);
			}
			
			// Only if still connected:

			if ( session.IsConnected() )
			{
				// Reset the time since we last received a packet from the server:
				
				session.GetServerPeer().SetLastReceiveTime();
			}
		}
	}
	
	// Send our packet off to the server if still connected:
	
	if ( ( send || out.GetBitCount() > 0 ) && session.IsConnected() ) 
	{
		// This should never fail:
		
		ENSURE( session.m_packet_center != 0 , "Game sesion must have a packet center" )	
	
		// Terminate the packet to the server:
		
		out.WriteChunkId(GamePacketChunk::PKCKID_END);
	
		// Off you go
	
		session.m_packet_center->WritePacket(out);
		
		// Reset the time since we last sent a packet to the server:
		
		session.GetServerPeer().SetLastSendTime();
	}
}

//=====================================================================================================================
/// @brief		Does all sending for the client
/// @param		out - packet to write our the data to
/// @pre		We must be connected
/// @pre		We must be a client
//=====================================================================================================================

void GameSessionDirector_Client::Send( GamePacket & out )
{
	// Make sure ok:

	ENSURE( m_session.IsConnected()	,	"We must be connected"	)
	ENSURE( m_session.IsClient()	,	"We must be a client"	)
	
	// See if we are playing or not yet:
	
	if ( m_session.GetState() == GameSession::STATE_CONNECTING )
	{
		// Send a join request to the server: keep on sending it until we get through or are refused. Brute force!
		
		out.WriteChunkId(GamePacketChunk::PKCKID_JOIN);
	}
	else
	{
		// Get our control object:
		
		GameObjectBase * control_object = m_session.GetControlObject();
	
		// Send updates for our object to the server:
		
		if ( control_object )
		{
			// Make sure it's not deleted:
			
			control_object->AddReference();		
		
			// Write the chunk id and id of the object into the packet:
		
			out.WriteChunkId(GamePacketChunk::PKCKID_GAME_DATA);
			out.WritePackedUint(control_object->GetNetId());
			
			// In debug make sure data is written:
			
			#ifdef DEBUG
			
				// Save whether it sent
			
				bool sent = control_object->NetSend(out,NetObject::SEND_CONTEXT_CLIENT_TO_SERVER);
				
				// Make sure it sent:
			
				ENSURE( sent , "Client controlled objects have no choice whether to send data to the server" )
			
			#else
			
				// Off you go
			
				control_object->NetSend(out,NetObject::SEND_CONTEXT_CLIENT_TO_SERVER);
			
			#endif
			
			// Done with this
			
			control_object->RemoveReference();
		}
	}
}

//=====================================================================================================================
/// @brief		Does all receiving for the client
/// @param		in - incoming packet
/// @param		out - outgoing packet, in case we need to write acknowledgements
//=====================================================================================================================

void GameSessionDirector_Client::Receive( GamePacket & in , GamePacket & out )
{
	// Create a reference to this in case we get deleted (due to disconnect) and our object gets invalidated:

	GameSession & session = m_session;

	// Continue reading the packet until we reach the end of the packet:
	
	bool continue_reading = true;
	
	// Keep reading the packet:
	
	while ( continue_reading )
	{
		// Read the id of the next chunk:
		
		GamePacketChunk::ChunkID id = GamePacketChunk::PKCKID_INVALID; 
		
		// Do the read: if it failed then abort
		
		if ( in.ReadChunkId(id) == false )
		{	
			continue_reading = false; continue;
		}
		
		// See what the chunk id is:
		
		switch ( id )
		{	
			// These is ok: there is no data in these so nothing can go wrong
							
			case GamePacketChunk::PKCKID_DISCONNECT:	HandlePacketChunk_Disconnect();										break;
			case GamePacketChunk::PKCKID_JOIN_REFUSE:	HandlePacketChunk_ConnectionRefused();								break;
			case GamePacketChunk::PKCKID_REMOTE_UPDATE:	HandlePacketChunk_RemoteUpdate();									break;
			
			// If these go wrong then drop the packet and read no further:
			
			case GamePacketChunk::PKCKID_JOIN_ACK:		continue_reading = HandlePacketChunk_ConnectionAcknowledged(in);	break;
			case GamePacketChunk::PKCKID_GAME_DATA:		continue_reading = HandlePacketChunk_ObjectData(in);				break;
			case GamePacketChunk::PKCKID_CREATE:		continue_reading = HandlePacketChunk_CreateObject(in,out);			break;
			case GamePacketChunk::PKCKID_DELETE:		continue_reading = HandlePacketChunk_DeleteObject(in,out);			break;
		
			// Unknown / Unsupported / Terminating chunk. Stop reading.
		
			default: continue_reading = false;
		}
		
		// If we are no longer connected then do no more reading:
		
		if ( session.IsConnected() == false ) continue_reading = false;		
	}
}

//=====================================================================================================================
/// @brief		Handles a disconnect packet chunk from the server
/// @pre		The GameCore object must exist
/// @pre		The GameCore object must have a menus object
//=====================================================================================================================

void GameSessionDirector_Client::HandlePacketChunk_Disconnect()
{
	// Make sure ok
	
	ENSURE( GameCore::GetSingleton()				, "The GameCore object must exist"					)
	ENSURE( GameCore::GetSingleton()->GetMenus()	, "The GameCore object must have a menus object"	)

	// Disconnect ourselves:
	
	if ( m_session.IsConnected() ) 
	{
		// Disconnect
	
		m_session.Disconnect();
	
		// Go to the disconnected menu:
		
		GameCore::GetSingleton()->GetMenus()->SetMenu( GameMenus::MENU_SERVER_DISCONNECT );
	}
}

//=====================================================================================================================
/// @brief		Handles a connection refused packet chunk
/// @pre		The GameCore object must exist
/// @pre		The GameCore object must have a menus object
//=====================================================================================================================

void GameSessionDirector_Client::HandlePacketChunk_ConnectionRefused()
{
	// Make sure ok
	
	ENSURE( GameCore::GetSingleton()				, "The GameCore object must exist"					)
	ENSURE( GameCore::GetSingleton()->GetMenus()	, "The GameCore object must have a menus object"	)

	// Only if connecting: ingore it otherwise. May have reached us mistakenly or the result of foul play ... 
	
	if ( m_session.GetState() == GameSession::STATE_CONNECTING )
	{
		// Disconnect ourselves:
		
		if ( m_session.IsConnected() )
		{
			// Disconnect
		
			m_session.Disconnect();
		
			// Go to the connection refused menu:
			
			GameCore::GetSingleton()->GetMenus()->SetMenu( GameMenus::MENU_CONNECTION_REFUSED );
		}
	}
}

//=====================================================================================================================
/// @brief		Handles a connection acknowledeged packet chunk
/// @param		in - incoming packet
/// @returns	True if the chunk was read successfully, false otherwise
/// @pre		The GameCore object must exist
/// @pre		The GameCore object must have a menus object
//=====================================================================================================================

bool GameSessionDirector_Client::HandlePacketChunk_ConnectionAcknowledged( GamePacket & in )
{
	// Make sure ok
	
	ENSURE( GameCore::GetSingleton()				, "The GameCore object must exist"					)
	ENSURE( GameCore::GetSingleton()->GetMenus()	, "The GameCore object must have a menus object"	)

	// Read our peer id:
	
	unsigned peer_id = 0xffffffff;
	
	// Read the peer id:
	
	bool success = in.ReadPackedUint(peer_id);

	// Only if we are not playing already;
	
	if ( success && m_session.GetState() == GameSession::STATE_CONNECTING )
	{
		// Now playing the game:
		
		m_session.m_session_state = GameSession::STATE_PLAY;
		
		// Set our peer id:
		
		m_session.m_peer_id = peer_id;
		
		// Don't show the connecting menu any more:
		
		GameCore::GetSingleton()->GetMenus()->SetMenu( GameMenus::MENU_NONE );
		
		// Center the mouse in the window: do this so the view won't jerk initially
		
		Mouse::CenterInWindow( GameCore::GetSingleton()->GetWindow() );				
		
		// Set window titles:
		
		GameCore::GetSingleton()->GetWindow()->SetTitle
		(
			std::string(GameCore::GAME_TITLE) + ": Client with id '" + 
			String::ToString((int)peer_id) + "' using server @ " + 
			m_session.GetServerAddress().GetHostName(m_session.GetServerAddress().GetIP()) + ":" + 
			String::ToString((int)m_session.GetServerAddress().GetPort())
		);
		
		#ifdef USE_DEBUG_CONSOLE
		
			{
				// Set for debug console also
				
				std::string title = std::string(GameCore::DEBUG_CONSOLE_TITLE) + ": Client with id '";
				
				title += String::ToString((int)peer_id) + "' using server @ "; 
				title += m_session.GetServerAddress().GetHostName(m_session.GetServerAddress().GetIP()) + ":";
				title += String::ToString((int)m_session.GetServerAddress().GetPort());
		
				DebugConsole::SetWindowTitle(title.c_str());
			}
		
		#endif
	}
	else if ( success && m_session.GetState() == GameSession::STATE_PLAY )
	{
		// If we are already connected and the id does not match our peer id then junk the packet: it's not for us
		
		if ( peer_id != m_session.m_peer_id )
		{
			success = false;
		}
	}
	
	// Return true if we successfully read the packet:
	
	return success;
}

//=====================================================================================================================
/// @brief		Handles a create object packet chunk
/// @param		in - incoming packet
/// @param		out - outgoing packet
/// @returns	True if the chunk was read successfully, false otherwise
/// @pre		The GameCore object must exist
/// @pre		The GameObjectManager object must exist
//=====================================================================================================================

bool GameSessionDirector_Client::HandlePacketChunk_CreateObject( GamePacket & in , GamePacket & out )
{
	// Make sure ok
	
	ENSURE( GameCore::GetSingleton()						, "The GameCore object must exist"			)
	ENSURE( GameCore::GetSingleton()->GetObjectManager()	, "The GameObjectManager object must exist"	)

	// Pass this one to the serializer:
	
	GameObjectBase * created_object = GameNetSerializer::Unpack(in);
	
	// See if the object was created:
	
	if ( created_object )
	{
		// Make sure this object is not already in the game:
		
		if ( GameCore::GetSingleton()->GetObjectManager()->FindByNetId(created_object->GetNetId()) == 0 )
		{
			// Add it into the game:
			
			GameCore::GetSingleton()->GetObjectManager()->AddObject(created_object);
			
			// If we own this object then make it ours:
			
			if ( created_object->IsLocalObject() ) m_session.SetControlObject(created_object);
		}
		
		// Write a chunk the output acknowledging receipt of this create message:
		
		out.WriteChunkId( GamePacketChunk::PKCKID_CREATE_ACK );
		out.WritePackedUint( created_object->GetNetId() );
		
		// Done with this object:
		
		created_object->RemoveReference();
		
		// Success:
		
		return true;
	}
	else
	{
		// Failed:
		
		return false;
	}
}

//=====================================================================================================================
/// @brief		Handles a delete object packet chunk
/// @param		in - incoming packet
/// @param		out - outgoing packet
/// @returns	True if the chunk was read successfully, false otherwise
//=====================================================================================================================

bool GameSessionDirector_Client::HandlePacketChunk_DeleteObject( GamePacket & in , GamePacket & out )
{
	// Make sure ok
	
	ENSURE( GameCore::GetSingleton()						, "The GameCore object must exist"			)
	ENSURE( GameCore::GetSingleton()->GetObjectManager()	, "The GameObjectManager object must exist"	)
	
	// Read which net object the chunk is for:
	
	unsigned net_id = 0; 
	
	// Read the net id of the object involved:
	
	bool success = in.ReadPackedUint(net_id);
	
	// If this is a non-networked or invalid object then ignore:
	
	if ( success )
	{
		if ( net_id == NetObject::NET_ID_NONE || net_id == NetObject::NET_ID_INVALID )
		{
			success = false;
		}
	}
	
	// See if that was ok:
	
	if ( success )
	{
		// Try to find this object:
		
		GameObjectBase * delete_object = GameCore::GetSingleton()->GetObjectManager()->FindByNetId(net_id);
		
		// See if there:
		
		if ( delete_object )
		{
			// Add reference to it first:
			
			delete_object->AddReference();
			
			// If we are controlling it then make it not our control object anymore:
			
			if ( m_session.GetControlObject() == delete_object ) m_session.SetControlObject(0);
			
			// Remove it from the game world:
			
			GameCore::GetSingleton()->GetObjectManager()->RemoveObject(delete_object);
			
			// Remove the reference: now done 
			
			delete_object->RemoveReference();
		}
	
		// Acknowledge receipt of this delete message
		
		out.WriteChunkId( GamePacketChunk::PKCKID_DELETE_ACK );
		out.WritePackedUint( net_id );
	}
	
	// Return if the read was successful or not
	
	return success;
}

//=====================================================================================================================
/// @brief		Handles a game object data packet chunk
/// @param		in - incoming packet
/// @returns	True if the chunk was read successfully, false otherwise
//=====================================================================================================================

bool GameSessionDirector_Client::HandlePacketChunk_ObjectData( GamePacket & in )
{
	// Read the net id of the object involved:
	
	unsigned net_id = 0;
	
	// Read it:
	
	bool success = in.ReadPackedUint(net_id);

	// If this is a non-networked or invalid object then ignore:
	
	if ( success )
	{
		if ( net_id == NetObject::NET_ID_NONE || net_id == NetObject::NET_ID_INVALID )
		{
			success = false;
		}
	}
	
	// See if ok:
	
	if ( success )
	{
		// See if the object exists in the world:
		
		GameObjectBase * object = GameCore::GetSingleton()->GetObjectManager()->FindByNetId(net_id);
		
		// See if found:
		
		if ( object )
		{
			// See if this object is a local client controlled object or a remotely controlled object:
			
			if ( object->IsLocalObject() )
			{
				// Found the object: let it read the data contained within:
				
				success = object->NetReceive(in,NetObject::RECV_CONTEXT_SERVER_TO_CLIENT);
			}
			else
			{				
				// Put this object into scope: we got data for it
				
				object->SetScope(true);
			
				// Found the object: let it read the data contained within:
				
				success = object->NetReceive(in,NetObject::RECV_CONTEXT_REMOTE_TO_CLIENT);
			}
		}
		else
		{
			// Didn't find the object: not successful
			
			success = false;
		}
	}
	
	// Return if that went ok:
	
	return success;
}

//=====================================================================================================================
/// @brief		Handles an message from the server signifying that we are receiving data for remote objects
//=====================================================================================================================

bool GameSessionDirector_Client::HandlePacketChunk_RemoteUpdate()
{
	// Clear the scope on all objects
	
	ClearScopes();
	
	// This always succeeds:
	
	return true;
}

//=====================================================================================================================
/// @brief		Clears the scope on all remote objects and sets them to be out of scope
/// @pre		GameCore must exist
/// @pre		GameCore must have an object manager
//=====================================================================================================================

void GameSessionDirector_Client::ClearScopes()
{	
	// Make sure ok
	
	ENSURE( GameCore::GetSingleton()						, "GameCore must exist"						)
	ENSURE( GameCore::GetSingleton()->GetObjectManager()	, "GameCore must have an object manager"	)

	// Get all of the net objects:
	
	std::vector<GameObjectBase*> net_objects; GameCore::GetSingleton()->GetObjectManager()->GetNetObjects(net_objects);
	
	// Run through all the net objects:
	
	for ( unsigned i = 0 ; i < net_objects.size() ; ++i )
	{
		// Get this object:
		
		GameObjectBase & obj = * net_objects[i];
		
		// Make sure is a networked object: also make sure this is a remotely controlled object
		
		if ( obj.IsNetworkedObject() && obj.IsRemoteObject() )
		{
			// Put out of scope:
		
			obj.SetScope(false);
		}
	}
}
