//////////////////////////////////////////////////////////////////////////////
//
// Asura_ReliableSocket.cpp
//
// Copyright (c) 2009 Rebellion. All rights reserved.
//
//////////////////////////////////////////////////////////////////////////////

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

#include "Core/Asura.h"

#ifdef ASURA_SUPPORT_EXTERNAL_NETWORK

// This file's header.
#include "Network/Asura_ReliableSocket.h"

// Asura includes
#include "Core/Asura_Timers.h"
#include "Network/Asura_ReliableSocket_TCP.h"
#ifdef ASURA_PC_SUPPORT_STEAMWORKS
#include "Network/Steamworks/Asura_PC_SteamworksReliableSocket.h"
#include "Network/Steamworks/Asura_PC_Steamworks.h"
#endif

// F U N C T I O N    D E F I N I T I O N S //////////////////////////////////
static void DoUnitTest( Asura_ReliableSocket_Client& xClientConnection, Asura_ReliableSocket_Listener& xListener );

// F U N C T I O N    I M P L E M E N T A T I O N ////////////////////////////
#ifdef _DEBUG
void Asura_ReliableSocket_UnitTest()
{
	//The unit test works for steamworks, but currently contains
	//calls to DelayThread() and slows down entrance to the lobby
	//too significantly. TODO: Enable for steamworks when delays
	//are removed.
#ifdef ASURA_PC_SUPPORT_STEAMWORKS
	if (Asura_PC_Steamworks::IsSteamworksEnabled()) return;
#endif

	Asura_ReliableSocket_Client* pxClientSocket = Asura_ReliableSocket_Client::Create();
	Asura_ReliableSocket_Listener* pxListenerSocket = Asura_ReliableSocket_Listener::Create();
	Asura_Assert(pxClientSocket && pxListenerSocket,"Unit test failed: could not create socket(s)");
	if (pxClientSocket && pxListenerSocket)
	{
		DoUnitTest( *pxClientSocket, *pxListenerSocket );
	}
	delete pxClientSocket;
	delete pxListenerSocket;
}
#endif

// C L A S S   I M P L E M E N T A T I O N ///////////////////////////////////

Asura_NetAddress Asura_ReliableSocket_Client::GetLoopbackAddress()
{
#ifdef ASURA_PC_SUPPORT_STEAMWORKS
		if ( Asura_PC_Steamworks::IsSteamworksEnabled() )
		{
			return Asura_PC_Steamworks::GetLoopbackAddress();
		}
		else
#endif
		{
			return Asura_NetAddress( inet_addr("127.0.0.1") );
		}
}

Asura_ReliableSocket_Client* Asura_ReliableSocket_Client::Create()
{
#ifdef ASURA_PC_SUPPORT_STEAMWORKS
	if ( Asura_PC_Steamworks::IsSteamworksEnabled() )
	{
		return new Asura_SteamReliableSocket_Client;
	}
	else
#endif
	{
		return new Asura_ReliableSocket_Client_TCP;
	}
}

Asura_ReliableSocket_Listener* Asura_ReliableSocket_Listener::Create()
{
#ifdef ASURA_PC_SUPPORT_STEAMWORKS
	if ( Asura_PC_Steamworks::IsSteamworksEnabled() )
	{
		Asura_SteamReliableSocket_Listener* pxListener = new Asura_SteamReliableSocket_Listener();
		if( pxListener )
		{
			// Do the authentication of clients on this socket, NOTE: we should only set this on
			// one listener socket. If this function is used to create more than one socket this needs
			// to be changed to selectively enable authentication
			pxListener->EnableAuthentication();
		}
		return pxListener;
	}
	else
#endif
	{
		return new Asura_ReliableSocket_Listener_TCP;
	}
}

/// Unit test /////////////////////////////////////////////////////////////////////////////////////

#ifdef _DEBUG

