/*
	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 class serves as post-office style object whereby packets are dropped in for later sending,
///				and where packets are put by for later receiving. This class automatically sends and receives 
///				in it's own thread when the network is ready. 
//=====================================================================================================================

#include "Precompiled.h"
#include "Game\GamePacketCenter.h"
#include "Game\GameCore.h"
#include "Network\DatagramSocket.h"

using namespace Game;

//=====================================================================================================================
// Statics
//=====================================================================================================================

// Debug only

#ifdef USE_DEBUG_CONSOLE

	/// A constant amount of lag to add to all packets
	
	int GamePacketCenter::s_fake_lag_ms = 0;
	
	/// A constant amount of random jitter to add to all packets.
	
	int GamePacketCenter::s_fake_jitter_ms = 0;
	
	/// Debug flag that enables monitoring of network stats
	
	bool GamePacketCenter::s_show_stats = false;
	
	/// For data monitoring: the number of packets that have come in within the monitoring interval
	
	int GamePacketCenter::s_stats_incoming_packets = 0;

	/// For data monitoring: the number of packets that have gone out within the monitoring interval
	
	int GamePacketCenter::s_stats_outgoing_packets = 0;
	
	/// For data monitoring: the number of bytes that have come in within the monitoring interval
	
	int GamePacketCenter::s_stats_incoming_bytes = 0;

	/// For data monitoring: the number of bytes that have gone out within the monitoring interval
	
	int GamePacketCenter::s_stats_outgoing_bytes = 0;
	
	/// For data monitoring: the time at which the current monitoring interval began
	
	int GamePacketCenter::s_stats_interval_begin = 0;

#endif

//=====================================================================================================================
/// @brief		Creates the packet center.
/// @param		port - this is the port we will communicate on. for a server this should be specified. for a client
///				this should be left at 0, meaning the system will pick a port for us.
/// @param		timeout_ms - amount of time in milliseconds that should elapse without receiving a packet before we 
///				consider the connection to have been lost.
/// @remarks	The packet center will immediately start sending and receiving, and will only stop once it is destroyed.
/// @pre		timeout_ms should be at least 100 ms. Anything lower is just plain silly.
//=====================================================================================================================

GamePacketCenter::GamePacketCenter( unsigned short port , unsigned timeout_ms )
{
	// Make sure ok:
	
	ENSURE( timeout_ms >= 100 , "timeout_ms should be at least 100 ms. Anything lower is just plain silly." )

	// Create the two pools of packets:
	
	m_outgoing_packet_pool = new Pool<GamePacket>( m_outgoing_packet_buffer , MAX_OUTGOING_PACKETS );
	m_incoming_packet_pool = new Pool<GamePacket>( m_incoming_packet_buffer , MAX_INCOMING_PACKETS );
	
	// Make our datagram socket
	
	m_socket = new DatagramSocket(port);	
	
	// Save timeout:
	
	m_timeout_ms = timeout_ms;
	
	// Keep the threads going until this is true:
	
	m_exit_io_threads = false;
	
	// There are no packets initially:
	
	m_outgoing_packets_condition.SetFalse();
	
	// Set the number for the next packet to be sent
	
	m_next_outgoing_packet_num = 0;
	
	// Make the two threads:
	
	m_incoming_thread = new Thread(IncomingPacketThread);
	m_outgoing_thread = new Thread(OutgoingPacketThread);
	
	// Begin running the io threads:
	
	m_incoming_thread->Run(this);
	m_outgoing_thread->Run(this);
	
	// Update the time since the last packet
	
	ResetTimeout();
	
	// Set the time since the last packet was sent to now:
	
	m_last_outgoing_time = GetTickCount();
	
	// Add in console commands:
	
	#ifdef USE_DEBUG_CONSOLE
	
		DebugConsole::AddCommand( "N_FakeLag"		, ConCmd_N_FakeLag		);
		DebugConsole::AddCommand( "N_FakeJitter"	, ConCmd_N_FakeJitter	);
		DebugConsole::AddCommand( "N_Stats"			, ConCmd_N_Stats		);
		
		// Clear these in debug also
		
		s_fake_lag_ms		= 0;
		s_fake_jitter_ms	= 0;		
	
	#endif
}

//=====================================================================================================================
/// @brief		Destroys the packet center.
//=====================================================================================================================

GamePacketCenter::~GamePacketCenter()
{
	// Do no more io on the two network threads:
	
	m_exit_io_threads = true;

	// Delete our socket: thus closing it. All blocking incoming read operations should halt.
	
	m_socket->RemoveReference();
	
	// Set the outgoing packets condition to true, in case the outgoing thread is waiting for outgoing packets
	
	m_outgoing_packets_condition.SetTrue();
	
	// Delete our two threads: this will end them
	
	delete m_incoming_thread;
	delete m_outgoing_thread;
	
	// Delete the two pools:
	
	m_outgoing_packet_pool->RemoveReference();
	m_incoming_packet_pool->RemoveReference();
	
	// Remove commands:
	
	#ifdef USE_DEBUG_CONSOLE
	
		DebugConsole::RemoveCommand( "N_FakeLag"	);
		DebugConsole::RemoveCommand( "N_FakeJitter"	);
		DebugConsole::RemoveCommand( "N_Stats"		);
	
	#endif	
}

//=====================================================================================================================
/// @brief		Tells if a timeout has occured. This will also return true if the datagram socket is invalid.
/// @returns	True if a timeout has occured
//=====================================================================================================================

bool GamePacketCenter::HasTimedOut()
{
	// See if the socket is invalid: 
	
	if ( m_socket->IsValid() == false ) return true;
	
	// Don't bother with synchronisation here: reading ints is atomic so theres no need for it
	
	unsigned last_time = m_last_incoming_time;
	
	// Get the current time:
	
	unsigned current_time = GetTickCount();
	
	// Get the difference:
	
	unsigned d = current_time - last_time;
	
	// See if bigger than timeout:
	
	if ( d > m_timeout_ms ) return true; else return false;
}

//=====================================================================================================================
/// @brief		Thread entry function for the incoming packets thread
/// @pre		The given packet center pointer cannot be null
//=====================================================================================================================

void GamePacketCenter::IncomingPacketThread( void * packet_center )
{
	// Make sure ok:
	
	ENSURE( packet_center , "The given packet center pointer cannot be null" )

	// Convert the packet center to it's proper type
	
	GamePacketCenter * pc = (GamePacketCenter*)(packet_center);
	
	// This is the next packet we will read
	
	GamePacket incoming_packet;
	
	// Continue running whilst we have been told to stay in the thread:
	
	while ( pc->m_exit_io_threads == false )
	{
		// Attempt to read the next packet
		
		bool success = incoming_packet.ReadFromSocket(pc->m_socket);
		
		// If that succeded then place it in the incoming packets list:
		
		if ( success )
		{
			// Lock the incoming packets list:
			
			pc->m_incoming_lock.Lock();
			
			// Get a free packet from the list. If none are free then this will use up the oldest received
			
			GamePacket * save_packet = pc->m_incoming_packet_pool->Alloc();
			
			// Copy our new packet into it:
			
			* save_packet = incoming_packet;
			
			// Done: unlock the incoming packets list
			
			pc->m_incoming_lock.Unlock();
			
			// Set the time that we received the last packet at:
			
			pc->ResetTimeout();
		}
	}
}

//=====================================================================================================================
/// @brief		Thread entry function for the outgoing packets thread
/// @pre		The given packet center pointer cannot be null
//=====================================================================================================================

void GamePacketCenter::OutgoingPacketThread( void * packet_center )
{
	// Make sure ok:
	
	ENSURE( packet_center , "The given packet center pointer cannot be null" )
	
	// Convert the packet center to it's proper type
	
	GamePacketCenter * pc = (GamePacketCenter*)(packet_center);
	
	// This is the next packet we will send
	
	GamePacket outgoing_packet;
	
	// Continue running whilst we have been told to stay in the thread:
	
	while ( pc->m_exit_io_threads == false )
	{	
		// Accquire the outgoing packet lock:
	
		pc->m_outgoing_lock.Lock();
		
		// See if there are any packets to be written:
		
		if ( pc->m_outgoing_packet_pool->GetUsedCount() > 0 )
		{
			// Get the tail item in the packet list: this is the oldest packet
			
			GamePacket * packet = pc->m_outgoing_packet_pool->GetUsedTail();
			
			#ifdef USE_DEBUG_CONSOLE
			
				// In debug mode only send a packet when it's time to send is ok
				
				if ( packet->GetSendTime() > GetTickCount() )
				{
					// Move onto the previous packet
					
					packet = (GamePacket*) packet->GetPoolablePrev();
				
					// Not time to send this packet: move back onto the next one
					
					while ( packet )
					{
						// See if it is time to send this packet:
						
						if ( packet->GetSendTime() <= GetTickCount() )
						{
							// Time to send this packet:
							
							break;
						}
						
						// Otherwise move onto the previous packet
						
						packet = (GamePacket*) packet->GetPoolablePrev();
					}
				}
				
				// Only if there is a packet to send:
				
				if ( packet )
				{
					// Take one packet from the pool:
				
					outgoing_packet = * packet;
					
					// Remove it from the pool:
					
					pc->m_outgoing_packet_pool->Dealloc(packet);
					
					// Compute the checksum for the packet:
					
					outgoing_packet.SetChecksum( outgoing_packet.ComputeChecksum() );
					
					// Do the sending:
					
					outgoing_packet.WriteToSocket(pc->m_socket);
					
					// Release the lock: we have sent
					
					pc->m_outgoing_lock.Unlock();
				}
				else
				{
					// Release the lock: done
					
					pc->m_outgoing_lock.Unlock();
				}							
			
			#else
			
				// Take one packet from the pool:
			
				outgoing_packet = * packet;
				
				// Remove it from the pool:
				
				pc->m_outgoing_packet_pool->Dealloc(packet);
				
				// Compute the checksum for the packet:
				
				outgoing_packet.SetChecksum( outgoing_packet.ComputeChecksum() );				
				
				// Do the sending:
				
				outgoing_packet.WriteToSocket(pc->m_socket);				
				
				// Release the lock: we have sent
				
				pc->m_outgoing_lock.Unlock();						

			#endif
		}
		else
		{
			// Nothing there: set the outgoing packets condition to false. Only if we are not exiting however:
			
			if ( pc->m_exit_io_threads == false )
			{			
				pc->m_outgoing_packets_condition.SetFalse();
			}
			
			// Release the outgoing packets lock:
			
			pc->m_outgoing_lock.Unlock();
			
			// Wait until there are more packets to send. No point in doing otherwise.
			
			if ( pc->m_exit_io_threads == false )
			{			
				pc->m_outgoing_packets_condition.WaitUntilTrue();
			}
		}
	}
}

//=====================================================================================================================
/// @brief		Gets the current number of outgoing packets
/// @remarks	This may decrease immediately after the function is called, due to the multi-threaded nature of 
///				the network IO.
//=====================================================================================================================

unsigned GamePacketCenter::GetOutgoingCount()
{
	// Lock the outgoing packets pool:
	
	m_outgoing_lock.Lock();
	
	// Get the count:
	
	unsigned count = m_outgoing_packet_pool->GetUsedCount();
	
	// Done:
	
	m_outgoing_lock.Unlock();
	
	// Return the count:
	
	return count;
}

//=====================================================================================================================
/// @brief		Gets the current number of incoming packets
/// @remarks	This may increase after the function is called, but will never decrease. Therefore it is always safe
///				to assume there is a packet to read if this function returns something other than 0.
//=====================================================================================================================

unsigned GamePacketCenter::GetIncomingCount()
{
	// Lock the incoming packets pool:
	
	m_incoming_lock.Lock();
	
	// Get the count:
	
	unsigned count = m_incoming_packet_pool->GetUsedCount();
	
	// Done:
	
	m_incoming_lock.Unlock();
	
	// Return the count:
	
	return count;
}

//=====================================================================================================================
/// @brief		Writes one packet to the network
/// @param		packet - packet to write
/// @pre		The game must exist
/// @remarks	This function will not write packets that have an invalid address.
//=====================================================================================================================

void GamePacketCenter::WritePacket( const GamePacket & packet )
{
	// Make sure ok:
	
	ENSURE( GameCore::GetSingleton() , "The game must exist" )
	
	// Don't bother sending to an invalid address
	
	if ( packet.GetAddress().IsValid() == false ) return;
	
	// Set the amount of time since the last packet was sent
	
	m_last_outgoing_time = GetTickCount();

	// Lock the outgoing packet list:
	
	m_outgoing_lock.Lock();
	
	// Alloc a packet from the outgoing packets list:
	
	GamePacket * outgoing = m_outgoing_packet_pool->Alloc();
	
	// Copy the data into it:
	
	* outgoing = packet;
	
	// Set the packet number for the packet:
	
	outgoing->SetPacketNumber(m_next_outgoing_packet_num);
	
	// Increment the number of the next packet to be sent
	
	m_next_outgoing_packet_num++;
	
	// Debug only: do latency and jitter
	
	#ifdef USE_DEBUG_CONSOLE
	
		// Get current time:
		
		unsigned time = GetTickCount();
		
		// Add to it by latency:
		
		time += s_fake_lag_ms;
		
		// Pick random jitter value:
		
		float j = GameCore::GetSingleton()->GetRandom().NextFloat();
		
		// Make jitter spike up and down sharply:
		
		j = Float::Pow(j,0.125f); j = 1.0f - j;
		
		// Add to lag by jitter
		
		time += (unsigned)( j * s_fake_jitter_ms );
		
		// Set the time to send at:
		
		outgoing->SetSendTime(time);
	
	#endif
	
	// Debug only: update network stats
	
	#ifdef USE_DEBUG_CONSOLE
	
	// Only if we are doing stats:
	
	if ( s_show_stats )
	{		
		// See if time to print stats:
		
		if ( GetTickCount() - s_stats_interval_begin > STAT_INTERVAL_MS )
		{
			// Print the results:
			
			DebugConsole::Print("\n\n#Net stats over ");			
			DebugConsole::Print(int(STAT_INTERVAL_MS));
			DebugConsole::Print("MS: \n\n");
			
			DebugConsole::Print("\nOutgoing packets		= "); DebugConsole::Print(s_stats_outgoing_packets);
			DebugConsole::Print("\nIncoming packets		= "); DebugConsole::Print(s_stats_incoming_packets);
			DebugConsole::Print("\nOutgoing bytes		= "); DebugConsole::Print(s_stats_outgoing_bytes);
			DebugConsole::Print("\nIncoming bytes		= "); DebugConsole::Print(s_stats_incoming_bytes);
						
			// Reset counts:
			
			s_stats_incoming_packets	= 0;
			s_stats_incoming_bytes		= 0;
			s_stats_outgoing_packets	= 0;
			s_stats_outgoing_bytes		= 0;
			
			// Reset time:
			
			s_stats_interval_begin = GetTickCount();
		}
	
		// Update outgoing stats
	
		s_stats_outgoing_packets++; s_stats_outgoing_bytes += packet.GetByteSize() + sizeof(GamePacket::Data::Header);
	}
		
	#endif
	
	// Set the outgoing packets condition to true
	
	m_outgoing_packets_condition.SetTrue();
	
	// All done:
	
	m_outgoing_lock.Unlock();
}

//=====================================================================================================================
/// @brief		Reads one packet from the network
/// @param		packet - where to save the read packet
/// @pre		There must be a packet to read first
/// @remarks	This returns the most recently read packet. Continue reading more packets
///				to read older packets.
//=====================================================================================================================

void GamePacketCenter::ReadPacket( GamePacket & packet )
{
	// Make sure ok:
	
	ENSURE( GetIncomingCount() > 0 , "There must be a packet to read first" )
	
	// Lock the incoming packet list:
	
	m_incoming_lock.Lock();
	
	// Get the tail item in the packet list:
	
	GamePacket * p = m_incoming_packet_pool->GetUsedTail();

	// Copy it:
	
	packet = * p;
	
	// Remove from the incoming packet list:
			
	m_incoming_packet_pool->Dealloc(p);
	
	// Done:
	
	m_incoming_lock.Unlock();
	
	// Debug only: update network stats
	
	#ifdef USE_DEBUG_CONSOLE
	
	// Only if we are doing stats:
	
	if ( s_show_stats )
	{
		// See if time to print stats:
		
		if ( GetTickCount() - s_stats_interval_begin > STAT_INTERVAL_MS )
		{
			// Print the results:
			
			DebugConsole::Print("\n\n#Net stats over ");			
			DebugConsole::Print(int(STAT_INTERVAL_MS));
			DebugConsole::Print("MS: \n\n");
			
			DebugConsole::Print("\nOutgoing packets		= "); DebugConsole::Print(s_stats_outgoing_packets);
			DebugConsole::Print("\nIncoming packets		= "); DebugConsole::Print(s_stats_incoming_packets);
			DebugConsole::Print("\nOutgoing bytes		= "); DebugConsole::Print(s_stats_outgoing_bytes);
			DebugConsole::Print("\nIncoming bytes		= "); DebugConsole::Print(s_stats_incoming_bytes);
						
			// Reset counts:
			
			s_stats_incoming_packets	= 0;
			s_stats_incoming_bytes		= 0;
			s_stats_outgoing_packets	= 0;
			s_stats_outgoing_bytes		= 0;
			
			// Reset time:
			
			s_stats_interval_begin = GetTickCount();
		}
	
		// Update incoming stats
	
		s_stats_incoming_packets++; s_stats_incoming_bytes += packet.GetByteSize() + sizeof(GamePacket::Data::Header);
	}
		
	#endif	
}

#ifdef USE_DEBUG_CONSOLE

//=====================================================================================================================
/// @brief		Console command. Enables/disables showing of network stats
/// @param		arg - argument
/// @param		arg_given - true if an argument is given
/// @returns	Value of this parameter
//=====================================================================================================================
					
bool GamePacketCenter::ConCmd_N_Stats( bool arg , bool arg_given )
{
	if ( arg_given )
	{
		// Only if change:
	
		s_show_stats = arg; 
		
		// Reset times:
		
		s_stats_incoming_packets	= 0;
		s_stats_incoming_bytes		= 0;
		s_stats_outgoing_packets	= 0;
		s_stats_outgoing_bytes		= 0;
		
		// Reset interval
		
		s_stats_interval_begin = GetTickCount();
	}	
		
	return s_show_stats;
}

#endif

#ifdef USE_DEBUG_CONSOLE

//=====================================================================================================================
/// @brief		Console command. Sets the amount of constant jitter to add to packets.
/// @param		arg - argument
/// @param		arg_given - true if an argument is given
/// @returns	Value of this parameter
//=====================================================================================================================

int GamePacketCenter::ConCmd_N_FakeJitter( int arg , bool arg_given )
{
	// See if tiven:
	
	if ( arg_given )
	{
		// Make sure not less than zero
		
		if ( arg < 0 ) arg = 0;
		
		// Set:
		
		s_fake_jitter_ms = arg;
	}
	
	// Return:
	
	return s_fake_jitter_ms;
}

#endif

#ifdef USE_DEBUG_CONSOLE

//=====================================================================================================================
/// @brief		Console command. Sets the amount of constant lag to add to packets.
/// @param		arg - argument
/// @param		arg_given - true if an argument is given
/// @returns	Value of this parameter
//=====================================================================================================================

int GamePacketCenter::ConCmd_N_FakeLag( int arg , bool arg_given )
{
	// See if tiven:
	
	if ( arg_given )
	{
		// Make sure not less than zero
		
		if ( arg < 0 ) arg = 0;
		
		// Set:
		
		s_fake_lag_ms = arg;
	}
	
	// Return:
	
	return s_fake_lag_ms;
}

#endif