/** \file
 *
 *  Contains the CommandLine class implementation.
 *
 *  Copyright (c) 2007,2008,2009 MBARI
 *  MBARI Proprietary Information.  All Rights Reserved
 */

#include "CommandLine.h"
#include "supervisor/Supervisor.h"

#include <unistd.h>

char CommandLine::ConsoleBuffer_[BUFF_SIZE];
int CommandLine::InputIndex_( 0 );

char CommandLine::OutputBuffer_[BUFF_SIZE];
int CommandLine::OutputIndex_( 0 );

bool CommandLine::AtMode_( false );

FILE* CommandLine::out_( stdout );
FILE* CommandLine::in_( stdin );

CommandLine* CommandLine::Instance_( NULL );

// BANG_CMD_OUTPUT_LINE_PREFIX: Each "\n" in the output from a
// banged command is replaced with this string
static const Str BANG_CMD_OUTPUT_LINE_PREFIX( "\n  " );

CommandLine::CommandLine( Supervisor& supervisor )
    : AsyncComponent( "CommandLine", NULL ),
      parsedCommandCount_( 0 ),
      supervisor_( supervisor ),
      prompt_( "" ),
      cmdHistoryIndex_( 0 ),
      gettingLines_( false )
{
    setAllowableFailures( 1 );
    setRetryTimeout( 5 );

    haveTermios_ = 0 == tcgetattr( fileno( stdin ), &originalTermios_ );

    if( haveTermios_ )
    {
        newTermios_ = originalTermios_;

        /* turn off CONICAL input and echo */
        newTermios_.c_lflag &= ~( ICANON | ECHO );

        /* set new attributes */
        tcsetattr( fileno( in_ ), TCSAFLUSH, &newTermios_ );
        fflush( in_ );
    }

    Instance_ = this;

    // Set the prompt
    if( 0 == gethostname( hostname_, MAX_HOST_NAME_SIZE - 1 ) )
    {
        char* dotAt = strchr( hostname_, '.' );
        if( NULL != dotAt && hostname_ != dotAt )
        {
            *dotAt = 0;
        }
        strncat( hostname_, ">", MAX_HOST_NAME_SIZE );
        prompt_ = hostname_;
    }
    else
    {
        prompt_ = "tethys>";
    }
}

CommandLine::~CommandLine()
{
    if( NULL != Instance_ )
    {
        Instance_ = NULL;
    }
    /* Return to normal */
    if( haveTermios_ )
    {
        tcsetattr( fileno( in_ ), TCSANOW, &originalTermios_ );
    }
}

void CommandLine::initialize()
{}

void CommandLine::uninitialize()
{}

void CommandLine::run()
{
    fflush( stdout );
    getLines();

}

bool CommandLine::isWritable()
{
    return enabled_ == true && state_ != BLOCK_COMPLETED;
}

/// writes num bytes from buffer to the output buffer/stream
OutStream& CommandLine::write( const char* buffer, size_t num )
{
    for( size_t i = 0; i < num; ++i )
    {
        OutputBuffer_[ OutputIndex_ ] = buffer[ i ];
        ++OutputIndex_;
        OutputBuffer_[ OutputIndex_ ] = 0;
        if( OutputIndex_ == BUFF_SIZE - 1 || buffer[ i ] == 10 || buffer[ i ] == 13 )
        {
            flush();
            OutputIndex_ = 0;  //TODO make Coverity not require this
        }
    }
    return *this;
}

void CommandLine::flush()
{
    // retract the command line
    for( size_t i = InputIndex_ + strlen( prompt_ ); i > 0 ; --i )
    {
        fwrite( "\b \b", 1, 3, out_ );
    }
    if( OutputIndex_ > 0 )
    {
        // print the incoming line:
        fwrite( OutputBuffer_, 1, OutputIndex_, out_ );
        if( OutputIndex_ == BUFF_SIZE - 1 )
        {
            fputc( '\n', out_ );
        }
        OutputIndex_ = 0;
        *OutputBuffer_ = 0;
    }
    // reprint the command line
    fwrite( prompt_, 1, strlen( prompt_ ), out_ );
    if( InputIndex_ > 0 )
    {
        fwrite( ConsoleBuffer_, 1, InputIndex_, out_ );
    }
    fflush( out_ );
}

