// Asura_PC_SteamworksServer.cpp
// Thomas Young - 13.01.09
// Copyright (C) 2009 Rebellion, All Rights Reserved.

// I N C L U D E S ///////////////////////////////////////////////////////////

#include "Core/Asura.h"

#ifdef ASURA_PC_SUPPORT_STEAMWORKS

#include "Core/Asura_List.h"
#include "Core/Asura_Timers.h"
#include "Core/Asura_Windows.h"
#include "Debugging/Asura_Logfile.h"
#include "Network/Asura_NetConnection.h"
#include "Network/Asura_NetTransport_System.h"
#include "Network/Asura_NetSocket.h"
#include "Network/Asura_Network.h"
#include "Network/Asura_Platform_NetSocket.h"
#include "Network/Steamworks/Asura_PC_NetSocketSteamworks.h"
#include "Network/Steamworks/Asura_PC_Steamworks.h"
#include "Network/Steamworks/Asura_PC_SteamworksClient.h"
#include "Network/Steamworks/Asura_PC_SteamworksMessages.h"
#include "Network/Steamworks/Asura_PC_SteamworksServer.h"
#include "Network/Online/Asura_Lobby.h"

// D E F I N E S /////////////////////////////////////////////////////////////

#define ASURA_PC_STEAMWORKS_CLIENT_AUTH_TIMEOUT			10.0f

#define ASURA_PC_STEAMWORKS_SERVER_VERSION				"1"

// C L A S S   D E C L A R A T I O N S ///////////////////////////////////////

// _______________________ Asura_PC_Steamworks_ClientInfo ___________________________

class Asura_PC_Steamworks_ClientInfo
{
	// This class holds information about connected/authorising clients
	public:

		Asura_NetAddress	m_xAddress;
		unsigned short		m_usPort;

		bool				m_bPendingAuth;
		float				m_fConnectionTime;
};

// S T A T I C   M E M B E R   V A R I A B L E S /////////////////////////////

const char*												Asura_PC_SteamworksServer::s_szGameName						= 0;
bool													Asura_PC_SteamworksServer::s_bServerRunning					= false;

bool													Asura_PC_SteamworksServer::s_bSteamConnected				= false;
ASURA_PC_STEAMWORKS_AUTH_LEVEL							Asura_PC_SteamworksServer::s_eAuthLevel						= ASURA_PC_STEAMWORKS_NO_AUTHENTICATE;

Asura_List<Asura_PC_Steamworks_ClientInfo>				Asura_PC_SteamworksServer::s_xClientList;

Asura_PC_Steam_Callback< SteamServersConnected_t >		Asura_PC_SteamworksServer::s_xCallback_SteamConnected		( Asura_PC_SteamworksServer::Callback_SteamConnected );
Asura_PC_Steam_Callback< SteamServersDisconnected_t >	Asura_PC_SteamworksServer::s_xCallback_SteamDisconnected	( Asura_PC_SteamworksServer::Callback_SteamDisconnected );
Asura_PC_Steam_Callback< GSClientApprove_t >			Asura_PC_SteamworksServer::s_xCallback_ClientApprove		( Asura_PC_SteamworksServer::Callback_ClientApprove );
Asura_PC_Steam_Callback< GSClientDeny_t >				Asura_PC_SteamworksServer::s_xCallback_ClientDeny			( Asura_PC_SteamworksServer::Callback_ClientDeny );
Asura_PC_Steam_Callback< GSPolicyResponse_t	>			Asura_PC_SteamworksServer::s_xCallback_PolicyResponse		( Asura_PC_SteamworksServer::Callback_PolicyResponse );

Asura_PC_SteamServer_Callback_ClientAccept				Asura_PC_SteamworksServer::s_pfnAsuraCallback_ClientAccept;
Asura_PC_SteamServer_Callback_ClientReject				Asura_PC_SteamworksServer::s_pfnAsuraCallback_ClientReject;

unsigned short											Asura_PC_SteamworksServer::s_usGamePort						= ASURA_PC_STEAMWORKS_GAME_PORT;

