// PNSocket.cpp: implementation of the CCPNSocket class.
//
//////////////////////////////////////////////////////////////////////
// disable warning C4786: symbol greater than 255 character,
// okay to ignore
#pragma warning(disable: 4786)

#include "stdafx.h"
#include "PNSocket.h"
// disable warning C4786: symbol greater than 255 character,
// okay to ignore
#pragma warning(disable: 4786)

#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif

//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////

// Common error strings, to save some space
static char ErrNotInit[] = "Socket not initialized";
static char ErrConTerm[] = "Connection terminated";
static char ErrTimedOut[]= "Connection timed out";

// m_szErrMsg is not part of the class, because of the const functions throwing exceptions
// might modify it.  Size is probably overkill.  Unfortunately, my g++ compiler doesn't
// support the sstream class.
static char m_szErrMsg[256];

// Statics for the class
bool CPNSocket::m_bThrowException = true;
map<int,int> CPNSocket::RefCount;

//////////////////////////////////////////// Start of the CPNSocket class functions

// Constructors
CPNSocket::CPNSocket( int nFamily, int nType, int nProtocol )
{
	m_nTimeOut=10000;
	// Try to intialize the CPNSocket, using the socket system call	
	m_nSocket = ::socket( nFamily, nType, nProtocol );
	if( m_nSocket < 0 && m_bThrowException ) // If an error, and we should throw it
		ThrowErrMsg( "socket() call failed: %d", m_nSocket );
	if( m_nSocket >= 0 )
		IncRef();
}

// Copy constructor
CPNSocket::CPNSocket( const CPNSocket& msrSrc )
{
	m_nTimeOut=10000;
	m_nSocket = msrSrc.m_nSocket;
	IncRef();
}

// Destructor
CPNSocket::~CPNSocket()
{
	if( m_nSocket >= 0 )
		Close();
}

// Closes socket, but pays attention to reference counting
int CPNSocket::Close()
{
	int Ret = m_nSocket;
	if( Ret < 0 && m_bThrowException )
		ThrowErrMsg( "Close called on a closed socket" );
	if( Ret > 0 )
	{
		DecRef();
		if( !GetRef() )
			closesocket( m_nSocket );
		m_nSocket = -1;
	}
	return( Ret );
}

// Connect - Establishes a socket connection to a server
BOOL CPNSocket::Connect( struct sockaddr * sapSrvrAdrs, int nLenAddr ) const
{
	if( m_nSocket < 0 )
	{
		if( m_bThrowException ) // If not setup correctly
			ThrowErrMsg( ErrNotInit );
		return(FALSE);
	}
	
	// Call the connect system function to actually do connect.
	int Ret = ::connect( m_nSocket, (struct sockaddr*)sapSrvrAdrs, nLenAddr );
	if( Ret==SOCKET_ERROR )
	{
		if( m_bThrowException )
			ThrowErrMsg( "connect() failed", WSAGetLastError() );
		return( FALSE );
	}
	
	if( TimedOut(toWrite) )
	{
		if( m_bThrowException )
			ThrowErrMsg( "connect timed out" );
		return(FALSE);
	}

	if( Ret < 0 && m_bThrowException ) // If there was an error, and we should throw it.
		ThrowErrMsg( "connect() failed: %d", Ret );
	return( Ret==0?TRUE:FALSE );
}

// Overloaded Connection function, easier to call then previous version
int CPNSocket::Connect( const char* cpHostAddr, short sPort ) const
{
	// Setup and init the sock_addr_in struct, needed by real connect function
	struct sockaddr_in SrvrAdrs;
	memset( &SrvrAdrs, 0, sizeof( SrvrAdrs ) );

	// If szHostAddr looks like '127.0.0.1' then it's easy, just
	// call inet_addr to convert the string into a netword address ID integer
	if( isdigit(cpHostAddr[0]) )
		SrvrAdrs.sin_addr.s_addr = inet_addr( cpHostAddr );
	else
	{
		// Otherwise, it may be a host name.  Get it's IP address and use that.
		// For example, szHostAddr may be www.acme.com
		struct hostent * pHostEnt = gethostbyname( cpHostAddr );
        if( pHostEnt == NULL )
		{
			if( m_bThrowException )
				ThrowErrMsg( "Unable to determine IP for '%s'", cpHostAddr );
			return( -1 );
		}
		SrvrAdrs.sin_addr.s_addr = *(long*)pHostEnt->h_addr_list[0];		
	}
	SrvrAdrs.sin_family = AF_INET;
	// Call htons, to convert out local pc short to format compatible with the 'net'
	SrvrAdrs.sin_port = htons( sPort );

	// finally, call the other version of Connect, with the struct we set up
	return( Connect( (struct sockaddr*) &SrvrAdrs , (int)sizeof( SrvrAdrs ) ) );
}

