/*
Copyright 2004-2006 Electronic Arts Inc.
*/

#include "GameTalk/Win32/GameTalkProtocol.h"

EA::GameTalk::GameTalkProtocol::GameTalkProtocol(const char* const ipAddress, const int port) :
	IGameTalkProtocol(ipAddress, port), mSocket(INVALID_SOCKET), mListenerSocket(INVALID_SOCKET), 
	mConnected(false), mReceiveBufferIndex(0)

{
	USER_GAMETALK_ASSERT((port >= 0) && (port <= 65535), "GameTalkProtocol: Bad port specified");
	InitSocket();
	Connect();
}

EA::GameTalk::GameTalkProtocol::GameTalkProtocol(const int port) :
IGameTalkProtocol(port), mSocket(INVALID_SOCKET), mListenerSocket(INVALID_SOCKET), 
mConnected(false), mReceiveBufferIndex(0)
{
	USER_GAMETALK_ASSERT((port >= 0) && (port <= 65535), "GameTalkProtocol: Bad port specified");
	InitSocket();
}

void EA::GameTalk::GameTalkProtocol::InitSocket()
{
	// Initialize socket
	mSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (mSocket == INVALID_SOCKET)
	{
		char errorMsg [1024];
		sprintf_s(errorMsg, 1024, "Failed to create socket, error %d.\n", WSAGetLastError());
		USER_GAMETALK_FAIL(errorMsg);
	}

	SOCKADDR_IN sa;
	USER_GAMETALK_MEMSET(&sa, 0, sizeof(sa));
	sa.sin_family      = AF_INET;
	sa.sin_addr.s_addr = INADDR_ANY;
	if (mIpAddress) // Setting the port to zero will allow winsock to choose the port
		sa.sin_port        = 0;
	else
		sa.sin_port        = htons((u_short)mPort);

	if (bind(mSocket, (const sockaddr*)&sa, sizeof(sa)) == SOCKET_ERROR)
	{
		char errorMsg [1024];
		sprintf_s(errorMsg, 1024, "Failed to bind socket, error %d.\n", WSAGetLastError());
		USER_GAMETALK_FAIL(errorMsg);
	}
	
	// Set sockets to non-blocking in both active and passive modes
	SetSocketBlocking(false);
	USER_GAMETALK_ASSERT(mSocket != INVALID_SOCKET, "mSocket is invalid");
	if (mIpAddress)
	{
		USER_GAMETALK_MEMSET(&mRemote, 0, sizeof(mRemote));
		mRemote.sin_family      = AF_INET;
		mRemote.sin_addr.s_addr = inet_addr(mIpAddress);
		mRemote.sin_port        = htons((u_short)mPort);
	}
	else
	{
		INT iBacklog = SOMAXCONN;
		INT iResult = listen( mSocket, iBacklog );
		if (iResult != 0)
		{
			char errorMsg [1024];
			sprintf_s(errorMsg, 1024, "Failed to listen on socket, error %d\n", iResult );
			USER_GAMETALK_FAIL(errorMsg);
		}
		else // The mListenerSocket is what we'll do our accept on
			mListenerSocket = mSocket;
	}
}

void EA::GameTalk::GameTalkProtocol::Disconnect()
{
	// In the case of a reconnection - we'll need to reset our index into our string buffer
	mReceiveBufferIndex = 0;

	if (!mConnected)
		return;

	mConnected = false;
	if (mSocket != INVALID_SOCKET)
	{
		closesocket(mSocket);
		mSocket = INVALID_SOCKET;
	}
}

EA::GameTalk::GameTalkProtocol::~GameTalkProtocol(void)
{
	Disconnect();
	if (mListenerSocket != INVALID_SOCKET)
	{
		closesocket(mListenerSocket);
		mListenerSocket = INVALID_SOCKET;
	}
}

bool EA::GameTalk::GameTalkProtocol::Listen (void)
{
	if (!mConnected)
	{
		if (mListenerSocket == INVALID_SOCKET)
			return false;

		int sizeOfRemoteAddr = sizeof(mRemote);
		SOCKET socket = accept( mListenerSocket, (sockaddr *) &mRemote, &sizeOfRemoteAddr );
		if( socket != INVALID_SOCKET && (&mRemote) != NULL )
		{
			Disconnect();
			mSocket = socket;
			mConnected = true;
		}
		else
		{
			int error = WSAGetLastError();
			if (error == WSAEISCONN)
				mConnected = true;
			
			char errorMsg [1024]; 
			sprintf_s(errorMsg, 1024, "connect failed with error %d\n", error);
			USER_GAMETALK_ASSERT((error == WSAEWOULDBLOCK) || (error == WSAEALREADY), errorMsg);
		}
	}
	return mConnected;
}