// F U N C T I O N   D E F I N I T I O N S ///////////////////////////////////

void Asura_PC_SteamworksServer::Initialise ( )
{
	if( !Asura_PC_Steamworks::IsInitialised() )
	{
		Asura_Assert( false, "Steamworks not initialised" );
		return;
	}

	s_xCallback_SteamConnected.Register		( true );
	s_xCallback_SteamDisconnected.Register	( true );
	s_xCallback_ClientApprove.Register		( true );
	s_xCallback_ClientDeny.Register			( true );
	s_xCallback_PolicyResponse.Register		( true );

	s_pfnAsuraCallback_ClientAccept = 0;
	s_pfnAsuraCallback_ClientReject = 0;

	s_szGameName = ASURA_PC_APPLICATION_NAME;
}


void Asura_PC_SteamworksServer::Create( const Asura_PC_Steamworks_ServerConfig* pxConfig )
{
	if( !Asura_PC_Steamworks::IsInitialised() )
	{
		return;
	}

	if( !pxConfig )
	{
		Asura_Assert( false, "No server config provided" );
		return;
	}

	EServerMode eSteamAuth = eServerModeInvalid;
	s_eAuthLevel = pxConfig->m_eAuthLevel;
	switch( s_eAuthLevel )
	{
		case ASURA_PC_STEAMWORKS_NO_AUTHENTICATE:		eSteamAuth = eServerModeNoAuthentication;			break;
		case ASURA_PC_STEAMWORKS_AUTHENTICATE:			eSteamAuth = eServerModeAuthentication;				break;
		case ASURA_PC_STEAMWORKS_AUTHENTICATE_AND_VAC:	eSteamAuth = eServerModeAuthenticationAndSecure;	break;
	}
		
	if( !SteamGameServer_Init( INADDR_ANY, ASURA_PC_STEAMWORKS_AUTH_PORT, s_usGamePort, 0, ASURA_PC_STEAMWORKS_UPDATER_PORT, eSteamAuth, /*s_szGameName*/"spacewar",  ASURA_PC_STEAMWORKS_SERVER_VERSION ) )
	{
		Asura_Assert( false, "Failed to initialise steam game server" );
		return;
	}

	s_bServerRunning = true;

	// Update the master server so people can find us
	if ( SteamMasterServerUpdater() )
	{

		SteamMasterServerUpdater()->SetActive( true );

		// Tell the master server about our game
		SteamMasterServerUpdater()->SetBasicServerData( 7,								// Protocol version, use the magic number 7
														true,							// dedicated server? TODO: the examples sets this to true ?
														"255",							// Region name unused, use the magic value "255"
														s_szGameName,					// Our game name
														pxConfig->m_usMaxPlayers,		// Maximum players
														false,							// passworded?
														pxConfig->m_szDescription	);	// Description

		char szText[256];

		Asura_PrintfToBfr( szText, "%s's game", 256, true, Asura_PC_SteamworksClient::GetSteamIDText() );

		// Tell the steam auth servers about our game
		SteamGameServer()->UpdateServerStatus(	0,												// Current player count
												static_cast<int>( pxConfig->m_usMaxPlayers),	// Maximum number of players
												0,												// Number of AI/Bot players
												pxConfig->m_szServerName,						// Server name
												"",												// Spectator server name
												pxConfig->m_szMapName	);						// Map name
	}
}


void Asura_PC_SteamworksServer::AddServerProperty( const char* szKey, const char* szValue )
{
	Asura_Assert( s_bServerRunning, "You should start the server before setting its properties" );
	if ( SteamMasterServerUpdater() )
	{
		SteamMasterServerUpdater()->SetKeyValue( szKey, szValue);
	}
}


