/***************************************************************************
                          cconnection.cpp  -  description
                             -------------------
    begin                : Sat Oct 6 2001
    copyright            : (C) 2001-2005 by Mathias Küster
    email                : mathen@users.berlios.de
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#include "cconnection.h"

#include <stdio.h>
#include <string.h>

#ifndef WIN32
#include <stdlib.h>
#include <unistd.h>
#else
#include <wtypes.h>
#include <winbase.h>
#include <stdlib.h>
#endif

/* strangely missing... */
#include <time.h>

#include "cbytearray.h"
#include "cmutex.h"
#include "cnetaddr.h"

#define BUFFER_SIZE		1024*50
#define DEFAULT_DC_PORT		411
#define CONNECTION_TIMEOUT	60
#define DATA_TIMEOUT		60
#define NOTIFY_TIMEOUT		1

CConnection::CConnection()
{
	m_nPort = DEFAULT_DC_PORT;

	m_bForceDisconnect = false;

	// set connection timeout
	m_nConnectTimeout = CONNECTION_TIMEOUT;
	m_timeNotify      = 0;
	m_timeConnection  = 0;

	m_eState = estNONE;

	m_pBuffer = new CByteArray(BUFFER_SIZE);

	m_pSendListMutex = new CMutex();
	m_pSendList = new CList<CByteArray>();

	m_pConnMutex = new CMutex();
}

CConnection::~CConnection()
{
	CSocket::Disconnect();

	delete m_pBuffer;
	m_pBuffer = 0;

	delete m_pSendList;
	m_pSendList = 0;

	delete m_pSendListMutex;
	m_pSendListMutex = 0;

	delete m_pConnMutex;
	m_pConnMutex = 0;
}

/** */
int CConnection::Connect()
{
	m_pConnMutex->Lock();

	int err = -1;

	if ( m_eState != estNONE )
	{
		StateDisconnect();
	}

	m_eState = estCONNECT;

	m_bForceDisconnect = false;

	err = 0;

	m_pConnMutex->UnLock();

	return err;
}

/** */
int CConnection::Connect( CString ip, eSocketType sockettype )
{
	unsigned int port;
	CString s;

	CNetAddr::ParseHost( ip, s, port );

	if ( port == 0 )
	{
		port = DEFAULT_DC_PORT;
	}

	return Connect( s, port, sockettype );
}

/** */
int CConnection::Connect( CString ip, int port, eSocketType sockettype )
{
	m_pConnMutex->Lock();

	m_sIP   = ip;
	m_nPort = port;

	SocketType = sockettype;

	m_pConnMutex->UnLock();

	return Connect();
}

/** */
int CConnection::Disconnect( bool force )
{
	int err = -1;

	if ( force )
	{
		err = 0;

		m_bForceDisconnect = true;
	}
	else
	{
		m_pConnMutex->Lock();

		if ( m_eState != estNONE )
		{
			m_eState = estDISCONNECTING;

			err = 0;
		}

		m_pConnMutex->UnLock();
	}

	return err;
}

/** */
int CConnection::SetSocket( int handle, eSocketType sockettype )
{
	CString ip;
	int port;

	if ( m_eState != estNONE )
	{
		return -1;
	}

	m_pConnMutex->Lock();

	m_sIP.Empty();
	m_nPort = 0;

	if ( CSocket::SetSocket(handle,sockettype) == -1 )
	{
		m_pConnMutex->UnLock();
		return -1;
	}

	// get remote host & port
	if ( GetPeerName( &ip, &port ) == false )
	{
		m_pConnMutex->UnLock();
		return -1;
	}

	// set remote host & port
	SetHost( ip, port );

	m_bForceDisconnect = false;

	// init data timeout
	m_timeConnection = time(0);
	// init notify timer
	m_timeNotify = time(0);

	m_eState = estCONNECTED;

	if ( m_eSocketMode == esmSOCKET )
	{
		connectionState(estCONNECTED);
	}
	else
	{
		connectionState(estSSLCONNECTED);
	}

	m_pConnMutex->UnLock();

	return 0;
}

/** */
CString CConnection::GetHost( bool peername )
{
	CString s,host;
	int port;

	if ( peername )
	{
		if ( !GetPeerName(&host,&port) )
		{
			return s;
		}
	}
	else
	{
		port = GetPort();
		host = GetIP();
	}

	s = host + ':' + CString::number(port);

	return s;
}

