/*
 * Copyright (C) 2007-2008 Xagasoft, All rights reserved.
 *
 * This file is part of the libbu++ library and is released under the
 * terms of the license contained in the file LICENSE.
 */

#include "bu/protocoltelnet.h"
#include "bu/client.h"

/* We apparently at least want defs for the lower 13, not sure we care about
 * the rest of the chars, maybe escape.
 */
#define CH_NUL		'\x00'	/* NUL */
#define CH_SOH		'\x01'	/* Start Of Heading */
#define CH_STX		'\x02'	/* Start of Text */
#define CH_ETX		'\x03'	/* End of Text */
#define CH_EOT		'\x04'	/* End of transmission */
#define CH_ENQ		'\x05'	/* Enquiery */
#define CH_ACK		'\x06'	/* Acknowledge */
#define CH_BEL		'\x07'	/* Bell */
#define CH_BS		'\x08'	/* Backspace */
#define CH_TAB		'\x09'	/* Horizontal Tab */
#define CH_LF		'\x0A'	/* NL Line feed, new line */
#define CH_VT		'\x0B'	/* Vertical Tab */
#define CH_FF		'\x0C'	/* Form feed, new page */
#define CH_CR		'\x0D'	/* Carriage return */
#define CH_ESC		'\x1B'	/* Escape */
#define CH_DEL		'\x7F'	/* Delete */

#define CODE_SE		'\xf0'	/* End of subnegotiation params. */
#define CODE_NOP	'\xf1'	/* No operation (keep-alive). */
#define CODE_DM		'\xf2'	/* Datastream side of a Synch. */
#define CODE_BRK	'\xf3'	/* Break character. */
#define CODE_IP		'\xf4'	/* Interrupt Process character. */
#define CODE_AO		'\xf5'	/* Abort Output character. */
#define CODE_AYT	'\xf6'	/* Are You There? character. */
#define CODE_EC		'\xf7'	/* Erase Character character. */
#define CODE_EL		'\xf8'	/* Erase Line character. */
#define CODE_GA		'\xf9'	/* Go Ahead signal. */
#define CODE_SB		'\xfa'	/* Begin subnegotiation options. */
#define CODE_WILL	'\xfb'	/* Desire to do something. */
#define CODE_WONT	'\xfc'	/* Refuse to perform. */
#define CODE_DO		'\xfd'	/* Request option. */
#define CODE_DONT	'\xfe'	/* Demand a stop. */

#define CODE_IAC	'\xff'	/* Interpret-As-Command. */

#define OPT_BINARY	'\x00'	/* Binary mode (file transfers?). */
#define OPT_ECHO	'\x01'	/* (local) Echo mode. */
#define OPT_SUPGA	'\x03'	/* Suppress Go Ahead signals. */
#define OPT_STATUS	'\x05'	/* Allow status messages. */
#define OPT_TIMING	'\x06'	/* Place a timing mark in the code. */
#define OPT_EXASCII	'\x11'	/* Extended ASCII. */
#define OPT_LOGOUT	'\x12'	/* Logout. */
#define OPT_TTYPE	'\x18'	/* Terminal Type. */
#define OPT_NAWS	'\x1f'	/* Negotiate about window size. */
#define OPT_TSPEED	'\x20'	/* Terminal Speed. */
#define OPT_NEWENV	'\x27'	/* New Environment Option. */
#define OPT_EXOPL	'\xff'	/* Can we, will we, handle extended options. */

#ifndef __TELNET_DEBUG
# define printCode( a ) (void)0
# define printOpt( a ) (void)0
#endif

Bu::ProtocolTelnet::ProtocolTelnet() :
	oBinary(	*this, OPT_BINARY ),
	oEcho(		*this, OPT_ECHO ),
	oNAWS(		*this, OPT_NAWS ),
	oSuppressGA(*this, OPT_SUPGA ),
	bCanonical( true ),
	bSubOpt( false )
{
}

Bu::ProtocolTelnet::~ProtocolTelnet()
{
}

void Bu::ProtocolTelnet::onNewConnection( Bu::Client *pClient )
{
	this->pClient = pClient;
}

