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

#include "NanoDVR.h"
#include "NanoDVRIF.h"

#include "controlModule/VerticalControlIF.h"
#include "data/ConfigReader.h"
#include "data/Slate.h"
#include "data/StrValue.h"
#include "data/UniversalDataReader.h"
#include "data/UniversalDataWriter.h"
#include "units/Units.h"

#include <sstream>      // std::stringstream
#include <iomanip>      // std::setw

/* Nano DVR Mux setting for connected camera */
const unsigned int NanoDVR::NANO_DVR_MUX_NUM( 0 );


NanoDVR::NanoDVR( const Module* module )
    : SyncSensorComponent( NanoDVRIF::NAME, module ),
      loadControl_( NanoDVRIF::LOAD_CONTROL, !simulateHardware(), logger_, this ),
      uart_( NanoDVRIF::UART, NanoDVRIF::BAUD, 0.10, logger_, 4095 ),
      powerOnTimeout_( 20.0 ),
      sampleTimeout_( 720 ),
      startTime_( Timestamp::NOT_SET_TIME ),
      sampling_( false ),
      debug_( false )
{

    // Slate outputs
    samplingNanoDVRDataWriter_ = newDataWriter( NanoDVRIF::SAMPLE_NANO_DVR );

    // Configuration inputs
    sampleTimeCfgReader_ = newConfigReader( NanoDVRIF::SAMPLE_TIME_CFG );

    // This configures the advanced run modes.
    setRunState( STOP );

    deviceResponse_[0] = '\0';
    this->setAllowableFailures( 3 );
    this->setRetryTimeout( 150 );
}

NanoDVR::~NanoDVR()
{}

bool NanoDVR::readConfig( void )
{
    // Config inputs
    float sampleTimeLoc;
    bool ok = sampleTimeCfgReader_->read( Units::SECOND, sampleTimeLoc );
    sampleTimeout_ = Timespan::Seconds( sampleTimeLoc );
    return ok;
}

void NanoDVR::run()
{
    //logger_.syslog("Run", Syslog::INFO);
}

/// Do what needs to be done to run
/// Similar to initialize, in old init/run/uninit sequence
Component::RunState NanoDVR::start()
{
    if( debug_ ) logger_.syslog( "Start", Syslog::INFO );

    // Set the time
    startTime_ = Timestamp::Now();
    if( !simulateHardware() )
    {
        // Open the uart
        uart_.open();
        if( uart_.hasError() )
        {
            logger_.syslog( "Failed to open port: ", uart_.errorString(), Syslog::ERROR );
            this->setFailure( FailureMode::COMMUNICATIONS );
            return STOP;
        }
        uart_.flush();

        logger_.syslog( "Powering up", Syslog::INFO );
        if( !loadControl_.powerUp() )
        {
            logger_.syslog( "Failed to power up", Syslog::FAULT );
            this->setFailure( FailureMode::HARDWARE );
            return STOP;
        }
    }

    if( !readConfig() )
    {
        // Critical error
        logger_.syslog( Str( "Failed to load parameters in initialization routine." ), Syslog::CRITICAL );
        this->setFailure( FailureMode::DATA );
        return START;
    }
    return STARTING;
}

/// Might follow a STOP...START sequence
Component::RunState NanoDVR::starting()
{
    if( debug_ ) logger_.syslog( "Starting", Syslog::INFO );

    if( startTime_.elapsed() > powerOnTimeout_ )
    {
        if( setTime() )
        {
            startTime_ = Timestamp::Now();
            return RUNNABLE;
        }

        logger_.syslog( "NanoDVR failed to initialize.", Syslog::FAULT );
        this->setFailure( FailureMode::COMMUNICATIONS );
        return STOP;
    }

    return STARTING;
}

/// Pause for a short period (indicated by pauseTime)
Component::RunState NanoDVR::pause()
{
    // State not used at this time. Just stop.
    if( debug_ ) logger_.syslog( "Pause", Syslog::INFO );
    return STOP;
}

/// Should eventually follow a PAUSE request: should set continueTime
Component::RunState NanoDVR::paused()
{
    if( debug_ ) logger_.syslog( "Paused", Syslog::INFO );
    return STOP;
}

Component::RunState NanoDVR::resume()
{
    if( debug_ ) logger_.syslog( "Resume", Syslog::INFO );
    return STOPPED; // State not used at this time
}

Component::RunState NanoDVR::resuming()
{
    if( debug_ ) logger_.syslog( "Resuming", Syslog::INFO );
    return STOPPED; // State not used at this time
}