void Asura_PC_SteamworksServer::Update ()
{
	SteamGameServer_RunCallbacks();
	
	// Check if we are waiting for any clients to authorise and they have timed out
	Asura_List_It< Asura_PC_Steamworks_ClientInfo > xIter( s_xClientList );
	for( ; !xIter.Done(); xIter.Next() )
	{
		Asura_PC_Steamworks_ClientInfo* pxClient = xIter.GetCurrent();
		if(		pxClient 
			&&	pxClient->m_bPendingAuth
			&&	( ( Asura_Timers::GetLocalUnscaledTime() - pxClient->m_fConnectionTime ) > ASURA_PC_STEAMWORKS_CLIENT_AUTH_TIMEOUT ) )
		{
			// This authorisation attempt has timed out, reject the connection and delete the client
			RunCallback_RejectClient( pxClient->m_xAddress.m_uAddress, pxClient->m_usPort, ASURA_NDR_TIMEOUT );
			
			s_xClientList.RemoveFromList( pxClient );
			delete pxClient;
			
			break;
		}
	}
}


void Asura_PC_SteamworksServer::Terminate ()
{
	if( !IsServerRunning() )
	{
		return;
	}

	if( !Asura_Network::IsDedicatedServer() )
	{
		// Disconnect our local client from the local server
		Asura_PC_SteamworksClient::TerminateServerConnection();
	}

	while( !s_xClientList.IsEmpty() )
	{
		// Send user disconnections for all our clients
		Asura_PC_Steamworks_ClientInfo* pxClient = s_xClientList.PopHeadObject();
		SteamGameServer()->SendUserDisconnect( pxClient->m_xAddress.m_xSteamID );
		delete pxClient;
	}

	// Notify Steam master server we are going offline
	if( SteamMasterServerUpdater() )
	{
		SteamMasterServerUpdater()->SetActive( false );
	}
	
	s_bServerRunning = false;

	if( SteamGameServer() )
	{
		SteamGameServer()->LogOff();
		SteamGameServer_Shutdown();
	}
}


void Asura_PC_SteamworksServer::Shutdown ()
{
	if( s_bServerRunning )
	{
		Terminate();
	}

	s_xCallback_SteamConnected.Unregister();
	s_xCallback_SteamDisconnected.Unregister();
	s_xCallback_ClientApprove.Unregister();
	s_xCallback_ClientDeny.Unregister();
	s_xCallback_PolicyResponse.Unregister();
}


void Asura_PC_SteamworksServer::Callback_SteamDisconnected	( const SteamServersDisconnected_t* const pxParam )
{
	s_bSteamConnected = false;
}


void Asura_PC_SteamworksServer::Callback_SteamConnected ( const SteamServersConnected_t* const pxParam )
{
	s_bSteamConnected = true;

	// TODO: We used to establish a local connection here, we need to move this somewhere else
	/*if( !Asura_Network::IsDedicatedServer() )
	{
		// Establish a local connection to this server and authenticate ourselves
		Asura_PC_SteamworksClient::InitiateServerConnection( SteamGameServer()->GetSteamID(), s_eAuthLevel, Asura_NetAddress( Asura_PC_Steamworks::GetLoopbackAddress() ), ASURA_PC_STEAMWORKS_GAME_PORT );
		
		void* pAuthBlob = 0;
		u_int uBlobSize = 0;

		if( Asura_PC_SteamworksClient::AcquireAuthBlob( pAuthBlob, uBlobSize ) )
		{
			HandleUserConnection( uLOCALHOST, ASURA_PC_STEAMWORKS_GAME_PORT, pAuthBlob, uBlobSize );
			Asura_PC_SteamworksClient::ReleaseAuthBlob();
		}
		#ifdef ASURA_ASSERT
		else
		{
			Asura_Assert( false, "Failed to get steam auth blob" );
		}
		#endif
	}*/
}


CSteamID Asura_PC_SteamworksServer::GetSteamID ( )
{
	if( SteamGameServer() )
	{
		return SteamGameServer()->GetSteamID();
	}
	return CSteamID();
}


uint64	Asura_PC_SteamworksServer::GetSteamID_uint64 ( )
{
	if( SteamGameServer() )
	{
		return SteamGameServer()->GetSteamID().ConvertToUint64();
	}
	return CSteamID().ConvertToUint64();
}