bool EA::GameTalk::GameTalkProtocol::Connect (void)
{
	if (!mIpAddress)
	{
		// If the mListenSocket has been initialized - then we need to listen - not try to connect to a server
		mConnected = Listen();
		return mConnected;
	}
	else if (!mConnected) // If this is active mode and we're not connected - connect
	{
		if (mSocket == INVALID_SOCKET)
			InitSocket();

		int result = connect(mSocket, (const sockaddr*)&mRemote, sizeof(mRemote));
		if (SOCKET_ERROR != result)
			mConnected = true;
		else
		{
			int error = WSAGetLastError();
			if (error == WSAEISCONN)
				mConnected = true;
			else if (error == WSAEWOULDBLOCK) // It is possible the connect function call has not yet completed (especially when we're reconnecting)
			{
				fd_set fdwrite;
				fd_set fdexcept;
				FD_ZERO(&fdwrite);
				FD_ZERO(&fdexcept);
				FD_SET(mSocket, &fdwrite);
				FD_SET(mSocket, &fdexcept);
				// We may want to pass a non-null timeval as the last argument to the select - since it seems in some situations this could result in blocking behavior
				int socketsAvailable = select(0, NULL, &fdwrite, &fdexcept, NULL);

				// If we had no errors and we have a writeable socket - we're golden - we don't need to use FD_ISSET because we only submitted one socket
				if ((socketsAvailable != SOCKET_ERROR) && (fdexcept.fd_count == 0) && (fdwrite.fd_count > 0))
					mConnected = true;
			}

			char errorMsg2 [1024]; 
			sprintf_s(errorMsg2, 1024, "connect failed with error %d\n", error);
			// We support the concept of the GameTalkServer not yet runnnig - hence why we don't assert on WSAECONNREFUSED
			USER_GAMETALK_ASSERT((error == WSAEWOULDBLOCK) || (error == WSAEALREADY) || (error == WSAECONNREFUSED) || (error == WSAEISCONN) || (error == WSAETIMEDOUT), errorMsg2);
		}
	}
	return(mConnected);
}

bool EA::GameTalk::GameTalkProtocol::IsConnected ()
{
	return mConnected;
}

bool EA::GameTalk::GameTalkProtocol::Send(void* buffer, size_t msgSize)
{
	if (this->Connect())
	{
		int dataSent = 0;
		int thisIsIgnored = 0;
		const char* charBuffer = (const char*)buffer;
		fd_set singleSocketSet;
		FD_ZERO(&singleSocketSet);
		FD_SET(mSocket, &singleSocketSet);
		// If the msgSize is greater than 2,147,483,648 bytes - the cast will lose data - hopefully no one ever sends a msg greater than two gigs
		int msgSizeSigned = static_cast<int>(msgSize);
		while (mConnected != false)
		{
			int socketsAvailable = select(thisIsIgnored, NULL, &singleSocketSet, NULL, NULL);
			if (socketsAvailable == SOCKET_ERROR)
			{
				// Since we are using an infinite timeout - we should never get here unless the connection breaks
				Disconnect();
				break;
			}

			dataSent += send(mSocket, &(charBuffer[dataSent]), msgSizeSigned-dataSent, 0);
			if (dataSent == msgSizeSigned)
				return(true);
			else if (dataSent < 0)
			{
				int errCode = WSAGetLastError();
				if (errCode == WSAENOTCONN)
					Disconnect();

				break;
			}
		}
	}
	return(false);
}