/// Report if a valid arg matches the supplied arg.
/// If so, deal with it.
/// If the last word in buffer is a valid arg, deal with it.
/// If we need to toss the last character, return false.
bool CommandLine::parseBufferEnd()
{
    // First deal with total deletions, etc.
    for( int i = parsedCommandCount_ - 1; i >= 0; --i )
    {
        ParsedCommand& parsedCommand = parsedCommands_[i];
        if( parsedCommand.getStart() > InputIndex_ )
        {
            parsedCommand.clearAll();
            --parsedCommandCount_;
        }
    }

    ParsedCommand* currentCommand = parsedCommandCount_ == 0 ? NULL : &parsedCommands_[parsedCommandCount_ - 1];
    CommandArgType currentArgType = currentCommand == NULL ? ARG_NONE : currentCommand->getCurrentArgType();
    bool inQuotedString = currentCommand == NULL ? false : currentCommand->isInQuotedString();

    // Maybe we've got a new command starting up?
    if( ( InputIndex_ == 0 && parsedCommandCount_ == 0 ) ||
            ( InputIndex_ > 0
              && ConsoleBuffer_[InputIndex_ - 1] == ';'
              && ( currentCommand == NULL ||
                   ( currentArgType != ARG_STRING
                     && !inQuotedString
                     && currentCommand->getStart() < InputIndex_ ) ) ) )
    {
        parsedCommands_[parsedCommandCount_].setStart( InputIndex_ );
        ++parsedCommandCount_;
    }

    if( parsedCommandCount_ == 0 )
    {
        return false;
    }

    ParsedCommand& pCommand( parsedCommands_[parsedCommandCount_ - 1] );

    return pCommand.parseBufferEnd( CommandExec::KnownCommands(),
                                    ConsoleBuffer_ + pCommand.getStart(), InputIndex_ - pCommand.getStart() );

}

/// Normally called in response to a tab keypress.
/// If there's a parsedCommand_, expand it.
/// Otherwise expand all options and start a new line.
void CommandLine::completeArg( char* buffer, int len )
{
    bool keepLine( true );

    if( parsedCommandCount_ > 0 )
    {
        ParsedCommand& pCommand( parsedCommands_[parsedCommandCount_ - 1] );
        keepLine = pCommand.completeArg( CommandExec::KnownCommands(),
                                         this, ConsoleBuffer_ + pCommand.getStart(), InputIndex_ - pCommand.getStart() );
    }
    else
    {
        for( unsigned int i = 0; i < CommandExec::KnownCommands().size(); ++i )
        {
            Command* command = CommandExec::KnownCommands().get( i );
            const char* keyword = command->getKeyword();
            if( i != 0 )
            {
                fputc( '|', out_ );
            }
            fwrite( keyword, 1, strlen( keyword ), out_ );
        }
        keepLine = false;
    }

    if( !keepLine )
    {
        fputc( '\n', out_ );
        flush();
    }
}

void CommandLine::backspace()
{
    if( InputIndex_ > 0 )
    {
        --InputIndex_;
        fwrite( "\b \b", 1, 3, out_ );
    }
    ConsoleBuffer_[ InputIndex_ ] = 0;
}

void CommandLine::clearLine()
{
    while( InputIndex_ > 0 )
    {
        backspace();
    }
}

void CommandLine::setLine( const char* line, bool allowExec )
{
    clearLine();
    clearParsed();
    unsigned int len = strlen( line );
    bool quoteMode = false;
    size_t startAt = 0;
    for( unsigned int i = 0; i < len; ++i )
    {
        quoteMode ^= ( line[i] == '\"' );

        // Note: CR or LF would not normally occur in the argument to setLine
        // but if they do, force the left half of the command to execute.
        // If a ';' is encountered, force the left half of the command to exeucte
        // only if it starts with an 'l', which would be a load command.
        if( line[i] == '\r' || line[i] == '\n'
                || ( allowExec && !quoteMode && line[i] == ';' && line[startAt] == 'l' ) )
        {
            execLine();
            clearLine();
            clearParsed();
            // Advance startAt
            startAt = i + 1;
        }
        else
        {
            setChar( line[i] );
            // Advance startAt for ';' or leading spaces
            if( line[i] == ' ' && i == startAt )
            {
                startAt = i + 1;
            }
            else if( !quoteMode && line[i] == ';' )
            {
                startAt = i + 1;
            }
        }
    }
}

void CommandLine::setChar( const char theChar )
{
    if( InputIndex_ >= BUFF_SIZE - 1 )
    {
        return;
    }
    ConsoleBuffer_[ InputIndex_ ] = theChar;
    ConsoleBuffer_[ InputIndex_ + 1 ] = 0;
    if( Instance_->parseBufferEnd() )
    {
        ++InputIndex_;
        fputc( theChar, out_ );
    }
    ConsoleBuffer_[ InputIndex_ ] = 0;
}

