/** \file
 *
 * ESPComm class implementation.
 *
 *  Copyright (c) 2015 MBARI
 *  MBARI Proprietary Information.  All Rights Reserved
 */

#include "ESPComm.h"

#include <errno.h>


ESPComm::ESPComm( Str name,
                  const int serverSocketPort,
                  Logger& logger,
                  size_t readBufferSize )

    : name_( name ),
      serverSocketPort_( serverSocketPort ),
      socketServer_( 0 ),

      espServerAddr_( "" ),
      espServerPort_( 0 ),

      socketClient_( 0 ),

      lineReader_( 0 ),
      readBufferSize_( readBufferSize ),
      currentStream_( UNKNOWN ),

      lastResult_( "" ),
      lastResultComplete_( false ),

      lastException_( "" ),
      lastExceptionComplete_( false ),

      espState_( ESP_STATE_IDLE ),
      espStateTransitionResult_( "" ),

      lastError_( PORT_NOT_OPEN ),
      logger_( logger ),
      espLogFile_( 0 ),
      espLogSummaryLines_( 0 ),
      debug_( false )
{
}

ESPComm::ESPComm( Str name,
                  const char* espServerAddr,
                  const int espServerPort,
                  Logger& logger,
                  size_t readBufferSize )

    : name_( name ),
      serverSocketPort_( -1 ),
      socketServer_( 0 ),

      espServerAddr_( espServerAddr ),
      espServerPort_( espServerPort ),

      socketClient_( 0 ),

      lineReader_( 0 ),
      readBufferSize_( readBufferSize ),
      currentStream_( UNKNOWN ),

      lastResult_( "" ),
      lastResultComplete_( false ),

      lastException_( "" ),
      lastExceptionComplete_( false ),

      espState_( ESP_STATE_IDLE ),
      espStateTransitionResult_( "" ),

      lastError_( PORT_NOT_OPEN ),
      logger_( logger ),
      espLogFile_( 0 ),
      espLogSummaryLines_( 0 ),
      debug_( false )
{
}

ESPComm::~ESPComm()
{
    setEspLogFile( 0 );

    if( isReadable( ) )
    {
        close();
    }

    if( espLogSummaryLines_ != 0 )
    {
        ::regfree( &espLogFilterRegex_ );
        delete espLogSummaryLines_;
    }
}


ESPComm& ESPComm::open()
{
    if( isReadable() || socketServer_ != 0 )
    {
        close();
    }
    lastError_ = ESPComm::OK;
    lastErrorDetail_ = "";
    if( serverSocketPort_ >= 0 )
    {
        // we are to be tcp server, which is what we will prepare here. Later on we will wait for ESP to connect.

        if( debug_ )
        {
            Str msg = Str( "ESPComm::open: opening server socket on port " ) + serverSocketPort_;
            logDebug( msg );
        }
        try
        {
            socketServer_ = new SocketServer( serverSocketPort_ );
        }
        catch( SocketException & se )
        {
            lastError_ = ESPComm::CANNOT_OPEN_SOCKET_PORT;
            lastErrorDetail_ = Str( se.what() ) + " errno=" + se.getErrno();
        }
    }
    else
    {
        // we are tcp client and will try to connect right now:
        connectAsClient();
    }

    return *this;
}

