/******************************************************************************/
/*!
    EasyTCP
    Copyright 2005 Electronic Arts Inc.

    \file
    \ingroup    Network
    \brief      Easy-to-use (simple) interface to a DirtySock TCP connection

    All connection maintainence is handled by the same thread, the game thread.
    Other threads signal their intention through flags.
*/
/******************************************************************************/

/*** Documentation ************************************************************/

/******************************************************************************/
/*! \class  EasyTCP

    \brief  Easy-to-use (simple) interface to a DirtySock TCP connection
*/
/******************************************************************************/

/*** Includes *****************************************************************/

#include <GameTalk/DirtySock/EasyTCP.h>
#include <dirtysock.h>
#include <netconn.h>

/*** Macros *******************************************************************/

/*** Interface ****************************************************************/

/*** Variables ****************************************************************/

/*** Implementation ***********************************************************/

/******************************************************************************/
/*! Connection :: Connection

    \brief  Constructor (doesn't actually create a connection to the server)
			Defaulting the timeout for a network IP address lookup to one second
			Increase this if your network and/or DNS Server are "slow"

    \return                   - void.
*/
/******************************************************************************/
EasyTCP::Connection::Connection(bool listener) :
mSocket(NULL),
mListenerSocket(NULL),
mTimeout(1000),
mConnectionError(false),
mParser(NULL),
mReceiveBufferIndex(0),
mListener(listener)
{
}

/******************************************************************************/
/*! Connection :: ~Connection

    \brief  Destructor...tears down the connection, if necessary.

    \param          - none.
    \return         - void.
*/
/******************************************************************************/
EasyTCP::Connection::~Connection()
{
    Disconnect();

	if(mListenerSocket)
	{
		SocketClose(mListenerSocket);
		mListenerSocket = NULL;
	}
}

/******************************************************************************/
/*! Connection :: Connect

    \brief  Establish a network connection to the server on the specified
            TCP port.

    \param  hostName - Server's DNS name or IP address (in textual form)
    \param  tcpPort  - Server's listening TCP port
    \return          - true if successfully connected (or already connected)

    NOTE:  This call is synchronous.  It blocks until either the connection
           completes or fails.
*/
/******************************************************************************/
bool EasyTCP::Connection::Connect(const char *hostName, uint16_t tcpPort)
{
	// At least on xenon we can use a socket that has never successfully connected to create a connection
	// So we'll potentially need to do the following even if we already have a valid mSocket - may just need to connect it
	// We may want to make the following conditional upon us not already having a valid connection - perhaps:
	// if (!IsConnected())
    uint32_t ipAddress = 0u;
    return LookupIPAddress(hostName, &ipAddress) && ConnectToServer(ipAddress, tcpPort);
}

/******************************************************************************/
/*! Connection :: Listen

\brief  Listen on the specified TCP port.

\param  tcpPort  - Listening TCP port
\return          - true if successfully connected (or already connected)

NOTE:  This call is synchronous.  It blocks until either the connection
completes or fails.
*/
/******************************************************************************/
bool EasyTCP::Connection::Listen()
{
	if (mListenerSocket)
	{
		AcceptConnection();
		return !ConnectionError();
	}

	// We can't listen if we don't have a valid listener socket
	return false;
}

/******************************************************************************/
/*! Connection :: IdlePulse

    \brief  Call this frequently (not necessarily every frame) to give the
            network system a slice of time.

    \param          - none.
    \return         - void.

    This function used to try to act on the settings of flags within this class.
	However, the functionality seemed needlessly complex - especially since we now support reconnections
*/
/******************************************************************************/
void EasyTCP::Connection::IdlePulse()
{
#ifndef GAMETALK_AUTO_IDLE_NETWORK_LAYER
    NetConnIdle();
#endif
}

/******************************************************************************/
/*! Connection :: Send

    \brief  Send data to the server.

    \param  bytes     - The payload.
    \param  numToSend - The size of the payload.
    \return           - true if successfully sent
*/
/******************************************************************************/
bool EasyTCP::Connection::Send(const uint8_t *bytes, uint32_t numToSend)
{
    if (mSocket)
    {
		if (SendWholeMsg(bytes, static_cast<int>(numToSend))) // Don't bother asserting if the message isn't sent - it is feasible for the connection to be lost at anytime
	        return true;
	}
	else
		USER_GAMETALK_FAIL("Tried to send when mSocket was null");

    mConnectionError = true;
    return false;
}