void Asura_PC_SteamworksServer::HandleUserConnection ( const u_int uIP, const unsigned short usPort, const void* pAuthBlob, const u_int uAuthBlobSize )
{
	if( !IsServerRunning() || !SteamGameServer() )
	{
		Asura_Assert( false, "Calling HandleUserConnection but no server is running" );
		return;
	}

	Asura_NetAddress xAddress;
	xAddress.m_uAddress = uIP;
	
	CSteamID xSteamID;	// This will be filled out with the steam id of the client

	if( GetAuthLevel() > ASURA_PC_STEAMWORKS_NO_AUTHENTICATE )
	{
		const bool bBasicCheckIsOK = SteamGameServer()->SendUserConnectAndAuthenticate( uIP, pAuthBlob, uAuthBlobSize, &xSteamID );
		if( !bBasicCheckIsOK )
		{
			// Not sure what reason to give here
			RunCallback_RejectClient( uIP, usPort, static_cast<u_int>( ASURA_NDR_TIMEOUT ) );
			return;
		}
	}
	else
	{
		RunCallback_AcceptClient( uIP, usPort );
	}
	
	// Record this client in our list

	for( u_int uClient = 0; uClient < static_cast<u_int>( s_xClientList.GetNumEntries() ); ++uClient )
	{
		Asura_PC_Steamworks_ClientInfo* pxClient = s_xClientList.GetEntryByIndex( uClient );

		if( pxClient->m_xAddress == xAddress )
		{
			// we have this client already
			return;
		}
	}

	Asura_PC_Steamworks_ClientInfo* pxClient = new Asura_PC_Steamworks_ClientInfo;

	pxClient->m_xAddress					= xAddress;
	pxClient->m_xAddress.m_xSteamID			= xSteamID;
	pxClient->m_usPort						= usPort;
	pxClient->m_bPendingAuth				= bool( GetAuthLevel() > ASURA_PC_STEAMWORKS_NO_AUTHENTICATE );
	pxClient->m_fConnectionTime				= Asura_Timers::GetLocalUnscaledTime();

	s_xClientList.AddToList( pxClient );

	ResendClientList();	// Update all our clients with a list of clients
}


void Asura_PC_SteamworksServer::HandleUserDisconnection ( const Asura_NetAddress xAddress, const unsigned short usPort )
{
	Asura_List_It< Asura_PC_Steamworks_ClientInfo > xIter( s_xClientList );
	for( ; !xIter.Done(); xIter.Next() )
	{
		Asura_PC_Steamworks_ClientInfo* pxClient = xIter.GetCurrent();
		if( pxClient && ( pxClient->m_xAddress == xAddress ) )
		{
			SteamGameServer()->SendUserDisconnect( pxClient->m_xAddress.m_xSteamID );
			s_xClientList.RemoveFromList( pxClient );
			delete pxClient;
			
			// Send the update client list to all clients
			ResendClientList();
			
			return;
		}
	}
}


void Asura_PC_SteamworksServer::Callback_ClientApprove ( const GSClientApprove_t* const pxParam )
{
	Asura_List_It< Asura_PC_Steamworks_ClientInfo > xIter( s_xClientList );
	for( ; !xIter.Done(); xIter.Next() )
	{
		Asura_PC_Steamworks_ClientInfo* pxClient = xIter.GetCurrent();
		if( pxClient && ( pxClient->m_xAddress.m_xSteamID == pxParam->m_SteamID ) )
		{
			RunCallback_AcceptClient( pxClient->m_xAddress.m_uAddress, pxClient->m_usPort );
			
			pxClient->m_bPendingAuth = false;

			return;
		}
	}
	Asura_Assert( false, "Failed to find client for authorisation approval" );
}