ESPComm& ESPComm::acceptClient( int timeoutMillis )
{
    // block added to facilitate debugging/verification
    if( socketClient_ != 0 )
    {
        if( debug_ ) logDebug( "ESPComm::acceptClient: socketClient_ not null" );
        socketClient_->close();
        delete socketClient_;
        socketClient_ = 0;
        if( lineReader_ != 0 )
        {
            delete lineReader_;
            lineReader_ = 0;
        }
    }

    lastError_ = ESPComm::OK;

    if( serverSocketPort_ >= 0 )
    {
        SocketServer* testClient = new SocketServer();
        try
        {
            socketServer_->accept( *testClient, timeoutMillis );
            socketClient_ = testClient;
            if( debug_ )
            {
                logDebug( "ESPComm::acceptClient: accepted ESP as client" );
            }
        }
        catch( SocketException & se )
        {
            delete testClient;
            if( se.getErrno() == EAGAIN )
            {
                lastError_ = ESPComm::TIMEOUT_ACCEPT_CLIENT;
            }
            else
            {
                socketServer_->close();
                delete socketServer_;
                socketServer_ = 0;
                lastError_ = ESPComm::CANNOT_ACCEPT_CLIENT;
            }
            return *this;
        }
    }
    else
    {
        socketClient_ = new SocketClient( espServerAddr_.cStr(), espServerPort_ ) ;
        if( debug_ )
        {
            Str msg = "ESPComm::acceptClient: actually connected as a client to the ESP as server";
            msg += " on " + espServerAddr_ + ":" + espServerPort_;
            logDebug( msg );
        }
    }

    lineReader_ = new LineReader( socketClient_->getDescriptor(), readBufferSize_ );

    Str nameLf = name_ + "\n";
    socketClient_->sendall( nameLf.cStr(),  nameLf.length() );

    if( debug_ )
    {
        Str escaped = escape( nameLf.cStr() );
        Str msg = Str( "|>| \"" ) + escaped + "\"";
        logDebug( msg );
    }

    return *this;
}

Str ESPComm::getPeerAddress()
{
    // credit: http://beej.us/guide/bgnet/output/html/multipage/getpeernameman.html
    socklen_t len;
    struct sockaddr_storage addr;
    char ipstr[INET6_ADDRSTRLEN];

    len = sizeof addr;
    getpeername( socketClient_->getDescriptor(), ( struct sockaddr* ) &addr, &len );

    // deal with both IPv4 and IPv6:
    if( addr.ss_family == AF_INET )
    {
        struct sockaddr_in *s = ( struct sockaddr_in * ) &addr;
        inet_ntop( AF_INET, &s->sin_addr, ipstr, sizeof ipstr );
    }
    else
    {
        // AF_INET6
        struct sockaddr_in6 *s = ( struct sockaddr_in6 * ) &addr;
        inet_ntop( AF_INET6, &s->sin6_addr, ipstr, sizeof ipstr );
    }

    if( debug_ )
    {
        logDebug( Str( "getPeerAddress: '" ) + ipstr + "'" );
    }

    return Str( ipstr );
}

ESPComm& ESPComm::connectAsClient()
{
    lastError_ = ESPComm::OK;
    lastErrorDetail_ = "";

    try
    {
        socketClient_ = new SocketClient( espServerAddr_.cStr(), espServerPort_ ) ;
    }
    catch( SocketException & se )
    {
        lastError_ = ESPComm::CANNOT_CONNECT;
        lastErrorDetail_ = Str( se.what() ) + " errno=" + se.getErrno();
        return *this;
    }

    if( debug_ )
    {
        Str msg = "ESPComm::connectAsClient: connected as a client to the ESP as server";
        msg += " on " + espServerAddr_ + ":" + espServerPort_;
        logDebug( msg );
    }

    lineReader_ = new LineReader( socketClient_->getDescriptor(), readBufferSize_ );

    Str nameLf = name_ + "\n";
    socketClient_->sendall( nameLf.cStr(),  nameLf.length() );

    return *this;
}

ESPComm& ESPComm::connectAsClient( int timeoutMillis )
{
    lastError_ = ESPComm::OK;
    lastErrorDetail_ = "";

    Socket* testSocket = 0;
    try
    {
        testSocket = new SocketClient( espServerAddr_.cStr(), espServerPort_, timeoutMillis ) ;
    }
    catch( SocketException & se )
    {
        if( se.getErrno() == EAGAIN )
        {
            lastError_ = ESPComm::TIMEOUT_CONNECTING;
        }
        else
        {
            lastError_ = ESPComm::CANNOT_CONNECT;
            lastErrorDetail_ = Str( "errno=" ) + se.getErrno();
        }
    }

    if( testSocket != 0 )
    {
        socketClient_ = testSocket;
        if( debug_ )
        {
            Str msg = "ESPComm::connectAsClient: connected as a client to the ESP as server";
            msg += " on " + espServerAddr_ + ":" + espServerPort_;
            logDebug( msg );
        }

        lineReader_ = new LineReader( socketClient_->getDescriptor(), readBufferSize_ );

        Str nameLf = name_ + "\n";
        socketClient_->sendall( nameLf.cStr(),  nameLf.length() );
    }

    return *this;
}