void Bu::ProtocolTelnet::onNewData( Bu::Client *pClient )
{
	char bc;
	int iLeft;
	while( (iLeft = pClient->getInputSize()) )
	{
		if( bSubOpt )
		{
			pClient->peek( &bc, 1 );
			if( bc == CODE_IAC )
			{
				if( iLeft <= 1 ) return;
				char bc2;
				printCode( CODE_IAC );
				pClient->peek( &bc2, 1, 1 );
				printCode( bc2 );
				if( bc2 == CODE_SE )
				{
					bSubOpt = false;
					onSubOpt();
				}
				else if( bc2 == CODE_IAC )
				{
					sSubBuf += CODE_IAC;
				}
				else
				{
					// Error of some sort.
				}
				pClient->seek( 1 );
			}
			else
			{
				sSubBuf += bc;
			}
			pClient->seek( 1 );
		}
		else
		{
			pClient->peek( &bc, 1 );
			if( bc == CODE_IAC )
			{
				if( iLeft <= 1 ) return;
				char bc2;
				pClient->peek( &bc2, 1, 1 );
				printCode( bc );
				printCode( bc2 );

				switch( bc2 )
				{
					case CODE_WILL:
						if( iLeft <= 2 ) return;
						{
							char bc3;
							pClient->peek( &bc3, 1, 2 );
							pClient->seek( 1 );
							printOpt( bc3 );
							onWill( bc3 );
						}
						break;

					case CODE_WONT:
						if( iLeft <= 2 ) return;
						{
							char bc3;
							pClient->peek( &bc3, 1, 2 );
							pClient->seek( 1 );
							printOpt( bc3 );
							onWont( bc3 );
						}
						break;

					case CODE_DO:
						if( iLeft <= 2 ) return;
						{
							char bc3;
							pClient->peek( &bc3, 1, 2 );
							pClient->seek( 1 );
							printOpt( bc3 );
							onDo( bc3 );
						}
						break;

					case CODE_DONT:
						if( iLeft <= 2 ) return;
						{
							char bc3;
							pClient->peek( &bc3, 1, 2 );
							pClient->seek( 1 );
							printOpt( bc3 );
							onDont( bc3 );
						}
						break;

					case CODE_SB:
						if( iLeft <= 2 ) return;
						{
							pClient->peek( &cSubOpt, 1, 2 );
							pClient->seek( 1 );
							printOpt( cSubOpt );
							bSubOpt = true;
						}
						break;

					case CODE_IAC:
						sDataBuf += CODE_IAC;
						printCode( CODE_IAC );
						break;
				}
				pClient->seek( 1 );
#ifdef __TELNET_DEBUG
				printf("\n");
#endif
			}
			else if( bc == CODE_SB )
			{
			}
			else
			{
				// This is where control code handling goes
				// Also, possibly, character code conversion, although I'm not
				// sure that really matters anymore, go ASCII/UTF-8
				if( bCanonical )
				{
					if( bc < 0x20 || bc >= CH_DEL )
					{
						if( bc == CH_CR )
						{
							if( iLeft <= 1 ) return;
							char bc2;
							pClient->peek( &bc2, 1, 1 );
							if( bc2 == CH_NUL || bc2 == CH_LF )
							{
								onCtlChar( bc );
								gotLine( sDataBuf );
								sDataBuf.clear();
							}
							pClient->seek( 1 );
						}
						else
						{
							onCtlChar( bc );
						}
					}
					else
					{
						sDataBuf += bc;
						if( oEcho.isLocalSet() )
						{
							pClient->write( &bc, 1 );
#ifdef __TELNET_DEBUG
							printf("%c", bc );
							fflush( stdout );
#endif
						}
					}
				}
				else
				{
					sDataBuf += bc;
					if( oEcho.isLocalSet() )
					{
						pClient->write( &bc, 1 );
					}
				}
			}
			pClient->seek( 1 );
		}
	}

	// It's true, this code will not be executed if we only have half of an
	// IAC code or multibyte escape sequence or something, but then again, it
	// shouldn't be called then, and really, shouldn't be, it'll be called soon
	// enough, when we get the rest of that code.
	if( !bCanonical )
	{
		gotData( sDataBuf );
	}
}

void Bu::ProtocolTelnet::setCanonical( bool bCon )
{
	bCanonical = bCon;
}

bool Bu::ProtocolTelnet::isCanonical()
{
	return bCanonical;
}

void Bu::ProtocolTelnet::write( const Bu::FString &sData )
{
	write( sData.getStr(), sData.getSize() );
}

void Bu::ProtocolTelnet::write( const char *pData, int iSize )
{
	int iLast = 0, j;
	for( j = iLast; j < iSize; j++ )
	{
		if( pData[j] == '\n' )
		{
			if( j+1 >= iSize ||
				(pData[j+1] != '\r' && pData[j+1] != '\0') )
			{
				pClient->write( pData+iLast, j-iLast );
				pClient->write( "\n\r", 2 );
				iLast = j+1;
			}
			else
			{
				j++;
			}
		}
	}
	if( j > iLast )
	{
		pClient->write( pData+iLast, iSize-iLast );
	}
	//pClient->write( pData, iSize );
}

void Bu::ProtocolTelnet::write( char cData )
{
	write( &cData, 1 );
}