DEBUGVAR Asura_NetPort dbg_xRS_UnitTest_Port( 0 );
const char* szLOOPBACK_ADDRESS = "127.0.0.1";
const u_int uRS_UNITTEST_BUFFER_SIZE = 1024;
const u_int uRS_UNITTEST_NUM_MESSAGES = 8;
const u_short ausTCP_UNITTEST_MESSAGE_SIZE[uRS_UNITTEST_NUM_MESSAGES] = 
{ 
	uRS_UNITTEST_BUFFER_SIZE, uRS_UNITTEST_BUFFER_SIZE / 2, uRS_UNITTEST_BUFFER_SIZE, uRS_UNITTEST_BUFFER_SIZE / 3, 
	1, (uRS_UNITTEST_BUFFER_SIZE / 2) - 1, uRS_UNITTEST_BUFFER_SIZE / 2, uRS_UNITTEST_BUFFER_SIZE / 2 
};
DEBUGVAR float dbg_fTimeoutSeconds = 10.f;
DEBUGVAR u_short dbg_usUnittestMessageID = 0xD0D0;
DEBUGVAR u_int dbg_uVersionNumber = 0xFADEDBEE;

enum ASURA_TCP_UNIT_TEST_STATE
{
	TEST_STATE_VOID,
	TEST_STATE_CONNECTING_1,
	TEST_STATE_SENDING_FROM_CLIENT_TO_SERVER,
	TEST_STATE_SENDING_FROM_SERVER_TO_CLIENT,
	TEST_STATE_SENDING_FROM_SERVER_TO_CLIENT_MULTIPLE,
	TEST_STATE_BROADCASTING,
	TEST_STATE_CLOSING_FROM_SERVER,
	TEST_STATE_CONNECTING_2,
	TEST_STATE_CLOSING_FROM_CLIENT,
	TEST_STATE_CONNECTING_3,
	TEST_STATE_CONNECTING_INVALID,
	TEST_STATE_FINISHED,
};

static ASURA_TCP_UNIT_TEST_STATE g_eTestState = TEST_STATE_VOID;
static float g_fStateChangeTime = 0.f;
static char g_acRandomBuffer[uRS_UNITTEST_BUFFER_SIZE];
static u_int g_uNumMessagesReceived = 0;
static Asura_ReliableConnectionID g_xConnectionID;
static bool g_bConnectionRejected = false;

void NextTestState()
{
	g_eTestState = static_cast<ASURA_TCP_UNIT_TEST_STATE>(g_eTestState + 1);
	g_fStateChangeTime = Asura_Timers::GetLocalUnscaledTime();
}

static void UnitTestHandleMessage(const Asura_ReliableConnectionID& xID, u_short usMsgID, u_short usMsgSize, const char* pcMsgBody)
{
	Asura_Assert(dbg_usUnittestMessageID == usMsgID, "Unexpected message ID");
	Asura_Assert(g_uNumMessagesReceived < uRS_UNITTEST_NUM_MESSAGES, "broken unit test");
	const u_int uBufferSize = ausTCP_UNITTEST_MESSAGE_SIZE[g_uNumMessagesReceived];
	Asura_Assert(usMsgSize == uBufferSize, "Unexpected message size");
	for(u_int u = 0; u < uBufferSize; ++u)
	{
		Asura_Assert(g_acRandomBuffer[u] == pcMsgBody[u], "Data has changed in transmission");
	}
	++g_uNumMessagesReceived;
}

static void UnitTestHandleMessageClient(u_short usMsgID, u_short usMsgSize, const char* pcMsgBody)
{
	Asura_Assert(dbg_usUnittestMessageID == usMsgID, "Unexpected message ID");
	Asura_Assert(g_uNumMessagesReceived < uRS_UNITTEST_NUM_MESSAGES, "broken unit test");
	const u_int uBufferSize = ausTCP_UNITTEST_MESSAGE_SIZE[g_uNumMessagesReceived];
	Asura_Assert(usMsgSize == uBufferSize, "Unexpected message size");
	for(u_int u = 0; u < uBufferSize; ++u)
	{
		Asura_Assert(g_acRandomBuffer[u] == pcMsgBody[u], "Data has changed in transmission");
	}
	++g_uNumMessagesReceived;
}

struct UnitTestConnectMessage
{
	u_int m_uVersionNumber;
};