ESPComm& ESPComm::setEspServerAddressAndPort( Str espServerAddr, const int espServerPort )
{
    espServerAddr_ = espServerAddr;
    espServerPort_ = espServerPort;

    return *this;
}

ESPComm& ESPComm::submitAbort()
{
    lastError_ = ESPComm::OK;
    lastErrorDetail_ = "";
    SocketClient* socket = new SocketClient( espServerAddr_.cStr(), espServerPort_ ) ;
    char cmd[name_.length() + 32];
    sprintf( cmd, "ESP[\"%s\"].interrupt", name_.cStr() );

    if( debug_ )
    {
        Str msg = Str( "submitAbort: issuing command: [" ) + cmd + "]";
        logDebug( msg );
    }

    // send name of the helper client:
    Str aborterName = name_ + " aborter-\n";
    socket->sendall( aborterName.cStr(),  aborterName.length() );

    // send command:
    sendLineAux( socket, cmd );

    socket->close();

    return *this;
}

ESPComm& ESPComm::close()
{
    if( socketClient_ != 0 )
    {
        int closeErr = socketClient_->close();
        if( closeErr != 0 )
        {
            this->logger_.syslog( "Client socket failed to close with err code", closeErr, Syslog::FAULT );
        }
        delete socketClient_;
        socketClient_ = 0;

        if( lineReader_ != 0 )
        {
            delete lineReader_;
            lineReader_ = 0;
        }
    }

    if( socketServer_ != 0 )
    {
        int closeErr = socketServer_->close();
        if( closeErr != 0 )
        {
            this->logger_.syslog( "Server socket failed to close with err code", closeErr, Syslog::FAULT );
        }
        delete socketServer_;
        socketServer_ = 0;
    }

    lastError_ = ESPComm::PORT_NOT_OPEN;
    return *this;
}

void ESPComm::setEspLogFile( const char* path, bool onlyLog )
{
    if( espLogFile_ != 0 )
    {
        if( debug_ ) logDebug( Str( "closing file to append ESP messages: " ) + path );
        *espLogFile_ << "##-- file closed " << Timestamp::Now().toString().cStr() << std::endl << std::endl;
        espLogFile_->close();
        delete espLogFile_;
        espLogFile_ = 0;
    }

    if( path != 0 )
    {
        if( debug_ ) logDebug( Str( "opening file to append ESP messages: " ) + path );
        espLogFile_ = new std::ofstream( path, std::ios::out | std::ios::app );
        onlyLog_ = onlyLog;
        *espLogFile_ << "##-- file opened " << Timestamp::Now().toString().cStr() << std::endl;
    }
}

void ESPComm::setEspLogFilterRegex( const char* regexString )
{
    if( espLogSummaryLines_ != 0 )  // any previous call?
    {
        ::regfree( &espLogFilterRegex_ );
        delete espLogSummaryLines_;
        espLogSummaryLines_ = 0;
    }

    if( regexString != 0 )
    {
        if( 0 == ::regcomp( &espLogFilterRegex_, regexString, REG_EXTENDED ) )
        {
            logger_.syslog( Str( "compiled regex: \"" ) + regexString + "\"", Syslog::INFO );
            espLogSummaryLines_ = new FlexArray<Str*>( true );
        }
        else
        {
            logger_.syslog( Str( "could not compile regex: \"" ) + regexString + "\"", Syslog::IMPORTANT );
        }
    }
}

FlexArray<Str*>* ESPComm::getEspLogSummaryLines()
{
    return espLogSummaryLines_;
}

void ESPComm::resetEspLogSummaryLines()
{
    if( espLogSummaryLines_ != 0 )
    {
        delete espLogSummaryLines_;
        espLogSummaryLines_ = new FlexArray<Str*>( true );
    }
}