Component::RunState NanoDVR::runnable()
{
    if( debug_ ) logger_.syslog( "Runnable", Syslog::INFO );

    readConfig();

    // Power down if no data is requested and the device is not in the middle of a sample
    if( !isDataRequested() && !sampling_ )
    {
        // Issue power down commnad
        logger_.syslog( "Requesting power off.", Syslog::INFO );
        powerNanoOff();
        startTime_ = Timestamp::Now();
        return STOP;
    }

    if( !simulateHardware() )
    {
        // Log voltage and current and check for any faults
        logVoltageAndCurrent();
    }

    if( sampling_ )
    {
        // Check to see if the current sample is finished
        if( startTime_.elapsed() > sampleTimeout_ )
        {
            // Done for now
            logger_.syslog( "Stop recording.", Syslog::INFO );
            stopRec();
            sampling_ = false;
            samplingNanoDVRDataWriter_->write( Units::BOOL, sampling_ );
            this->resetFailCount();
        }
        // Still waiting
        else
        {
            samplingNanoDVRDataWriter_->write( Units::BOOL, sampling_ );
            return RUNNABLE;
        }
    }
    // Otherwise it's time to start a sample
    else
    {
        if( startRec() )
        {
            logger_.syslog( "Recording.", Syslog::INFO );
            startTime_ = Timestamp::Now(); // Start the clock
            sampling_ = true;
            samplingNanoDVRDataWriter_->write( Units::BOOL, sampling_ );
        }
    }

    return RUNNABLE;
}

Component::RunState NanoDVR::stop()
{
    if( debug_ ) logger_.syslog( "Stop", Syslog::INFO );

    // Wait for po timeout, then power down
    if( startTime_.elapsed() < powerOnTimeout_ )
    {
        return STOP;
    }

    if( !simulateHardware() )
    {
        logger_.syslog( "Powering down.", Syslog::INFO );
        if( !loadControl_.powerDown() )  // First power down then query for faults next cycle
        {
            logger_.syslog( "Failed to power down", Syslog::FAULT );
            this->setFailure( FailureMode::HARDWARE );
        }
        uart_.close();
    }
    sampling_ = false;
    samplingNanoDVRDataWriter_->write( Units::BOOL, sampling_ );
    return STOPPING;
}

Component::RunState NanoDVR::stopping()
{
    if( debug_ ) logger_.syslog( "Stopping", Syslog::INFO );

    if( !simulateHardware() )
    {
        loadControl_.readFaults();
        if( loadControl_.hasError() )
        {
            logger_.syslog( "LCB fault: " + loadControl_.errorString(), Syslog::FAULT );
            this->setFailure( FailureMode::HARDWARE );
        }
    }

    return STOPPED;
}

Component::RunState NanoDVR::stopped()
{
    if( debug_ ) logger_.syslog( "Stopped", Syslog::INFO );
    if( isDataRequested() )
    {
        return start();
    }

    return STOPPED;
}

/// Sets the time on the NanoDVR to the current application time
bool NanoDVR::setTime()
{
    SetTimeMsg* cmd = new SetTimeMsg();
    tm now = Timestamp::Now().asStructTm();

    cmd->hour_   = now.tm_hour;
    cmd->minute_ = now.tm_min;
    cmd->second_ = now.tm_sec;
    cmd->day_    = now.tm_mday;
    cmd->month_  = now.tm_mon + 1;
    cmd->year_   = now.tm_year - 100; // year offset since 2000 (tm has year offset since 1900...)

    cmd->header_.checksum_ = calcChecksum( ( char* )cmd, NANO_DVR_SET_TIME_MSG_SIZE );

    if( debug_ )
    {
        logger_.syslog( "Setting time:\n"
                        " hour  = " + Str( cmd->hour_ ) + ",\n"
                        " min   = " + Str( cmd->minute_ ) + ",\n"
                        " sec   = " + Str( cmd->second_ ) + ",\n"
                        " day   = " + Str( cmd->day_ ) + ",\n"
                        " month = " + Str( cmd->month_ ) + ",\n"
                        " year  = " + Str( cmd->year_ ), Syslog::INFO );
    }

    sendCmd( ( char* )cmd, NANO_DVR_SET_TIME_MSG_SIZE );
    delete cmd;

    return readNanoAck();
}

/// Commands the NanoDVR to start recording on NANO_DVR_MUX_NUM
bool NanoDVR::startRec()
{
    return setRec( true );
}

/// Commands the NanoDVR to stop recording
bool NanoDVR::stopRec()
{
    return setRec( false );
}

bool NanoDVR::setRec( bool record )
{
    SetRecMsg* cmd = new SetRecMsg();

    cmd->muxNum_ = NANO_DVR_MUX_NUM;
    cmd->rec_ = record;

    cmd->header_.checksum_ = calcChecksum( ( char* )cmd, NANO_DVR_SET_REC_MSG_SIZE );

    sendCmd( ( char* )cmd, NANO_DVR_SET_REC_MSG_SIZE );
    delete cmd;

    // The Nano seems to have a hard time acking recording commands (even
    // though they execute), so simply returning true here.
    //return readNanoAck();
    return true;
}

/// Commands the NanoDVR to power off
bool NanoDVR::powerNanoOff()
{
    PowerOffMsg* cmd = new PowerOffMsg();

    sendCmd( ( char* )cmd, NANO_DVR_POWER_OFF_MSG_SIZE );
    delete cmd;

    return readNanoAck();
}

