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

#ifndef ESPCOMM_H_
#define ESPCOMM_H_

#include "logger/Logger.h"
#include "utils/Str.h"
#include "utils/FlexArray.h"
#include "io/LineReader.h"
#include "io/SocketServer.h"
#include "io/SocketClient.h"

#include <fstream>      // std::ofstream
#include <regex.h>

/**
 * Base socket-level interface to the ESP.
 * Besides communication establishment, provides basic operations (readLine, sendLine)
 * to support implementation of the actual ESP component.
 */
class ESPComm
{
public:

    /// Errors
    typedef enum { OK = 0,
                   CANNOT_OPEN_SOCKET_PORT,
                   TIMEOUT_ACCEPT_CLIENT,
                   CANNOT_ACCEPT_CLIENT,
                   PORT_NOT_OPEN,
                   TIMEOUT_CONNECTING,
                   CANNOT_CONNECT
                 } Error;

    ///
    /// Constructor for when the LRAUV is to act as the server
    /// in the socket communication.
    ///
    /// \param name              String to identify this instance as an ESP client
    /// \param serverSocketPort  The local port for the server socket
    /// \param logger            Logger
    /// \param readBufferSize    Passed to LineReader; 1024 by default
    ///
    ESPComm( Str name,
             const int serverSocketPort,
             Logger& logger,
             size_t readBufferSize = 1024 );

    ///
    /// Constructor for when the remote ESP acts as the server
    /// in the socket communication.
    ///
    /// \param name              String to identify this instance as an ESP client
    /// \param espServerAddr     ESP address
    /// \param espServerPort     ESP port
    /// \param logger            Logger
    /// \param readBufferSize    Passed to LineReader; 1024 by default
    ///
    ESPComm( Str name,
             const char* espServerAddr,
             const int espServerPort,
             Logger& logger,
             size_t readBufferSize = 1024 );

    /// Destructor
    ~ESPComm();

    /// Starts the server on the given port
    ESPComm& open() ;

    /// Client connection accept with timeout (0 by default)
    ESPComm& acceptClient( int timeoutMillis = 0 ) ;

    ///
    /// Gets the address of peer connected to the client socket.
    /// This is used to switch server/client roles once the ESP
    /// connects to the vehicle.
    ///
    Str getPeerAddress();

    ///
    /// Can be called when hasEspServerAddressAndPort is true.
    ///
    ESPComm& connectAsClient();

    ///
    /// Similar to connectAsClient() but with timeout.
    /// Uses constructor SocketClient( const char* host, int port, int timeoutMillis ).
    /// See SocketClient.h for relevant documentation.
    ///
    /// getError() will be ESPComm::TIMEOUT_CONNECTING if there's a connection refused
    /// error or there's a timeout connecting.
    ///
    /// \param timeoutMillis
    ///      Timeout in millis. A value of 100 or more has worked well according to testing
    ///      with client on testhyssim and server on bufflehead.
    ///
    ESPComm& connectAsClient( int timeoutMillis );

    ///
    /// Sets the address and port of the ESP server.
    /// This information can be used for later connection as a client, in particular
    /// for the submitAbort call.
    ///
    /// This method called after being connected with the vehicle as server once
    /// we know the peer address (from the socket connection) and the
    /// port, which is explicitly reported by the ESP in a first line in the connection.
    ///
    /// \param espServerAddr     ESP address
    /// \param espServerPort     ESP port
    ///
    ESPComm& setEspServerAddressAndPort( Str espServerAddr, const int espServerPort );

    ///
    /// Do we already know the address and port where the ESP is/will be running as server?
    ///
    bool hasEspServerAddressAndPort()
    {
        return espServerAddr_.length() > 0 && espServerPort_ > 0;
    }

    ///
    /// log helper: "address:port" string with the espServerAddr_ and espServerPort fields.
    ///
    Str showEspServerAddressAndPort()
    {
        Str str = espServerAddr_;
        str += ":";
        str += espServerPort_;
        return str;
    }

    ///
    /// Issues command to request the ESP to interrupt the thread associated with this
    /// client. A new socket is created to make such special "out-of-band" request.
    ///
    ESPComm& submitAbort();

    /// is a client connected?
    inline bool isClientConnected()
    {
        return socketClient_ != 0;
    }

    /// Closes underlying sockets
    ESPComm& close();

    bool isReadable()
    {
        return socketClient_ != 0;
    }

    bool eof()
    {
        return isReadable();
    }

    bool isWritable()
    {
        return socketClient_ != 0;
    }

    /// the ESP virtual streams
    enum EspStream
    {
        UNKNOWN = 0,
        RESULT     = 0201,  // the ruby eval return value stream
        OUTPUT     = 0202,  // the ruby eval output stream  (puts, etc.)
        EXCEPTION  = 0203,  // synchronous exceptions resulting from ruby cmd eval
        LOG        = 0204,  // asynchronous logging stream (messages not directly related to cmds)
        SET_PROMPT = 0205,  // set prompt string (only last line is used)
        STATE_VEC  = 0206,  // the state vector update stream (not yet implemented)
        STATUS     = 0207,  // status (state) updates
        PROMPT     = 0200,  // used like a stream identifier, but not really one. It merely causes the client to prompt for input when received
    };