int ESPComm::readLine( char* buffer, size_t bufferSize, EspStream *stream )
{
    int len = lineReader_->readLine( buffer, bufferSize );
    if( len > 0 && buffer[len - 1] == '\n' )
    {
        if( espLogFile_ != 0 || debug_ )
        {
            Str stream = ( Str() + "<" + StreamName( currentStream_ ) + ">" );
            Str escapedLine = escape( buffer, len - 1 );  // omit trailing newline

            if( espLogFile_ != 0 )
            {
                // include line if !onlyLog_; otherwise, if it's a <log> line,
                // include it except if it's just a stream indicator, eg: "<log> \201"
                bool includeLine = !onlyLog_ ||
                                   ( currentStream_ == LOG && ( len != 2 || !( ( ( unsigned char ) buffer[0] ) & 0200 ) ) );

                if( includeLine )
                {
                    if( !onlyLog_ )
                    {
                        // include the stream piece only when showing all lines
                        espLogFile_->width( 9 );
                        *espLogFile_ << std::right << stream.cStr() << " " << std::left;
                    }
                    *espLogFile_ << escapedLine.cStr() << std::endl;

                    // ESP Log summary handling:
                    if( espLogSummaryLines_ != 0 )
                    {
                        int rc = ::regexec( &espLogFilterRegex_, escapedLine.cStr(), 0, NULL, 0 );
                        if( 0 == rc )
                        {
                            espLogSummaryLines_->push( new Str( escapedLine ) );
                        }
                    }
                }
            }

            if( debug_ )
            {
                char prefix[256];
                sprintf( prefix, "|<| %10s %9s", EspStateName( getEspState() ), stream.cStr() );
                Str msg = Str( prefix ) + " " + escapedLine;
                logDebug( msg );
            }
        }

        bool withNewStream = false;
        EspStream newStream = UNKNOWN;  // initialize to avoid compiler warning

        if( len > 1 && ( ( unsigned char ) buffer[len - 2] & 0200 ) )
        {
            // capture new stream and remove the indicator from the reported buffer:
            newStream = ( EspStream )( unsigned char ) buffer[len - 2];
            buffer[len - 2] = '\n';
            len--;
            withNewStream = true;

            if( debug_ && newStream != currentStream_ )
            {
                char msg[128];
                sprintf( msg, "stream change: %s -> %s", StreamName( currentStream_ ), StreamName( newStream ) );
                logDebug( msg );
            }
        }

        // output currentStream_ before any possible update below:
        if( stream != 0 )
        {
            *stream = currentStream_;
        }

        bool justStreamIndicator = withNewStream && len == 1;

        if( !justStreamIndicator )
        {
            if( currentStream_ == RESULT )
            {
                lastResult_ += Str( buffer, len );
            }
            else if( currentStream_ == EXCEPTION )
            {
                lastException_ += Str( buffer, len );
            }
        }

        if( currentStream_ == STATUS )
        {
            Str lastStatus = Str( buffer, len - 1 ); // -1 to exclude \n

            if( lastStatus.length() > 0 )
            {
                const char* lastStatusCStr = lastStatus.cStr();
                char fromStateName[lastStatus.length()];
                char toStateName[lastStatus.length()];
                int parseRes = sscanf( lastStatusCStr, "%[^-]-->%[^@\n]", fromStateName, toStateName );
                if( parseRes != 2 )
                {
                    Str msg = Str( "Couldn't parse status line: '" ) + escape( lastStatusCStr ) + "'. Ignoring.";
                    logError( msg );
                }
                else
                {
                    espState_ = espStateFromReportedString( toStateName );

                    // get state transition result:
                    const char* at = strchr( lastStatusCStr, '@' );
                    if( at != 0 )
                    {
                        // exclude '\n' if any
                        const char* nl = strchr( at + 1, '\n' );
                        if( nl != 0 )
                        {
                            espStateTransitionResult_ = Str( at, nl - at );
                        }
                        else
                        {
                            espStateTransitionResult_ = Str( at );
                        }
                    }
                    else
                    {
                        espStateTransitionResult_ = "";
                    }

                    if( debug_ )
                    {
                        logDebug( Str( "got status line='" ) + escape( lastStatusCStr ) + "' " +
                                  "ESP transitioned to: " + espState_ + ": " + EspStateName( espState_ ) );
                    }
                }
            }
        }

        if( withNewStream )
        {
            // the reception of a stream indicator ...

            // ... 1) marks the completion of ongoing individual (possibly multi-line) response:
            if( currentStream_ == RESULT )
            {
                lastResultComplete_ = true;
            }
            else if( currentStream_ == EXCEPTION )
            {
                lastExceptionComplete_ = true;
            }

            // ... 2) becomes the current stream right after this line:
            currentStream_ = newStream;
        }

        if( justStreamIndicator )
        {
            return 0;  // do not notify this "special" line
        }
    }
    return len;
}