void Bu::ProtocolTelnet::onWill( char cCode )
{
	try
	{
		Option *pOpt = hOpts[cCode];
		if( pOpt->isRemoteEnabled() )
		{
			pOpt->fOpts |= Option::fRemoteIs;
			char buf[3] = { CODE_IAC, CODE_DO, cCode };
			pClient->write( buf, 3 );
		}
		else
		{
			char buf[3] = { CODE_IAC, CODE_DONT, cCode };
			pClient->write( buf, 3 );
		}

	}
	catch( Bu::HashException &e )
	{
		char buf[3] = { CODE_IAC, CODE_DONT, cCode };
		pClient->write( buf, 3 );
	}
}

void Bu::ProtocolTelnet::onWont( char cCode )
{
	try
	{
		Option *pOpt = hOpts[cCode];

		pOpt->fOpts &= ~Option::fRemoteIs;
		char buf[3] = { CODE_IAC, CODE_DONT, cCode };
		pClient->write( buf, 3 );
	}
	catch( Bu::HashException &e )
	{
		char buf[3] = { CODE_IAC, CODE_DONT, cCode };
		pClient->write( buf, 3 );
	}
}

void Bu::ProtocolTelnet::onDo( char cCode )
{
	try
	{
		Option *pOpt = hOpts[cCode];
		if( pOpt->isLocalEnabled() )
		{
			pOpt->fOpts |= Option::fLocalIs;
			char buf[3] = { CODE_IAC, CODE_WILL, cCode };
			pClient->write( buf, 3 );
		}
		else
		{
			char buf[3] = { CODE_IAC, CODE_WONT, cCode };
			pClient->write( buf, 3 );
		}

	}
	catch( Bu::HashException &e )
	{
		char buf[3] = { CODE_IAC, CODE_WONT, cCode };
		pClient->write( buf, 3 );
	}
}

void Bu::ProtocolTelnet::onDont( char cCode )
{
	try
	{
		Option *pOpt = hOpts[cCode];

		pOpt->fOpts &= ~Option::fLocalIs;
		char buf[3] = { CODE_IAC, CODE_DONT, cCode };
		pClient->write( buf, 3 );
	}
	catch( Bu::HashException &e )
	{
		char buf[3] = { CODE_IAC, CODE_DONT, cCode };
		pClient->write( buf, 3 );
	}
}

void Bu::ProtocolTelnet::onSubOpt()
{
	switch( cSubOpt )
	{
		case OPT_NAWS:
			{
				uint16_t iWidth, iHeight;
				((char *)&iWidth)[1] = sSubBuf[0];
				((char *)&iWidth)[0] = sSubBuf[1];
				((char *)&iHeight)[1] = sSubBuf[2];
				((char *)&iHeight)[0] = sSubBuf[3];
				onSubNAWS( iWidth, iHeight );
			}
			break;

		default:
			onSubUnknown( cSubOpt, sSubBuf );
			break;
	}

	sSubBuf.clear();
}

void Bu::ProtocolTelnet::onCtlChar( char cChr )
{
#ifdef __TELNET_DEBUG 
	switch( cChr )
	{
		case CH_NUL:	printf("NUL ");	break;
		case CH_SOH:	printf("SOH ");	break;
		case CH_STX:	printf("STX ");	break;
		case CH_ETX:	printf("ETX ");	break;
		case CH_EOT:	printf("EOT ");	break;
		case CH_ENQ:	printf("ENQ ");	break;
		case CH_ACK:	printf("ACK ");	break;
		case CH_BEL:	printf("BEL ");	break;
		case CH_BS:		printf("BS ");	break;
		case CH_TAB:	printf("TAB ");	break;
		case CH_LF:		printf("LF ");	break;
		case CH_VT:		printf("VT ");	break;
		case CH_FF:		printf("FF ");	break;
		case CH_CR:		printf("CR ");	break;
		case CH_ESC:	printf("ESC "); break;
		case CH_DEL:	printf("DEL ");	break;
		default:		printf("!![%02x] ", cChr );	break;
	}
	fflush( stdout );
#endif

	switch( cChr )
	{
		case CH_DEL:
			{
				if( sDataBuf.getSize() > 0 )
				{
					sDataBuf.resize( sDataBuf.getSize()-1 );
					char buf[3] = { CH_BS, ' ', CH_BS };
					pClient->write( buf, 3 );
				}
			}
			break;

	}
}