    static const char* StreamName( EspStream stream )
    {
        switch( stream )
        {
        case UNKNOWN:
            return "unknown";
        case RESULT:
            return "result";
        case OUTPUT:
            return "output";
        case EXCEPTION:
            return "exception";
        case LOG:
            return "log";
        case SET_PROMPT:
            return "set_prompt";
        case STATUS:
            return "status";
        case PROMPT:
            return "prompt";
        default:
            return "unrecognized-esp-stream";
        }
    }

    // Operating ESP states:
    enum EspState
    {
        ESP_STATE_UNKNOWN,
        ESP_STATE_IDLE,
        ESP_STATE_LOADING,
        ESP_STATE_READY,
        ESP_STATE_STOPPED,
        ESP_STATE_PRIMING,
        ESP_STATE_FILTERING,
        ESP_STATE_PAUSED,
        ESP_STATE_FILTERING_ERR,
        ESP_STATE_PROCESSING,
        ESP_STATE_PROCESS_FAILED,
        ESP_STATE_PROCESSED,
        ESP_STATE_UNLOADING
    };

    static const char* EspStateName( EspState espState )
    {
        switch( espState )
        {
        case ESP_STATE_UNKNOWN:
            return "ESP_STATE_UNKNOWN";
        case ESP_STATE_IDLE:
            return "ESP_STATE_IDLE";
        case ESP_STATE_LOADING:
            return "ESP_STATE_LOADING";
        case ESP_STATE_READY:
            return "ESP_STATE_READY";
        case ESP_STATE_STOPPED:
            return "ESP_STATE_STOPPED";
        case ESP_STATE_PRIMING:
            return "ESP_STATE_PRIMING";
        case ESP_STATE_FILTERING:
            return "ESP_STATE_FILTERING";
        case ESP_STATE_PAUSED:
            return "ESP_STATE_PAUSED";
        case ESP_STATE_FILTERING_ERR:
            return "ESP_STATE_FILTERING_ERR";
        case ESP_STATE_PROCESSING:
            return "ESP_STATE_PROCESSING";
        case ESP_STATE_PROCESS_FAILED:
            return "ESP_STATE_PROCESS_FAILED";
        case ESP_STATE_PROCESSED:
            return "ESP_STATE_PROCESSED";
        case ESP_STATE_UNLOADING:
            return "ESP_STATE_UNLOADING";
        default:
            return "(invalid EspState)";
        }
    };

    EspState espStateFromReportedString( const char* str )
    {
        if( strcmp( "IDLE", str ) == 0 ) return ESP_STATE_IDLE;
        if( strcmp( "LOADING", str ) == 0 ) return ESP_STATE_LOADING;
        if( strcmp( "READY", str ) == 0 ) return ESP_STATE_READY;
        if( strcmp( "STOPPED", str ) == 0 ) return ESP_STATE_STOPPED;
        if( strcmp( "PRIMING", str ) == 0 ) return ESP_STATE_PRIMING;
        if( strcmp( "FILTERING", str ) == 0 ) return ESP_STATE_FILTERING;
        if( strcmp( "PAUSED", str ) == 0 ) return ESP_STATE_PAUSED;
        if( strcmp( "FILTERING_ERR", str ) == 0 ) return ESP_STATE_FILTERING_ERR;
        if( strcmp( "PROCESSING", str ) == 0 ) return ESP_STATE_PROCESSING;
        if( strcmp( "PROCESS_FAILED", str ) == 0 ) return ESP_STATE_PROCESS_FAILED;
        if( strcmp( "PROCESSED", str ) == 0 ) return ESP_STATE_PROCESSED;
        if( strcmp( "UNLOADING", str ) == 0 ) return ESP_STATE_UNLOADING;

        logger_.syslog( Str( "Unrecognized string for ESP state: '" ) + str + "'",
                        Syslog::ERROR );
        return ESP_STATE_UNKNOWN;
    };

    ///
    /// Received lines from the ESP are reported in the given file.
    /// Any previous file specified with this method is first closed.
    ///
    /// \param path     Path to output file. Can be 0 to cause any previously specified file to be closed.
    /// \param onlyLog  true to only report \<log> lines, that is, those coming from the EspStream::LOG
    ///                 stream; otherwise, lines from all streams are reported.
    ///                 This parameter is true by default.
    ///
    void setEspLogFile( const char* path, bool onlyLog = true );

    ///
    /// With a valid regular expression, this method enables ESP Log summary handling:
    /// ESPComm will internally keep all ESP log lines that satisfy the given
    /// regular expression. These lines are returned by getEspLogSummaryLines.
    ///
    /// \param regexString    The regular expression indicating the desired lines.
    ///                       Can be 0 to disable this handling.
    ///
    void setEspLogFilterRegex( const char* regexString );