/******************************************************************************/
/*! Connection :: IsConnected

\brief  Check whether we are connected

\param          - none.
\return         - true if connected otherwise false.
*/
/******************************************************************************/
bool EasyTCP::Connection::IsConnected() const
{
	if (mSocket)
	{
		const int info = USER_GAMETALK_CREATE_BINARY_GUID('s', 't', 'a', 't');
		int32_t success = SocketInfo(mSocket, info, 0, NULL, 0);
		return success > 0;
	}
	return false;
}

/******************************************************************************/
/*! Connection :: Disconnect

    \brief  Tear down the connection, if it exists.

    \param          - none.
    \return         - void.
*/
/******************************************************************************/
void EasyTCP::Connection::Disconnect()
{
    if (mSocket)
    {
        SocketClose(mSocket);
        mSocket = NULL;
    }
	// In the case of a reconnection - we'll need to reset our index into our string buffer
	mReceiveBufferIndex = 0;
}

/******************************************************************************/
/*! Connection :: AcceptConnection

\brief  Accept a connection if one

*/
/******************************************************************************/
void EasyTCP::Connection::AcceptConnection()
{
	if (!mListenerSocket) // If we're not in passive mode - we don't have a socket to listen on (mListenerSocket)
		return;

	// See if we got a connection
	sockaddr addr;
	int32_t len = sizeof(addr);
	USER_GAMETALK_MEMSET(&addr, 0, sizeof(addr));
	SocketT* sock = SocketAccept(mListenerSocket, &addr, &len);
	if (sock != NULL)
	{
		// Store the socket for the connection !!!
		mSocket = sock;
		mConnectionError = false;
	}
}

/******************************************************************************/
/*! Connection :: LookupIPAddress

    \brief  Resolve the hostName into an IP address (either through a DNS
            lookup or through direct interpretation).

    \param  hostName  - DNS name or IP address in textual form
    \param  ipAddress - [OUT] The resulting IP address
    \return           - true if successful

    NOTE:  This call is synchronous.  It blocks until either the response
           is received, there's a network problem, or the connection times out.
*/
/******************************************************************************/
bool EasyTCP::Connection::LookupIPAddress(const char *hostName, uint32_t *ipAddress)
{
    HostentT *Host = SocketLookup(hostName, mTimeout);
    bool wasSuccessful = false;

    if (Host)
    {
        int32_t Success;

        for (Success = 0; !Success; Success = Host->Done(Host))
            IdlePulse();

        if (Success > 0)
        {
            *ipAddress = Host->addr;
            wasSuccessful = true;
        }

        Host->Free(Host);
    }

    return wasSuccessful;
}

/******************************************************************************/
/*! Connection :: InitListenerSocket

\brief  Create a connection to listen to the server specified at the given TCP port.

\param  ipAddress - The IP address of the server
\param  tcpPort   - The TCP port of the server to connect to
\return           - true if successfully established a connection to
the server.

NOTE:  This call is synchronous.  It blocks until either the connection
completes or fails.
*/
/******************************************************************************/
bool EasyTCP::Connection::InitListenerSocket(uint16_t tcpPort)
{
	USER_GAMETALK_ASSERT(!mSocket, "Connection already exists");

	// Get a socket
	mSocket = SocketOpen(AF_INET, SOCK_STREAM, 0);
	if (mSocket == NULL)
		return false;

	// Bind it to a port
	sockaddr addr;
	SockaddrInit(&addr, AF_INET);
	SockaddrInSetPort(&addr, tcpPort);
	int32_t netError = SocketBind(mSocket, &addr, sizeof(addr));
	if (netError != 0)
		return false;

	// Start a listen
	netError = SocketListen(mSocket, 5);
	if (netError != 0)
		return false;

	mConnectionError = false;
	mListenerSocket = mSocket;
	// Set callback from DirtySock
	return true;
}