static bool UnitTestConnectCallback(const Asura_ReliableConnectionID& xID, u_short usConnectDataSize, const char* pcConnectData)
{
	Asura_Assert(usConnectDataSize == sizeof(UnitTestConnectMessage), "Unexpected message size");
	const UnitTestConnectMessage* pxMessage = reinterpret_cast<const UnitTestConnectMessage*>(pcConnectData);

	if(dbg_uVersionNumber == pxMessage->m_uVersionNumber)
	{
		Asura_Assert(!g_xConnectionID.IsValid() && xID.IsValid(), "unexpected connection ID");
		g_xConnectionID = xID;
		g_bConnectionRejected = false;
		return true;
	}
	else
	{
		g_bConnectionRejected = true;		
	}
	return false;
}

static void UnitTestDisconnectCallback(const Asura_ReliableConnectionID& xID, ASURA_RELIABLE_SOCKET_DISCONNECT_REASON eReason)
{
	if (g_xConnectionID == xID)
	{
		g_xConnectionID.Invalidate();
	}
}

static void TestConnect(Asura_ReliableSocket_Client& xConnection, u_int uVersionNumber, u_short usPort)
{
	// Create a connection socket using loopback address
	UnitTestConnectMessage xMessage;
	xMessage.m_uVersionNumber = uVersionNumber;
	Asura_NetPort xPort( usPort );
	xConnection.Connect(Asura_ReliableSocket_Client::GetLoopbackAddress(), xPort, &xMessage, sizeof(xMessage));
}