void Asura_PC_SteamworksServer::Callback_ClientDeny ( const GSClientDeny_t* const pxParam )
{
	Asura_List_It< Asura_PC_Steamworks_ClientInfo > xIter( s_xClientList );
	for( ; !xIter.Done(); xIter.Next() )
	{
		Asura_PC_Steamworks_ClientInfo* pxClient = xIter.GetCurrent();
		if( pxClient && ( pxClient->m_xAddress.m_xSteamID == pxParam->m_SteamID ) )
		{
			u_int uRejectReason = ASURA_NDR_UNKNOWN;

			switch ( pxParam->m_eDenyReason )
			{
				case k_EDenyInvalid:
				case k_EDenyUnknownText:
				case k_EDenyMemoryCorruption:
				{
					uRejectReason = ASURA_NDR_UNKNOWN;
				}
				break;

				case k_EDenySteamOwnerLeftGuestUser:
				case k_EDenyLoggedInElseWhere:
				case k_EDenyNotLoggedOn:
				{
					uRejectReason = ASURA_NDR_PROFILE_LOGGED_OUT;
				}
				break;

				case k_EDenyGeneric:
				case k_EDenyNoLicense:
				case k_EDenyIncompatibleAnticheat:
				{
					uRejectReason = ASURA_NDR_CDKEY;
				}
				break;

				case k_EDenyInvalidVersion:
				case k_EDenyIncompatibleSoftware:
				{
					uRejectReason = ASURA_NDR_VERSION;
				}
				break;

				case k_EDenyCheater:
				{
					uRejectReason = ASURA_NDR_CHEATING;
				}
				break;
				
				case k_EDenySteamResponseTimedOut:
				case k_EDenySteamValidationStalled:
				{
					uRejectReason = ASURA_NDR_TIMEOUT;
				}
				break;

				case k_EDenySteamConnectionLost:
				case k_EDenySteamConnectionError:
				{
					uRejectReason = ASURA_NDR_CONNECTION_DROPPED;
				}
				break;
			}

			RunCallback_RejectClient( pxClient->m_xAddress.m_uAddress, pxClient->m_usPort, uRejectReason );

			// delete this client
			s_xClientList.RemoveFromList( pxClient );
			delete pxClient;
			
			return;
		}
	}
	Asura_Assert( false, "Failed to find client for authorisation denial" );
}


void Asura_PC_SteamworksServer::Callback_PolicyResponse ( const GSPolicyResponse_t* const pxParam )
{
	Asura_OutputToDebugger( ( "Steam server is%s secure\n", pxParam->m_bSecure ? "" : " not" ) );
}