/******************************************************************************/
/*! Connection :: ConnectToServer

    \brief  Create a connection to the server specified at the given TCP port.

    \param  ipAddress - The IP address of the server
    \param  tcpPort   - The TCP port of the server to connect to
    \return           - true if successfully established a connection to
                        the server.

    NOTE:  This call is synchronous.  It blocks until either the connection
           completes or fails.
*/
/******************************************************************************/
bool EasyTCP::Connection::ConnectToServer(const uint32_t ipAddress, uint16_t tcpPort)
{
	if (!mSocket)
	{
		mSocket = SocketOpen(AF_INET, SOCK_STREAM, 0);
		if (!mSocket)
		{
			USER_GAMETALK_FAIL("Couldn't create socket");
			return false;
		}
	}

	sockaddr addr;
	SockaddrInit(&addr, AF_INET);
	SockaddrInSetPort(&addr, tcpPort);
	SockaddrInSetAddr(&addr, ipAddress);

	int socketerror = SocketConnect(mSocket, &addr, sizeof(sockaddr));
	if (SOCKERR_NONE != socketerror)
		return false;

	int32_t success(0);
	do // Since we're in non-blocking we may need to call this a few times to get a one (instead of a zero)
	{
		const int info = USER_GAMETALK_CREATE_BINARY_GUID('s', 't', 'a', 't');
		success = SocketInfo(mSocket, info, 0, NULL, 0);
		IdlePulse();
	}
	while(!success);

    if (success <= 0)   // failed?
    {
		// If disconnect is done on a socket that was never connected - XDK errors are written to the console
		// On xenon it is actually possible to use a socket that was never connected to 
		// connect to the server - but I'll need to test this on other platforms before 
		// I change the DirtySock GameTalkProtocol to work like that
        Disconnect();
        return false;
    }

    mConnectionError = false;
    return true;
}

/******************************************************************************/
/*! Connection :: SendWholeMsg

    \brief  Completely send the amount of data given.

    \param  Bytes     - The data to send
    \param  NumToSend - How much data to send
    \return           - true  => sent all data successfully
                        false => network problem or timeout
*/
/******************************************************************************/
bool EasyTCP::Connection::SendWholeMsg(const uint8_t *bytes, int numToSend)
{
	IdlePulse();
    USER_GAMETALK_ASSERT(mSocket, "No Connection");

    int totalSent = 0;
	uint32_t startTime = NetConnElapsed();
    while ((mSocket) && (totalSent < numToSend))
    {
        const int32_t numSent = SocketSend(mSocket, reinterpret_cast<const char *>(bytes) + totalSent, numToSend - totalSent, 0);
        if (numSent > 0)
        {
            totalSent += numSent;
			// We reset the timer each time we have a successful send in case user
			// tries to send a massive amount of data that would exceed our statically set timeout
			startTime = NetConnElapsed();
        }
        else if (numSent < SOCKERR_NONE)    // error?
        {
            break;
        }
		else if (NetConnElapsed() - startTime > 5000) // Have we waited more than five seconds just to send one byte of data?
		{
			// The above condition used to be for just 1000 milliseconds - but for large sends that can be insufficient
			break;
		}
    }
	return totalSent == numToSend;
}

/******************************************************************************/
/*! Connection :: PollSocket

    \brief  Process as much data received from the network as possible.

    \param          - none.
    \return         - true  => all data was received successfully
                      false => network error or timeout

    NOTE:  This method is called from a network thread (i.e. a thread
           other than the game thread).
*/
/******************************************************************************/
int EasyTCP::Connection::PollSocket()
{
	if (mConnectionError)
		return -1;
    else if (mSocket)
    {
		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]);
        const int32_t numReceived = SocketRecv(mSocket, receiveBuffer, GAMETALK_RECEIVE_BUFFER_SIZE-mReceiveBufferIndex, 0);
        if (numReceived < SOCKERR_NONE)    // error?
		{
            mConnectionError = true;
			return numReceived;
		}
        else if (uint32_t(numReceived) > GAMETALK_RECEIVE_BUFFER_SIZE)
		{
			USER_GAMETALK_FAIL("Buffer is too small for data");
			return 0;
		}

        if (numReceived > 0)
		{
			mReceiveBufferIndex += numReceived;
			mParser->DigestReceivedData(receiveBuffer, static_cast<uint32_t>(numReceived));
			mReceiveBufferIndex -= numReceived;
		}

		return numReceived;
    }
    return 0;
}

bool EasyTCP::Connection::ConnectionError()
{
	return mConnectionError;
}