// Bind Function
int CPNSocket::Bind( struct sockaddr* sapMyAddr, int nAddrLen ) const
{
	if( m_nSocket < 0 )
	{
		if( m_bThrowException ) // If not setup correctly
			ThrowErrMsg( ErrNotInit );
		return(-1);
	}
	// Call the bind system function to actually do connect.
	int Ret = ::bind( m_nSocket, sapMyAddr, nAddrLen );
	if( Ret < 0 && m_bThrowException ) // If there was an error, and we should throw it.
		ThrowErrMsg( "bind() failed: %d", Ret );
	return( Ret );
}

// Overloaded Bind function, is easier to call then previous
int CPNSocket::Bind( int nPort, int nFamily, int nAddr ) const
{
	struct sockaddr_in SrvrAdrs;
	memset( &SrvrAdrs, 0, sizeof( SrvrAdrs ) );
	SrvrAdrs.sin_family = nFamily;
	SrvrAdrs.sin_addr.s_addr = htonl( nAddr );
	SrvrAdrs.sin_port = htons( nPort );
	// Here, we call our 'other' Bind member function
	return( Bind( (struct sockaddr*)&SrvrAdrs, (int)sizeof( SrvrAdrs ) ) );
}

// Listen - Tells Operating System we are ready to accept connections on a socket
int CPNSocket::Listen( int nBackLog ) const
{
	if( m_nSocket < 0 )
	{
		if( m_bThrowException )
			ThrowErrMsg( ErrNotInit );
		return( -1 );
	}
	// Call the system 'listen' function
	int Ret = ::listen( m_nSocket, nBackLog );
	if( Ret < 0 && m_bThrowException )
		ThrowErrMsg( "listen() failed: %d", Ret );
	return( Ret );
}

//Server - Helper function, for writing a Server application
int CPNSocket::Server( int nPort ) const
{
	int Ret;
	Ret = Bind( nPort );
	if( Ret >= 0 )
		Ret = Listen();
	return( Ret );
}

// Accept - Used by servers, to accept a socket connection from a client
int CPNSocket::Accept( CPNSocket& msrRet, struct sockaddr* sapClientAddr, int* npLen ) const
{
	int MyLen;
	struct sockaddr_in Client;
	if( sapClientAddr == 0 ) // To make things easier on our caller, we can create the struct
	{
		sapClientAddr = (sockaddr*)& Client;
		npLen = &MyLen;
	}
	if( m_nTimeOut && TimedOut(toRead) )
	{
		if( m_bThrowException )
			ThrowErrMsg( "Timed out on Accept" );
		return( -1 );
	}
	// Call the system 'accept' function
	int nRet = ::accept( m_nSocket, sapClientAddr, (int*)npLen );
	if( nRet < 0 && m_bThrowException )
		ThrowErrMsg( "accept failed: %d", nRet );
	msrRet = nRet;
	return( nRet );
}

// Assignment operator, if from another CPNSocket.  Invokes the other operator=
CPNSocket& CPNSocket::operator=( CPNSocket& msrSrc )
{
	if( &msrSrc != this ) // Make sure caller didn't to X = X;
		*this = msrSrc.m_nSocket;
	return( *this );
}

// Asignment operator.  Pays attention to reference counting
CPNSocket& CPNSocket::operator=( int nSocket )
{
	if(  nSocket < 0 && m_bThrowException )
		throw "operator= called with bad socket";

	if( m_nSocket >= 0 )
		Close();
	m_nSocket = nSocket;
	IncRef();
	return( *this );
}

// TimedOut - Uses select() to determine if data is ready to be read, or written, with timeout period
int CPNSocket::TimedOut( toMode Mode ) const
{
	fd_set fdSet;
	struct timeval TimeToWait;
	TimeToWait.tv_sec = m_nTimeOut / 1000;
	TimeToWait.tv_usec = m_nTimeOut % 1000;
	FD_ZERO( &fdSet );
	FD_SET( m_nSocket+1, &fdSet );
	int Ret;

	Ret = select( m_nSocket+1, &fdSet, &fdSet, &fdSet, &TimeToWait );

	return( Ret>0?0:1 );
}

// Sets the exception operating mode for the class.  Throw exceptions, or return error codes.
bool CPNSocket::SetExceptions( bool bMode )
{
	bool Ret;
	Ret = m_bThrowException;
	m_bThrowException = bMode;
	return( Ret );
}

// Sets the time out for an individual socket.
int CPNSocket::SetTimeOut( int nMilliSeconds )
{
	int Ret = m_nTimeOut;
	m_nTimeOut = nMilliSeconds;	
	return( Ret );
}