void CommandLine::execLine()
{
    if( InputIndex_ > 0 )
    {
        // queue up for execution
        CommandExec::QueueCommand( ConsoleBuffer_ );

        // make some assumptions re updated the history size
        cmdHistoryIndex_ = CommandExec::GetCommandHistorySize() + 1;

        // reset the console buffer
        *ConsoleBuffer_ = 0;
        InputIndex_ = 0;
        clearParsed();
    }
    flush();
}

void CommandLine::clearParsed()
{
    while( parsedCommandCount_ > 0 )
    {
        --parsedCommandCount_;
        parsedCommands_[parsedCommandCount_].clearAll();
    }
}

void CommandLine::getLines()
{
    clearParsed();
    prompt_ = hostname_;
    fwrite( prompt_, 1, strlen( prompt_ ), out_ );
    fflush( out_ );
    int theChar = -1;
    InputIndex_ = 0;
    ConsoleBuffer_[ InputIndex_ ] = 0;
    bool escMode = false;
    bool escBracketMode = false;
    gettingLines_ = true;
    while( enabled_ )
    {
        try
        {
            theChar = kbGetc();

            switch( theChar )
            {
            case 0:
                break;
            case 0x03: // CTRL-C
                enabled_ = false;
                clearLine();
                parseBufferEnd();
                break;
            case 0x04: // CTRL-D
                clearLine();
                parseBufferEnd();
                break;
            case 0x09: // tab
                completeArg( ConsoleBuffer_, InputIndex_ );
                break;
            case 0x0A: // LF
            case 0x0D: // CR
                fputc( '\n', out_ );
                execLine();
                break;
            case 0x08: // BS
            case 0x7F: // DEL
                backspace();
                parseBufferEnd();
                break;
            case 0x1B: // escape
                escMode = !escMode;
                escBracketMode = false;
                break;
            case 0x5B: // [
                if( escMode && ! escBracketMode )
                {
                    escBracketMode = true;
                    escMode = false;
                    break;
                }
            // tell static analysis no break intended
            default:
                if( !escBracketMode && ( ( theChar > 0x1F && theChar < 0x7F ) || theChar > 0xA1 ) )
                {
                    setChar( theChar );
                }

                // user hit up or down arrow to show history
                if( escBracketMode && ( ( 0x41 == theChar ) || ( 0x42 == theChar ) ) && CommandExec::GetCommandHistorySize() )
                {
                    bool showHistory = true;
                    if( 0x41 == theChar )  // up arrow
                    {
                        if( 0 == cmdHistoryIndex_ )
                        {
                            logger_.syslog( "End of History", Syslog::INFO );
                        }
                        else
                        {
                            cmdHistoryIndex_--;
                        }
                    }
                    else // down arrow
                    {
                        cmdHistoryIndex_++;
                        if( cmdHistoryIndex_ >= CommandExec::GetCommandHistorySize() )
                        {
                            cmdHistoryIndex_ = CommandExec::GetCommandHistorySize();
                            showHistory = false;
                        }
                    }

                    const char* cmd = "";
                    if( showHistory )
                    {
                        cmd = CommandExec::GetCommandHistory( cmdHistoryIndex_ );
                    }
                    setLine( cmd, false );
                }
                escBracketMode = false;
                break;
            }
            fflush( out_ );
        }
        catch( ... ) {}
    }
}

char* CommandLine::nextWord( char** line )
{
    char * theWord = *line;
    while( **line == ' ' || **line == '\t' )
    {
        **line = 0;
        ++*line;
    }
    while( **line != 0 && **line != ' ' && **line != '\t' )
    {
        ++*line;
    }
    if( **line != 0 )
    {
        **line = 0;
        ++*line;
    }
    return theWord;
}

unsigned char CommandLine::kbGetc()
{

    fd_set read_file_descr;
    struct timeval timeout;
    unsigned char c = 0;
    int ret;

    if( AtMode_ || !haveTermios_ )
    {
        Timespan( 0.1 ).sleepFor();
        return c;
    }

    // initialize read_file_descr to be the empty set
    FD_ZERO( &read_file_descr );
    // adds fileno(stdin) to the file descriptor read_file_descr
    FD_SET( fileno( in_ ), &read_file_descr );

    timeout.tv_sec = 0;
    timeout.tv_usec = 100000;

    // select waits for specified filedescr. to have a signal
    ret = select( fileno( in_ ) + 1, &read_file_descr, NULL, NULL, &timeout );

    // If user has data, return it
    if( ret && FD_ISSET( fileno( in_ ), &read_file_descr ) )
    {
        if( 0 >= read( fileno( in_ ), &c, sizeof( c ) ) )
        {
            c = 0;
        }
        FD_CLR( fileno( in_ ), &read_file_descr );
    }

    return ( unsigned char )c;
}