    ///
    /// Returns all the ESP log lines in the ongoing interaction session
    /// that satisfy the regex given in the last call to setEspLogFilterRegex.
    /// This will be 0 if no such filtering is in place.
    ///
    FlexArray<Str*>* getEspLogSummaryLines();

    ///
    /// Resets the internal buffer of ESP log summary lines so an immediate
    /// next call to getEspLogSummaryLines will return a list with 0 elements.
    /// Does nothing if no ESP log summary handling is in place.
    ///
    void resetEspLogSummaryLines();

    ///
    /// Reads a line (non-blocking) from the ESP.
    ///
    /// \param buffer       output buffer
    /// \param bufferSize   size of output buffer
    /// \param stream       If not null, associated stream is stored here
    /// \return             See LineReader::readLine for documentation
    ///
    int readLine( char* buffer, size_t bufferSize, EspStream *stream = 0 ) ;

    ///
    /// Returns the current ESP operating state.
    ///
    EspState getEspState()
    {
        return espState_;
    }

    ///
    /// Returns the last "state transition result", that is, the string
    /// starting with the `@` character in the "status" virtual stream
    /// line associated with the transition that led to the current ESP
    /// operating state.
    /// For example, for `FILTERING-->PAUSED@10.0` the returned value
    /// will be `"@10.0"`.
    ///
    /// The returned string will be empty if there's no `@` included in
    /// the transition line.
    /// For example, in the `IDLE-->LOADING` transition.
    ///
    Str getEspStateTransitionResult()
    {
        return espStateTransitionResult_;
    }

    ///
    /// Returns the current ESP virtual stream.
    ///
    EspStream getCurrentStream()
    {
        return currentStream_;
    }

    ///
    /// Gets the last result received from the ESP.
    ///
    /// \return   result string; 0 if no result at all has yet been received or if a previous
    ///           result has already been reported. Otherwise the complete last result.
    ///           Note: caller is responsible for releasing (delete'ing) the returned object.
    ///
    Str* getLastResult() ;

    ///
    /// Gets the last exception received from the ESP.
    ///
    /// \return   0 if no exception at all has been received or if a previous
    ///           exception has already been reported. Otherwise the complete last exception.
    ///           Note: caller is responsible for releasing (delete'ing) the returned object.
    ///
    Str* getLastException() ;

    ///
    /// Sends a single line to the ESP.
    /// The given string may omit the trailing '\n'.
    /// The string is encoded as expected by the ESP.
    /// This "encoding" is simply the insertion of a NUL character before the '\n'.
    /// The encoded string is sent out using Socket::sendall.
    ///
    /// \param buffer      string
    /// \param bufferSize  size of buffer; if unspecified, strlen is used to determine the size
    /// \return The value returned by the internal call to Socket::sendall
    ///
    int sendLine( const char* buffer, size_t bufferSize = 0xFFFFFFFF );

    ///
    /// Debugging utility: returns a readable version of contents received from the ESP
    ///
    /// \param buffer      string
    /// \param bufferSize  size of buffer; if unspecified, strlen is used to determine the size
    /// \return readable version of contents received from the ESP
    ///
    Str escape( const char* buffer, size_t bufferSize = 0xFFFFFFFF );

    /// get last error
    inline ESPComm::Error getError()
    {
        return lastError_;
    }

    /// Additional detail in some cases when getError returns != ESPComm::OK
    inline Str getErrorDetail()
    {
        return lastErrorDetail_;
    }

    inline bool hasError()
    {
        return lastError_ != OK;
    }

    const char* errorString();

    void setDebug( bool debugOn )
    {
        debug_ = debugOn;
    }

private:

    // disallow copy constructor
    ESPComm( const ESPComm& old );

    Str name_;

    int serverSocketPort_;         // for LRAUV acting as server in socket communication
    SocketServer* socketServer_;   // the corresponding LRAUV server socket

    Str espServerAddr_;            // for ESP acting as server in socket communication
    int espServerPort_;

    Socket* socketClient_;         // the ESP client connection socket

    LineReader* lineReader_;
    size_t readBufferSize_;

    EspStream currentStream_;

    Str lastResult_;              // ongoing last result
    bool lastResultComplete_;     // is the last result now complete?

    Str lastException_;           // ongoing last exception if any
    bool lastExceptionComplete_;  // is the last exception now complete?

    EspState espState_;           // current ESP operating state
    Str espStateTransitionResult_;

    ESPComm::Error lastError_;
    Str lastErrorDetail_;

    Logger logger_;

    std::ofstream* espLogFile_;
    bool onlyLog_;

    FlexArray<Str*>* espLogSummaryLines_;
    regex_t espLogFilterRegex_;

    bool debug_;

    int sendLineAux( Socket* socket, const char* buffer, size_t bufferSize = 0xFFFFFFFF );

    // convenience methods to facilitate testing outside of the regular LRAUV framework
    void logDebug( Str msg );
    void logError( Str msg );

};

#endif /*ESPCOMM_H_*/