void Asura_PC_SteamworksServer::ResendClientList ( )
{
	// Send a message to each client which contains a list of every client
	// this is done so that each client can update their P2P voice connections
	#if 0
	// Build the message
	unsigned short usMsgSize = static_cast< unsigned short >( Asura_PC_SteamMsg_SendHdr::Size() + Asura_PC_SteamMsg_ClientListHdr::Size() + s_xClientList.GetNumEntries() * Asura_PC_SteamMsg_ClientEntry::Size() );
	char* pcMsgData = static_cast<char*>( Asura_Allocate( usMsgSize ) );

	if( !pcMsgData )
	{
		return;
	}

	Asura_PC_SteamMsg_SendHdr* pxMsgHdr = reinterpret_cast<Asura_PC_SteamMsg_SendHdr*>( pcMsgData );

	pxMsgHdr->m_usSequence	= 0;
	pxMsgHdr->m_usChecksum	= 0;
	pxMsgHdr->m_usMsgID		= ASURA_NETTRANSPORT_MSG_CLIENTLIST;
	pxMsgHdr->m_usMsgSize	= usMsgSize;

	Asura_PC_SteamMsg_ClientListHdr* pxClientListHdr = reinterpret_cast< Asura_PC_SteamMsg_ClientListHdr* >( pcMsgData + Asura_PC_SteamMsg_SendHdr::Size() );

	pxClientListHdr->m_uNumClients = static_cast< unsigned short >( s_xClientList.GetNumEntries() );

	Asura_PC_SteamMsg_ClientEntry* pxClientList = &pxClientListHdr->m_xClients;

	for( u_int uClient = 0; uClient < pxClientListHdr->m_uNumClients; ++uClient )
	{
		Asura_PC_Steamworks_ClientInfo* pxClient = s_xClientList.GetEntryByIndex( uClient );
		
		pxClientList[uClient].m_usPort			= pxClient->m_usPort;
		pxClientList[uClient].m_xAddress		= pxClient->m_xAddress;
		pxClientList[uClient].m_bIsClientSentTo	= false;
		pxClientList[uClient].m_bIsServer		= false;

		// Remember if this client is us, since we wont want to do a 'propper' send
		if( pxClientList[uClient].m_xAddress.m_xSteamID == Asura_PC_SteamworksClient::GetSteamID( ) )
		{
			pxClientList[uClient].m_bIsServer = true;
		}
	}

	//Send the message to each client
	// TODO: Fix up this voice stuff
	/*for( u_int uClient = 0; uClient < pxClientListHdr->m_uNumClients; ++uClient )
	{
		// Update the flag which says if this entry is the recipient
		if( uClient != 0 )
		{
			pxClientList[uClient - 1].m_bIsClientSentTo	= false;
		}
		pxClientList[uClient].m_bIsClientSentTo = true;


		if( pxClientList[uClient].m_bIsServer )
		{
			// This one is for ourselves, dont sent over the network
			if( !Asura_Network::IsDedicatedServer() )
			{
				Asura_PC_SteamworksClient::HandleMessage( pcMsgData + 4, usMsgSize - 4 );
			}
		}
		else
		{
			// update the checksum
			pxMsgHdr->m_usChecksum = Asura_NetTransport_System::ComputeChecksum( pcMsgData + 4, usMsgSize - 4 );

			Asura_NetSocket* pxSocket = Asura_NetTransport_System::GetSocket();

			Asura_PC_NetSocketSteamworks* pxSteamSocket = reinterpret_cast< Asura_PC_NetSocketSteamworks* >( pxSocket );
			pxSteamSocket->UseReliableSend( true );

			#ifdef ASURA_ASSERT
			const bool bSentOK =
			#endif
			pxSocket->Send( pxClientList[uClient].m_xAddress, pxClientList[uClient].m_usPort, pcMsgData, usMsgSize, false);
			Asura_Assert( bSentOK, "Failed to send client list update" );

			pxSteamSocket->UseReliableSend( false );
		}
	}*/

	Asura_Deallocate( pcMsgData );
	#endif
}


void Asura_PC_SteamworksServer::RegisterCallback_ClientAccept ( Asura_PC_SteamServer_Callback_ClientAccept pfnCallback )
{
	s_pfnAsuraCallback_ClientAccept = pfnCallback;
}


void Asura_PC_SteamworksServer::RegisterCallback_ClientReject ( Asura_PC_SteamServer_Callback_ClientReject pfnCallback )
{
	s_pfnAsuraCallback_ClientReject = pfnCallback;
}


void Asura_PC_SteamworksServer::RunCallback_AcceptClient ( const u_int uIP, const unsigned short usPort )
{
	if( s_pfnAsuraCallback_ClientAccept )
	{
		s_pfnAsuraCallback_ClientAccept( uIP, usPort );
	}
}


void Asura_PC_SteamworksServer::RunCallback_RejectClient ( const u_int uIP, const unsigned short usPort, const u_int uReason )
{
	if( s_pfnAsuraCallback_ClientReject )
	{
		s_pfnAsuraCallback_ClientReject( uIP, usPort, uReason );
	}
}


void Asura_PC_SteamworksServer::SetGamePort ( const unsigned short usPort )
{
	if( IsServerRunning() )
	{
		Asura_Assert( false, "You cannot set the steam server game port while the server is running" );
		return;
	}

	s_usGamePort = usPort;
}


unsigned short Asura_PC_SteamworksServer::GetGamePort ( )
{
	return s_usGamePort;
}


u_int Asura_PC_SteamworksServer::GetPublicIP ( )
{
	if( IsServerRunning() && SteamGameServer() )
	{
		return SteamGameServer()->GetPublicIP();
	}

	return 0;
}


#endif //ASURA_PC_SUPPORT_STEAMWORKS

//eof