Str* ESPComm::getLastResult()
{
    Str* ret = 0;
    if( lastResultComplete_ )
    {
        ret = new Str( lastResult_ );
        lastResult_ = "";
        lastResultComplete_ = false;
    }
    return ret;
}

Str* ESPComm::getLastException()
{
    Str* ret = 0;
    if( lastExceptionComplete_ )
    {
        ret = new Str( lastException_ );
        lastException_ = "";
        lastExceptionComplete_ = false;
    }
    return ret;
}

Str ESPComm::escape( const char* buffer, size_t bufferSize )
{
    if( bufferSize == 0xFFFFFFFF )
    {
        bufferSize = ( size_t ) strlen( buffer );
    }

    Str result = "";
    char aux[64];

    for( size_t i = 0; i < bufferSize; ++i )
    {
        if( buffer[i] == '\n' )
        {
            result += "\\n";
        }
        else if( buffer[i] == '\r' )
        {
            result += "\\r";
        }
        else if( buffer[i] == '\t' )
        {
            result += "\\t";
        }
        else if( buffer[i] == '"' )
        {
            result += "\\\"";
        }
        else if( buffer[i] == '\0' || ( ( unsigned char ) buffer[i] ) & 0200 )
        {
            char aux[64];
            sprintf( aux, "\\%03o", ( unsigned char ) buffer[i] );
            result += aux;
        }
        else
        {
            sprintf( aux, "%c", buffer[i] );
            result += aux;
        }
    }

    return result;
}

int ESPComm::sendLine( const char* buffer, size_t bufferSize )
{
    return sendLineAux( socketClient_, buffer, bufferSize );
}

int ESPComm::sendLineAux( Socket* socket, const char* buffer, size_t bufferSize )
{
    if( bufferSize == 0xFFFFFFFF )
    {
        bufferSize = ( size_t ) strlen( buffer );
    }

    // +1 for inserted '\0' right before the '\n':
    size_t encodedSize = bufferSize + 1;
    if( buffer[bufferSize - 1] != '\n' )
    {
        // + 1 for added trailing '\n'
        encodedSize += 1;
    }
    char encoded[encodedSize];
    memcpy( encoded, buffer, bufferSize );
    encoded[encodedSize - 2] = '\0';
    encoded[encodedSize - 1] = '\n';
    int res = socket->sendall( encoded,  encodedSize );
    if( debug_ )
    {
        Str escaped = escape( encoded, encodedSize );
        Str msg = Str( "|>| \"" ) + escaped + "\"";
        logDebug( msg );
    }
    return res;
}


void ESPComm::logDebug( Str msg )
{
    logger_.syslog( Str( "ESPComm: " ) + msg, Syslog::DEBUG );
    //printf( "DEBUG: %s\n", msg.cStr() );
}

void ESPComm::logError( Str msg )
{
    logger_.syslog( msg, Syslog::ERROR );
    //printf( "ERROR: %s\n", msg.cStr() );
}

const char* ESPComm::errorString()
{
    switch( lastError_ )
    {
    case ESPComm::OK:
        return "no error";
    case ESPComm::CANNOT_OPEN_SOCKET_PORT:
        return "cannot open socket port";
    case ESPComm::CANNOT_ACCEPT_CLIENT:
        return "cannot accept client connection";
    case ESPComm::TIMEOUT_ACCEPT_CLIENT:
        return "timeout while accepting client connection";
    case ESPComm::PORT_NOT_OPEN:
        return "port not yet opened";
    case ESPComm::TIMEOUT_CONNECTING:
        return "timeout while trying to connect as client to the ESP";
    case ESPComm::CANNOT_CONNECT:
        return "error connecting as client to the ESP";
    }
    return "(undefined)";
};