#ifdef __TELNET_DEBUG
void Bu::ProtocolTelnet::printCode( char cCode )
{
	switch( cCode )
	{
		case CODE_SE:		printf("SE ");		break;
		case CODE_NOP:		printf("NOP ");		break;
		case CODE_DM:		printf("DM ");		break;
		case CODE_BRK:		printf("BRK ");		break;
		case CODE_IP:		printf("IP ");		break;
		case CODE_AO:		printf("AO ");		break;
		case CODE_AYT:		printf("AYT ");		break;
		case CODE_EC:		printf("EC ");		break;
		case CODE_EL:		printf("EL ");		break;
		case CODE_GA:		printf("GA ");		break;
		case CODE_SB:		printf("SB ");		break;
		case CODE_WILL:		printf("WILL ");	break;
		case CODE_WONT:		printf("WONT ");	break;
		case CODE_DO:		printf("DO ");		break;
		case CODE_DONT:		printf("DONT ");	break;
		case CODE_IAC:		printf("IAC ");		break;
		default:			printf("??%02x ", cCode );	break;
	}
	fflush( stdout );
}

void Bu::ProtocolTelnet::printOpt( char cOpt )
{
	switch( cOpt )
	{
		case OPT_BINARY:	printf("BINARY ");	break;
		case OPT_ECHO:		printf("ECHO ");	break;
		case OPT_SUPGA:		printf("SUPGA ");	break;
		case OPT_STATUS:	printf("STATUS ");	break;
		case OPT_TIMING:	printf("TIMING ");	break;
		case OPT_EXASCII:	printf("EXASCII ");	break;
		case OPT_LOGOUT:	printf("LOGOUT ");	break;
		case OPT_TTYPE:		printf("TTYPE ");	break;
		case OPT_NAWS:		printf("NAWS ");	break;
		case OPT_TSPEED:	printf("TSPEED ");	break;
		case OPT_NEWENV:	printf("NEWENV ");	break;
		case OPT_EXOPL:		printf("EXOPL ");	break;
		default:			printf("??%02x ", cOpt);	break;
	}
	fflush( stdout );
}
#endif

Bu::ProtocolTelnet::Option::Option( Bu::ProtocolTelnet &rPT, char cCode ) :
	rPT( rPT ),
	fOpts( 0 ),
	cCode( cCode )
{
	rPT.hOpts.insert( cCode, this );
}

Bu::ProtocolTelnet::Option::~Option()
{
}

void Bu::ProtocolTelnet::Option::localEnable( bool bSet )
{
	if( bSet == (bool)(!(fOpts&fLocalCant)) ) return;

	if( bSet )
		fOpts &= ~fLocalCant;
	else
		fOpts |= fLocalCant;
}

void Bu::ProtocolTelnet::Option::localSet( bool bSet )
{
	if( bSet == (bool)(fOpts&fLocalIs) ) return;

	char buf[3] = { CODE_IAC, 0, cCode };

	if( bSet )
	{
		buf[1] = CODE_WILL;
		rPT.pClient->write( buf, 3 );
#ifdef __TELNET_DEBUG
		printf("<= ");
		rPT.printCode( buf[0] );
		rPT.printCode( buf[1] );
		rPT.printOpt( buf[2] );
		printf("\n");
#endif
	}
	else
	{
		buf[1] = CODE_WONT;
		rPT.pClient->write( buf, 3 );
#ifdef __TELNET_DEBUG
		printf("<= ");
		rPT.printCode( buf[0] );
		rPT.printCode( buf[1] );
		rPT.printOpt( buf[2] );
		printf("\n");
#endif
	}
}

bool Bu::ProtocolTelnet::Option::isLocalEnabled()
{
	return (bool)(!(fOpts&fLocalCant));
}

bool Bu::ProtocolTelnet::Option::isLocalSet()
{
	return (bool)(fOpts&fLocalIs);
}

void Bu::ProtocolTelnet::Option::remoteEnable( bool /*bSet*/ )
{
	return;
}

void Bu::ProtocolTelnet::Option::remoteSet( bool bSet )
{
	//if( bSet == (bool)(fOpts&fRemoteIs) ) return;

	char buf[3] = { CODE_IAC, 0, cCode };

	if( bSet )
	{
		buf[1] = CODE_DO;
		rPT.pClient->write( buf, 3 );
#ifdef __TELNET_DEBUG
		printf("<= ");
		rPT.printCode( buf[0] );
		rPT.printCode( buf[1] );
		rPT.printOpt( buf[2] );
		printf("\n");
#endif
	}
	else
	{
		buf[1] = CODE_DONT;
		rPT.pClient->write( buf, 3 );
#ifdef __TELNET_DEBUG
		printf("<= ");
		rPT.printCode( buf[0] );
		rPT.printCode( buf[1] );
		rPT.printOpt( buf[2] );
		printf("\n");
#endif
	}
}

bool Bu::ProtocolTelnet::Option::isRemoteEnabled()
{
	return (bool)(!(fOpts&fRemoteCant));
}

bool Bu::ProtocolTelnet::Option::isRemoteSet()
{
	return (bool)(fOpts&fRemoteIs);
}