/** */
bool CConnection::ChangeSocketMode( eSocketMode mode, CString cert, CString key )
{
	bool res = false;

	m_pConnMutex->Lock();

	if ( m_eState == estCONNECTED )
	{
		// flush all data
		StateSend();

		if ( m_eState == estCONNECTED )
		{
			res = CSocket::ChangeSocketMode(mode, cert, key );

			if ( res && (mode != esmSOCKET) )
			{
				m_eState = estCONNECTING;
			}
		}
	}
	else
	{
		res = CSocket::ChangeSocketMode(mode, cert, key );
	}

	m_pConnMutex->UnLock();

	return res;
}

/** */
bool CConnection::IsSendQueueEmpty()
{
	bool res = true;

	if ( m_pSendList != 0 )
	{
		m_pSendListMutex->Lock();

		res = (m_pSendList->Count() == 0);

		m_pSendListMutex->UnLock();
	}

	return res;
}

/** */
void CConnection::Thread()
{
	int wait = 4;

	m_pConnMutex->Lock();

	if (m_bForceDisconnect)
	{
		// first we flush all messages
		if ( m_eState == estCONNECTED )
			StateSend();
		// set disconnect state
		if ( m_eState != estNONE )
			m_eState = estDISCONNECTING;
		m_bForceDisconnect = false;
	}

	switch(m_eState)
	{
		case estNONE:
			break;
		case estCONNECT:
			StateConnect();
			if ( m_eState == estCONNECT )
				wait = 100;
			break;
		case estCONNECTING:
			StateConnecting();
			break;
		case estCONNECTED:
			// read data
			StateRead();
			// send data
			if ( m_eState == estCONNECTED )
				StateSend();
			// send data from higher level
			if ( m_eState == estCONNECTED )
				DataSend();
			// check data timeout
			if ( m_eState == estCONNECTED )
			{
				if ( (time(0)-m_timeConnection) >= DATA_TIMEOUT )
				{
					DataTimeout();
					m_timeConnection = time(0);
				}
			}
			break;
		case estDISCONNECTING:
			StateDisconnect();
			break;
		case estDISCONNECTED:
			break;
		default:
			break;
	}

	if ( (time(0)-m_timeNotify) >= NOTIFY_TIMEOUT )
	{
		m_pConnMutex->UnLock();
		Notify();
		m_pConnMutex->Lock();
		m_timeNotify = time(0);
	}

	m_pConnMutex->UnLock();

	if ( iRun == 1 )
	{
		NanoSleep(wait);
	}
}

/** */
void CConnection::connectionState( eConnectionState e )
{
	m_pConnMutex->UnLock();

	ConnectionState(e);

	m_pConnMutex->Lock();
}

/** */
void CConnection::StateConnect()
{
	eConnectState ecs;

	m_timeConnection = time(0);

	ecs = CSocket::Connect( m_sIP, m_nPort, true );

	if ( ecs == ecsERROR )
	{
		m_eState = estDISCONNECTING;

		connectionState(estSOCKETERROR);
	}
	else if ( ecs == ecsSUCCESS )
	{
		m_eState  = estCONNECTING;
	}
}

/** */
void CConnection::StateConnecting()
{
	int err = CSocket::IsConnect();

	if ( err < 0 )
	{
		m_eState = estDISCONNECTING;

		connectionState(estSOCKETERROR);
	}
	else if ( err == 1 )
	{
		// init data timeout
		m_timeConnection = time(0);
		// init notify timer
		m_timeNotify = time(0);

		m_eState = estCONNECTED;

		if ( m_eSocketMode == esmSOCKET )
			connectionState(estCONNECTED);
		else
			connectionState(estSSLCONNECTED);
	}
	// check connection timeout
	else if ( (time(0) - m_timeConnection) >= m_nConnectTimeout )
	{
		m_eState = estDISCONNECTING;

		connectionState(estCONNECTIONTIMEOUT);
	}
}