// Reads data from a socket, a line (CR delimited) or a block
int CPNSocket::Receive( char *cpDest, int nSize, char Term ) const
{
	int ReadIn=0, Stat=0;
	if( m_nSocket < 0 )
	{
		if( m_bThrowException )
			ThrowErrMsg( ErrNotInit );
		return( 0 );
	}

	while( ReadIn < nSize-2 )
	{
		if( m_nTimeOut && TimedOut(toRead) )
		{
			if( m_bThrowException )
				ThrowErrMsg( ErrTimedOut );
			return( 0 );
			
		}
		Stat = recv( m_nSocket, cpDest+ReadIn, 1, 0 );
		if( Stat < 0 )
		{
			if( m_bThrowException )
				ThrowErrMsg( Stat==0?ErrConTerm:"ReadLine error: %d", Stat );
			return( 0 );
		}
		if( cpDest[Stat+ReadIn-1]==Term )
			break;
		ReadIn += Stat;
	}
	cpDest[ReadIn+Stat-1]='\0';
	return( ReadIn+Stat );
}

int CPNSocket::ReceiveLine( char *cpDest, int nSize ) const
{
	int Ret=Receive( cpDest, nSize, '\r' );
	if(Ret)
	{
		while( strlen(cpDest)>0 && cpDest[strlen(cpDest)-1]<' ')
			cpDest[strlen(cpDest)-1]='\0';
		while( strlen(cpDest)>0 && cpDest[0]<' ' )
			memmove( cpDest, cpDest+1, strlen(cpDest));
	}
	return(Ret);
}

// Writes a block of data to a socket
int CPNSocket::Send( const char *cpSrc, int Len ) const
{
	int Written=0;
	if( m_nSocket < 0 )
	{
		if( m_bThrowException )
			ThrowErrMsg( ErrNotInit );
		return( 0 );
	}
	while( Len>0 )
	{
		Written=send( m_nSocket, cpSrc, Len, 0 );
		if( Written <=0 )
		{
			if( m_bThrowException )
				ThrowErrMsg( Written==0?ErrConTerm:"Writing socket: %d", Written );
			return( 0 );
		}
		cpSrc += Written;
		Len -= Written;
	}
	return( 1 );
}

// Writes a line of text to a socket, like a string
int CPNSocket::SendLine( const char *cpSrc, char Term ) const
{
	int Written=0, Len;
	Len = strlen(cpSrc);
	if( Term=='\0' ) // If terminator is '\0', include that in data out
		Len++;
	return( Send( cpSrc, Len ) );
}

// ThrowErrMsg - Helper function to create a string, and throw it as an exception
void CPNSocket::ThrowErrMsg( const char* cpMsg, int nCode ) const
{
	sprintf( m_szErrMsg, cpMsg, nCode );
	throw m_szErrMsg ;
}

// WasConTermErr - Helper function, to determine if the connection was terminated
bool CPNSocket::WasConTermErr( const char* ErrMsg )
{
	return( strcmp(ErrMsg,ErrConTerm)==0);
}

const char* CPNSocket::GetErrMsg() const 
{
	static char Msg[128];
	char* pMsg="";
	int ErrCode;

	if( 0 != (ErrCode=WSAGetLastError()) )
	{
      switch( ErrCode )
      {
         case WSAEADDRINUSE: 
            pMsg = "The specified address is already in use.";
            break;
         case WSAEADDRNOTAVAIL: 
            pMsg = "The specified address is not available from the local machine.";
            break;
         case WSAEAFNOSUPPORT: 
            pMsg = "Addresses in the specified family cannot be used with this socket.";
            break;
         case WSAECONNREFUSED: 
            pMsg = "The attempt to connect was forcefully rejected.";
            break;
         case WSAEDESTADDRREQ: 
            pMsg = "A destination address is required.";
            break;
         case WSAEFAULT: 
            pMsg = "The lpSockAddrLen argument is incorrect.";
            break;
         case WSAEINVAL: 
            pMsg = "The socket is already bound to an address.";
            break;
         case WSAEISCONN: 
            pMsg = "The socket is already connected.";
            break;
         case WSAEMFILE: 
            pMsg = "No more file descriptors are available.";
            break;
         case WSAENETUNREACH: 
            pMsg = "The network cannot be reached from this host at this time.";
            break;
         case WSAENOBUFS: 
            pMsg = "No buffer space is available. The socket cannot be connected.";
            break;
         case WSAENOTCONN: 
            pMsg = "The socket is not connected.";
            break;
         case WSAENOTSOCK: 
            pMsg = "The descriptor is a file, not a socket.";
            break;
         case WSAETIMEDOUT: 
            pMsg = "The attempt to connect timed out without establishing a connection. ";
            break;
         default:
            wsprintf(Msg, "Unknown error: %d", ErrCode);
            pMsg = Msg;
            break;
		}		
	}
	return( pMsg );
}