static void DoUnitTest( Asura_ReliableSocket_Client& xClientConnection, Asura_ReliableSocket_Listener& xListener )
{
	// By doing the test in a static function outside the class,
	// we ensure that we are not accessing any protected functionality

	// Create a listener socket.
	xListener.SetCallback_Connection( UnitTestConnectCallback );
	xListener.SetCallback_Disconnection( UnitTestDisconnectCallback );
	xListener.SetCallback_Message( UnitTestHandleMessage );
	Asura_Assert(!xListener.IsActive(), "IsActive() test failed");
	bool bListenOK = xListener.Listen(dbg_xRS_UnitTest_Port);
	if (!bListenOK)
	{
		return;
	}
	Asura_Assert(xListener.IsActive(), "IsActive() test failed");
	const u_short usPort = xListener.GetListenPort();

//	xClientConnection.SetCallback_Disconnection( UnitTestDisconnectCallback );
	xClientConnection.SetCallback_Message( UnitTestHandleMessageClient );

	// Generate a random buffer

	for(u_int u = 0; u < uRS_UNITTEST_BUFFER_SIZE; ++u)
	{
		//g_acRandomBuffer[u] = static_cast<char>(Asura_Random::GetInt(-127, 128));
		u_int uHex0 = u & 15;
		u_int uHex1 = (u >> 4) & 15;
		g_acRandomBuffer[u] = static_cast<u_char>(uHex0 | (uHex1 << 4));
	}

	g_eTestState = TEST_STATE_VOID;
	g_fStateChangeTime = Asura_Timers::GetLocalUnscaledTime();
	while(TEST_STATE_FINISHED != g_eTestState)
	{
#ifdef ASURA_PC_SUPPORT_STEAMWORKS
		if( Asura_PC_Steamworks::IsSteamworksEnabled() )
		{
			Asura_PC_Steamworks::Update();
		}
#endif

		Asura_Timers::HandlePrimaryTimer();
		const float fElapsedTime = Asura_Timers::GetLocalUnscaledTime() - g_fStateChangeTime;
		if(fElapsedTime > dbg_fTimeoutSeconds)
		{
			Asura_Assert(0, "Unit Test failed: timed out waiting for it to complete");
			break;
		}

		xListener.Update();
		if(xClientConnection.IsActive())
		{
			xClientConnection.Update();
		}
		switch(g_eTestState)
		{
		case TEST_STATE_VOID:
			{
				TestConnect(xClientConnection, dbg_uVersionNumber, usPort);
				NextTestState();
				break;
			}
		case TEST_STATE_CONNECTING_1:
			{
				if(xClientConnection.IsConnected() && xListener.GetNumConnections())
				{
					// Now try sending a message
					xClientConnection.SendMessage(dbg_usUnittestMessageID, uRS_UNITTEST_BUFFER_SIZE, g_acRandomBuffer);
					NextTestState();
				}
				break;
			}
		case TEST_STATE_SENDING_FROM_CLIENT_TO_SERVER:
			{
				if(g_uNumMessagesReceived == 1)
				{
					g_uNumMessagesReceived = 0;
					// send from server to client
					Asura_Assert(g_xConnectionID.IsValid(), "unexpected connection ID");
					xListener.SendMessage(g_xConnectionID, dbg_usUnittestMessageID, uRS_UNITTEST_BUFFER_SIZE, g_acRandomBuffer);
					NextTestState();
				}
				break;
			}
		case TEST_STATE_SENDING_FROM_SERVER_TO_CLIENT:
			{
				if(g_uNumMessagesReceived == 1)
				{
					g_uNumMessagesReceived = 0;
					// send various-sized messages one after another (server to client multiple)
					for (u_int u = 0; u < uRS_UNITTEST_NUM_MESSAGES; ++u)
					{
						u_short usBufferSize = ausTCP_UNITTEST_MESSAGE_SIZE[u];
						xListener.SendMessageToAll(dbg_usUnittestMessageID, usBufferSize, g_acRandomBuffer);
					}
					NextTestState();
				}
				break;
			}
		case TEST_STATE_SENDING_FROM_SERVER_TO_CLIENT_MULTIPLE:
			{
				if(g_uNumMessagesReceived == uRS_UNITTEST_NUM_MESSAGES)
				{
					g_uNumMessagesReceived = 0;
					// do broadcast.
					xListener.SendMessageToAll(dbg_usUnittestMessageID, uRS_UNITTEST_BUFFER_SIZE, g_acRandomBuffer);
					NextTestState();
				}
				break;
			}
		case TEST_STATE_BROADCASTING:
			{
				if(g_uNumMessagesReceived == 1)
				{
					g_uNumMessagesReceived = 0;
					xListener.Disconnect(g_xConnectionID, ASURA_RS_DISCONNECT_UNKNOWN);
					NextTestState();
				}
				break;
			}
		case TEST_STATE_CLOSING_FROM_SERVER:
			{
				if(!xClientConnection.IsActive() && !xListener.GetNumConnections())
				{
					TestConnect(xClientConnection, dbg_uVersionNumber, usPort);
					NextTestState();
				}
				break;
			}
		case TEST_STATE_CONNECTING_2:
			{
				if(xClientConnection.IsConnected() && xListener.GetNumConnections())
				{
					xClientConnection.Disconnect(false, ASURA_RS_DISCONNECT_UNKNOWN);
					NextTestState();
				}
				break;
			}
		case TEST_STATE_CLOSING_FROM_CLIENT:
			{
				if(!xClientConnection.IsActive() && !xListener.GetNumConnections())
				{
					TestConnect(xClientConnection, dbg_uVersionNumber, usPort);
					NextTestState();
				}
				break;
			}
		case TEST_STATE_CONNECTING_3:
			{
				if(xClientConnection.IsConnected() && xListener.GetNumConnections())
				{
					// disconnect the previous connection
					xClientConnection.Disconnect(false, ASURA_RS_DISCONNECT_UNKNOWN);
					// try and connect with incorrect version number
					TestConnect(xClientConnection, dbg_uVersionNumber + 1, usPort);
					NextTestState();
				}
				break;
			}
		case TEST_STATE_CONNECTING_INVALID:
			{
				if(g_bConnectionRejected && !xListener.GetNumConnections() && !xClientConnection.IsConnected())
				{
					NextTestState();
				}
				break;
			}
		case TEST_STATE_FINISHED:
			{
				break;
			}
		}
	}

	Asura_Assert(!g_xConnectionID.IsValid(), "disconnection callback not called");

	// Cleanup
	if(xClientConnection.IsActive())
	{
		xClientConnection.Disconnect(true, ASURA_RS_DISCONNECT_UNKNOWN);
	}

	Asura_Assert(xListener.IsActive(), "IsActive() test failed");
	xListener.Close();
	Asura_Assert(!xListener.IsActive(), "IsActive() test failed");
}

#endif // #ifdef _DEBUG

#endif // #ifdef ASURA_SUPPORT_EXTERNAL_NETWORK