/// Reads and parses NanoDVR Ack/Nack messages
bool NanoDVR::readNanoAck()
{
    if( simulateHardware() )
        return true;

    deviceResponse_[0] = '\0';

    // read ACK/NACK
    uart_.read( deviceResponse_, NANO_DVR_ACK_MSG_SIZE );

    // Check response
    if( uart_.hasError() )
    {
        logger_.syslog( uart_.errorString(), Syslog::ERROR );
    }
    else if( uart_.bytesRead() == NANO_DVR_ACK_MSG_SIZE )
    {
        AckMsg* ackMsg = ( AckMsg* )deviceResponse_;
        if( ackMsg->header_.checksum_ == calcChecksum( ( char* )ackMsg, NANO_DVR_ACK_MSG_SIZE ) )
        {
            if( ackMsg->ack_ == NACK )
            {
                logger_.syslog( "Nono DVR returned NACK.", Syslog::ERROR );
            }

            if( debug_ ) logger_.syslog( "Nono DVR returned " + Str( ackMsg->ack_ == ACK ? "ACK" : "NACK" ) + ".", Syslog::INFO );
            return( ackMsg->ack_ == ACK );
        }
        else
        {
            logger_.syslog( "Ack message checksum did not match.", Syslog::ERROR );
            printBufferHex( "Received:", deviceResponse_, ( unsigned )uart_.bytesRead() );
            uart_.flush();
        }
    }

    return false;
}

unsigned char NanoDVR::calcChecksum( char *msgBuffer, int msgSize )
{
    unsigned char checksum( 0 );
    for( int i = 0; i < msgSize; ++i )
    {
        // Sum all chars excluding the checksum element
        if( i != 2 )
            checksum += msgBuffer[i];
    }
    return checksum;
}

void NanoDVR::sendCmd( char* cmd, unsigned int cmdSize )
{
    if( debug_ )
        printBufferHex( "Sending: ", cmd, cmdSize, Syslog::INFO );

    if( !simulateHardware() )
        uart_.flush().write( cmd, cmdSize );
}

void NanoDVR::printBufferHex( const char * prefix, const char * buffer, unsigned int length, Syslog::Severity severity )
{
    std::stringstream ss;
    ss << prefix << " (" << length << "): ";
    for( unsigned int i = 0; i < length; ++i )
        ss << std::hex << std::setfill( '0' ) << std::setw( 2 ) << ( int )( buffer[i] ) << " ";
    std::string str = ss.str();
    const char *cstr = str.c_str();
    logger_.syslog( Str( cstr ), severity );
}


// Log voltage and current and check for any faults
void NanoDVR::logVoltageAndCurrent()
{
    loadControl_.requestVoltageAndCurrent();
    if( loadControl_.hasError() )
    {
        logger_.syslog( "LCB fault: " + loadControl_.errorString(), Syslog::FAULT );
        this->setFailure( FailureMode::HARDWARE );
        stop();
    }
}

bool NanoDVR::isDataRequested()
{
    return samplingNanoDVRDataWriter_->isDataRequested();
}

/// Should return [myNamespace]::SIMULATE_HARDWARE, or [myNamespace]::POWER, etc
ConfigURI NanoDVR::getConfigURI( ConfigOption configOption ) const
{
    return configOption == CONFIG_SIMULATE_HARDWARE ? NanoDVRIF::SIMULATE_HARDWARE : ConfigURI::NO_CONFIG_URI;
}

///////// Nano DVR message constructors /////////

/// Nano DVR message header constructor
NanoDVR::NanoHeader_def::NanoHeader_def( unsigned char msgID, unsigned char dataLength, unsigned char checksum )
    : msgSync_( SYNC ),
      msgID_( msgID ),
      checksum_( checksum ),
      dataLength_( dataLength )
{}

/// Nano DVR message header copy constructor
NanoDVR::NanoHeader_def::NanoHeader_def( const NanoHeader& header )
    : msgSync_( header.msgSync_ ),
      msgID_( header.msgID_ ),
      checksum_( header.checksum_ ),
      dataLength_( header.dataLength_ )
{}

/// Nano DVR ACK msg constructor
NanoDVR::AckMsg_def::AckMsg_def()
    : header_( 0xFF, 0x03 )
{}

/// Nano DVR power off msg constructor
NanoDVR::PowerOff_def::PowerOff_def()
    : header_( 0x01, 0x04, 0xDC ),
      magic1_( 0x0D ),
      magic2_( 0x0E ),
      magic3_( 0x0A ),
      magic4_( 0x0D )
{}

/// Nano DVR set time msg constructor
NanoDVR::SetTime_def::SetTime_def()
    : header_( 0x09, 0x06 )
{}

/// Nano DVR set recording msg constructor
NanoDVR::SetRec_def::SetRec_def()
    : header_( 0x3A, 0x02 )
{}