int EA::GameTalk::GameTalkProtocol::Poll()
{
	if(!IsConnected()) // Not much point in trying to receive on a socket that isn't connected
		return -1;

	if (GAMETALK_RECEIVE_BUFFER_SIZE - mReceiveBufferIndex <= 0) // We're likely still connected - there just isn't any room in our receiving buffer
	{
		// We retest the same condition to avoid the const expression compiler error/warning
		USER_GAMETALK_ASSERT(GAMETALK_RECEIVE_BUFFER_SIZE - mReceiveBufferIndex > 0, "There is no room left in the receiving buffer - try increasing GAMETALK_RECEIVE_BUFFER_SIZE.");
		return 0;
	}

	char * receiveBuffer = &(mReceiveBuffer[mReceiveBufferIndex]);
	int numbytes = recv(mSocket, receiveBuffer, GAMETALK_RECEIVE_BUFFER_SIZE - mReceiveBufferIndex, 0);
	if ((numbytes > 0) && (numbytes != SOCKET_ERROR))
	{
		mReceiveBufferIndex += numbytes;
		mParser->DigestReceivedData(receiveBuffer, (uint32_t)numbytes);
		mReceiveBufferIndex -= numbytes;
	}
	else if (numbytes == SOCKET_ERROR)
	{
		int winsockErrCode = WSAGetLastError();
		if (winsockErrCode == WSAEWOULDBLOCK) // Reassure our caller that we didn't lose our connection
			numbytes = 0;
		else if (mIpAddress)
		{
			if ((winsockErrCode == WSAENOTCONN) || (winsockErrCode == WSAECONNRESET))
				Disconnect();
		}
		else
		{
			if (winsockErrCode == WSAECONNRESET)
				Disconnect();
			else if(winsockErrCode == WSAENOTCONN)
			{
				// We need to reassure our caller that we didn't lose our connection
				numbytes = 0;
				if (!Listen())
					numbytes = -1;
			}
		}
	}

	return numbytes; 
}

bool EA::GameTalk::GameTalkProtocol::InitNetwork()
{
	bool successfullyInitialized = true;
#if defined(EA_PLATFORM_XENON)
	XNetStartupParams xnsp;
	USER_GAMETALK_MEMSET(&xnsp, 0, sizeof(xnsp));
	xnsp.cfgSizeOfStruct = sizeof(xnsp);
	xnsp.cfgFlags = XNET_STARTUP_BYPASS_SECURITY;
	INT err = XNetStartup(&xnsp);
	USER_GAMETALK_USE_VAR_TO_AVOID_WARNING(err);
	char errorStr [1024];
	if (err != 0)
	{
		sprintf(errorStr, "XNetStartup failed, error %d.\n", err);
		USER_GAMETALK_ASSERT(false, errorStr);
		successfullyInitialized = false;
	}
#endif

	WORD    wVersion = MAKEWORD( 2, 2 );   // request version 2.2 of Winsock
	WSADATA wsaData;
	int wsaErr = WSAStartup(wVersion, &wsaData);
	if (wsaErr != 0)
	{
		char errorStr [1024];
		sprintf_s(errorStr, 1024, "WSAStartup failed, error %d.\n", wsaErr);
		USER_GAMETALK_FAIL(errorStr);
		successfullyInitialized = false;
	}

	// Verify version of Winsock
	if (wsaData.wVersion != wVersion)
	{
		char errorStr [1024];
		sprintf_s(errorStr, 1024, "Failed to get proper version of Winsock, got %d.%d.\n", LOBYTE( wsaData.wVersion ), HIBYTE( wsaData.wVersion ));
		USER_GAMETALK_FAIL(errorStr);
		successfullyInitialized = false;
	}

	return successfullyInitialized;
}

bool EA::GameTalk::GameTalkProtocol::ShutdownNetwork()
{
	WSACleanup();
#if defined(EA_PLATFORM_XENON)
	XNetCleanup();
#endif

	return true;
}

bool EA::GameTalk::GameTalkProtocol::SetSocketBlocking(bool blocking)
{
	// Set *argp to a nonzero value if the nonblocking mode should be enabled, or zero if the nonblocking mode should be disabled
	unsigned long nonBlockingMode = (blocking)?0:1;
	if( ioctlsocket( mSocket, FIONBIO, &nonBlockingMode ) != 0 )
	{
		char errorMsg [1024];
		if (nonBlockingMode)
			sprintf_s(errorMsg, 1024, "Failed to set socket to nonblocking, error %d\n", WSAGetLastError());
		else
			sprintf_s(errorMsg, 1024, "Failed to set socket to blocking, error %d\n", WSAGetLastError());
		USER_GAMETALK_FAIL(errorMsg);
		return false;
	}
	return true;
}

