/*
 * Copyright (C) 2007-2023 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"
#include "bu/mutexlocker.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 ),
    bEcho( true ),
    bSubOpt( false )
{
}

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

void Bu::ProtocolTelnet::onNewConnection( Bu::Client *pClient )
{
    mRead.lock();
    mWrite.lock();
    this->pClient = pClient;
    mWrite.unlock();
    mRead.unlock();
}

void Bu::ProtocolTelnet::onNewData( Bu::Client *pClient )
{
    MutexLocker l( mRead );
    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 );
                                Bu::String sLine = sDataBuf;
                                sDataBuf.clear();
                                gotLine( sLine );
                            }
                            pClient->seek( 1 );
                        }
                        else
                        {
                            onCtlChar( bc );
                        }
                    }
                    else
                    {
                        sDataBuf += bc;
                        if( oEcho.isLocalSet() && bEcho )
                        {
                            mWrite.lock();
                            pClient->write( &bc, 1 );
                            mWrite.unlock();
#ifdef __TELNET_DEBUG
                            printf("%c", bc );
                            fflush( stdout );
#endif
                        }
                    }
                }
                else
                {
                    sDataBuf += bc;
                    if( oEcho.isLocalSet() && bEcho )
                    {
                        mWrite.lock();
                        pClient->write( &bc, 1 );
                        mWrite.unlock();
                    }
                }
            }
            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 && !sDataBuf.isEmpty() )
    {
        Bu::String sDat = sDataBuf;
        sDataBuf.clear();
        gotData( sDat );
    }
}

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

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

void Bu::ProtocolTelnet::setEcho( bool bOpt )
{
    bEcho = bOpt;
}

bool Bu::ProtocolTelnet::isEchoEnabled()
{
    return bEcho;
}

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

void Bu::ProtocolTelnet::write( const char *pData, int iSize )
{
    Bu::MutexLocker l( mWrite );
    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::flush()
{
    Bu::MutexLocker l( mWrite );
    pClient->flush();
}

void Bu::ProtocolTelnet::close()
{
    Bu::MutexLocker l( mWrite );
    pClient->close();
}

void Bu::ProtocolTelnet::setBuffer( const Bu::String &sNewBuffer )
{
    sDataBuf = sNewBuffer;
}

void Bu::ProtocolTelnet::appendBuffer( const Bu::String &sNewData )
{
    sDataBuf += sNewData;
}

void Bu::ProtocolTelnet::onWill( char cCode )
{
    Bu::MutexLocker l( mWrite );
    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 )
{
    Bu::MutexLocker l( mWrite );
    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 )
{
    Bu::MutexLocker l( mWrite );
    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 )
{
    Bu::MutexLocker l( mWrite );
    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:
        case CH_BS:  // This isn't exactly standard
            {
                if( sDataBuf.getSize() > 0 )
                {
                    sDataBuf.resize( sDataBuf.getSize()-1 );
                    char buf[3] = { CH_BS, ' ', CH_BS };
                    mWrite.lock();
                    pClient->write( buf, 3 );
                    mWrite.unlock();
                }
            }
            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);
}