/** */
void CConnection::StateRead()
{
	int len=1;
	int i;

	if ( m_pBuffer == 0 )
	{
		return;
	}

	for (i=0;(i<25)&&(m_eState==estCONNECTED)&&(m_bForceDisconnect==false)&&(len>0);i++)
	{
#if 0
		/* this may also be useful for testing data buffering/read issues */
		printf("CConnection::StateRead: debug mode memset\n");
		memset( m_pBuffer->Data(), '~', BUFFER_SIZE );
#endif
		len = CSocket::Read((char*)m_pBuffer->Data(),BUFFER_SIZE-1,0,1);

		if ( len < 0 )
		{
			m_eState = estDISCONNECTING;

			connectionState(estSOCKETERROR);

			break;
		}
		else if ( len > 0 )
		{
			// update timeout
			m_timeConnection = time(0);

			/* this should be completely unnecessary however it is also not wrong */
			// fix end
			m_pBuffer->Data()[len] = 0;

			// hack
			m_pConnMutex->UnLock();
#if 0
			/* test data buffering by enabling this codepath */
			printf("CConnection::StateRead: debug mode DataAvailable len=1 always\n");
			for ( int bufpos = 0; bufpos < len; ++bufpos )
			{
				DataAvailable( (char*)m_pBuffer->Data()+bufpos, 1 );
			}
#else
			DataAvailable((char*)m_pBuffer->Data(),len);
#endif
			m_pConnMutex->Lock();
		}
		else
		{
			break;
		}
	}
}

/** */
void CConnection::StateSend()
{
	int err = 0;
	CByteArray * ba = 0;

	if ( m_pSendList != 0 )
	{
		m_pSendListMutex->Lock();

		// building one big package in now done in CConnection::Write
		ba = m_pSendList->Next(0);
		
		if ( ba )
		{
			if ( ba->Size() > 0 )
			{
				err = CSocket::Write( ba->Data(), ba->Size(), 0, 1 );

				if ( (err > 0) && (err != ba->Size()) )
				{
					// TODO: make this better ;-)
					CByteArray b;
					printf("CConnection: warning send %d %ld\n",err,ba->Size());
					b.Append(ba->Data()+err,ba->Size()-err);
					ba->SetSize(0);
					ba->Append(b.Data(),b.Size());
					// add traffic control
					CSocket::m_Traffic.AddTraffic(ettCONTROLTX,err);
					// update timeout
					m_timeConnection = time(0);
				}
				else if ( err == ba->Size() )
				{
					// remove object from the list
					m_pSendList->Del(ba);
					// add traffic control
					CSocket::m_Traffic.AddTraffic(ettCONTROLTX,err);
					// update timeout
					m_timeConnection = time(0);
				}
			}
			else
			{
				printf("CConnection::StateSend: removed empty CByteArray from send list\n");
				m_pSendList->Del(ba);
			}
		}

		m_pSendListMutex->UnLock();
	}

	if ( err == -1 )
	{
		m_eState = estDISCONNECTING;

		connectionState(estSOCKETERROR);
	}
}

/** */
void CConnection::StateDisconnect()
{
	CSocket::Disconnect();

	if (m_pSendList!=0)
	{
		m_pSendListMutex->Lock();

		m_pSendList->Clear();

		m_pSendListMutex->UnLock();
	}

	m_eState = estNONE;

	connectionState(estDISCONNECTED);
}

/** */
int CConnection::Write( const unsigned char * buffer, int len, bool direct )
{
	int err = 0;

	if ( !direct )
	{
		if ( (m_pSendList != 0) &&
	    	    ((m_eState == estCONNECTED) ||
	     	     (m_eState == estCONNECTING)) )
		{
			if ( len > 0 )
			{
				m_pSendListMutex->Lock();
				
				/*
				 * Warning: CList::Prev() (previously called Forw())
				 * is not used anywhere else in dclib or valknut.
				 * Prev(0) gets the last item in the list.
				 */
				CByteArray * ba = m_pSendList->Prev(0);

				if ( ba && (ba->Size() < 1024) )
				{
					ba->Append(buffer,len);
				}
				else
				{
					ba = new CByteArray();
					ba->Append(buffer,len);
					m_pSendList->Add(ba);
				}
				
				m_pSendListMutex->UnLock();
			}
		}
	}
	else
	{
		err = CSocket::Write( buffer, len, 0, 1 );
	}

	if ( err == -1 )
	{
		m_eState = estDISCONNECTING;

		ConnectionState(estSOCKETERROR);
	}

	return err;
}
