#include <functional>

#include "Dim.h"
#include "Event.h"
#include "Shell.h"
#include "StateMachineDim.h"
#include "Connection.h"
#include "LocalControl.h"
#include "Configuration.h"
#include "Console.h"
#include "Converter.h"

#include "tools.h"

#include "HeadersAgilent.h"

namespace ba    = boost::asio;
namespace bs    = boost::system;
namespace bapla = ba::placeholders;

using namespace std;
using namespace Agilent;

// ------------------------------------------------------------------------

class ConnectionAgilent : public Connection
{
    boost::asio::streambuf fBuffer;
    bool fIsVerbose;
    bool fDump;
    ofstream fDumpStream;
    int fState;
    float fMeasuredVoltage;
    float fMeasuredCurrent;
    

protected:

    virtual void UpdateDim(const vector<float> &)
    {
    }

    void Dump(const string &str)
    {
        if (!fDumpStream.is_open())
        {
            fDumpStream.open("socket_dump-agilent.txt", ios::app);
            if (!fDumpStream)
            {
                //ostringstream str;
                //str << "Open file " << name << ": " << strerror(errno) << " (errno=" << errno << ")";
                //Error(str);

                return;
            }
        }

        fDumpStream << str << endl;
    }
    
boost::asio::deadline_timer fCheckStatusTimer;
boost::asio::deadline_timer fAntiFloddingTimer;

void PostStatusRequest()
{
    if (IsConnected())
    {
        PostMessage(string("*IDN?\n"));
        PostMessage(string("meas:volt?\n"));
        PostMessage(string("meas:curr?\n"));
    }
}


void RequestStatus()
{
    PostStatusRequest();

    fCheckStatusTimer.expires_from_now(boost::posix_time::seconds(60));
    fCheckStatusTimer.async_wait(boost::bind(&ConnectionAgilent::HandleRequest,
                                      this, bapla::error));
}

void HandleRequest(const bs::error_code &error)
{
    // 125: Operation canceled (bs::error_code(125, bs::system_category))
    if (error && error!=ba::error::basic_errors::operation_aborted)
    {
        ostringstream str;
        str << "Write timeout of " << URL() << ": " << error.message() << " (" << error << ")";// << endl;
        Error(str);

        PostClose(false);
        return;
    }

    if (!is_open())
    {
        // For example: Here we could schedule a new accept if we
        // would not want to allow two connections at the same time.
        PostClose(true);
        return;
    }

    // Check whether the deadline has passed. We compare the deadline
    // against the current time since a new asynchronous operation
    // may have moved the deadline before this actor had a chance
    // to run.
    if (fCheckStatusTimer.expires_at() > ba::deadline_timer::traits_type::now())
        return;

    RequestStatus();
}
private:

int  fLineCounter;

void Start_async_read_until()
{
    // Bind Received Data Handler to the event: "received <enter> character"
    //   this Handler will try to parse the incoming data
    ba::async_read_until(*this, fBuffer, "\n",
                         boost::bind(&ConnectionAgilent::ReceivedStatusHandler, this,
                                     bapla::error, bapla::bytes_transferred, 0));

    // FIXME: Add timeout here
}

void ReceivedStatusHandler(const bs::error_code& err, size_t bytes_received, int /*type*/)
{
    
    // Do not schedule a new read if the connection failed.
    if (bytes_received==0 || err)
    {
        if (err==ba::error::eof)
            Warn("Connection closed by remote host (FTM).");

        // 107: Transport endpoint is not connected (bs::error_code(107, bs::system_category))
        // 125: Operation canceled
        if (err && err!=ba::error::eof &&                     // Connection closed by remote host
            err!=ba::error::basic_errors::not_connected &&    // Connection closed by remote host
            err!=ba::error::basic_errors::operation_aborted)  // Connection closed by us
        {
            ostringstream str;
            str << "Reading from " << URL() << ": " << err.message() << " (" << err << ")";// << endl;
            Error(str);
        }
        PostClose(err!=ba::error::basic_errors::operation_aborted);
        return;
    }

    if (fIsVerbose)
    {
       Out() << kBold << "Received (" << bytes_received << " bytes):" << endl;
    }
    // FIXME: the piece of code below causes a Seg Fault in case 
    // The Agilent is not listening during program start, then after a while is 
    // listening and after connection established.
    // It sends: 1.) a string and 2.) and empty line (just an <Enter> pressed)
    //
    // then the agilent_ctrl segfaults ... was able to reproduce it.
    // gdp just gave me the hint, that Time().GetAdStr() caused the Seg Fault in a way...
    // Is Time threadsafe?
    /*
    if (fDump)
    {
        ostringstream msg;
        msg << "--- " << Time().GetAsStr() << " --- received " << bytes_received << " bytes.";
        Dump(msg.str());
    }
    */


    istream is(&fBuffer);
    fLineCounter++;

    if (fLineCounter == 1)
    {
        // this is the Agilent identity string, do nothing
        string s;
        getline(is,s, '\n');
        Out() << "ID string: " << s << endl;
    }
    else if (fLineCounter == 2)
    {
        // this should be a float containing the measured voltage
        is >> fMeasuredVoltage;
        Out() << "voltage: " << fMeasuredVoltage << endl;
    }
    else if (fLineCounter >= 3)
    {
        // this should be a float containing the measured voltage
        is >> fMeasuredCurrent;
        Out() << "current: " << fMeasuredCurrent << endl;
        fLineCounter = 0;

        // data should contain two floats:
        //  * measured output voltage in volts
        //  * measured ouput current in amperes
        vector<float> data(2);
        data[0] = fMeasuredVoltage;
        data[1] = fMeasuredCurrent;
        UpdateDim(data);
    }
   
    // read the buffer empty ...
    // FIXME: I surely misunderstand that damn fBuffer thing.
    // I thought it should be emtpy by now...
    string buffer;
    while (getline(is,buffer, '\n'))
    {
        buffer = Tools::Trim(buffer);
        if (buffer.empty()) continue;
    }


    if (fMeasuredVoltage > 1.0)
    {
        fState = State::kVoltage_On;
    }
    else
    {
        fState = State::kVoltage_Off;
    }
    Start_async_read_until();
}


// This is called when a connection was established
void ConnectionEstablished()
{
    // DN 07.08.2012: The next line is imho not needed.
    // But I'm in a train and cannot test it.
    fState = State::kConnected;
    Start_async_read_until();
    RequestStatus();
    
    fLineCounter = 0;
    fBuffer.prepare(1000);
}

public:

ConnectionAgilent(ba::io_service& ioservice, MessageImp &imp) : Connection(ioservice, imp()),
    fIsVerbose(true),
    fDump(true),
    fCheckStatusTimer(ioservice),
    fAntiFloddingTimer(ioservice)
{
    SetLogStream(&imp);
    fState = State::kDisconnected;
    fMeasuredVoltage=-1;
    fMeasuredCurrent=-1;
}

void SetVerbose(bool b)
{
    fIsVerbose = b;
}

void SetDumpStream(bool b)
{
    fDump = b;
}

void SetOutput(bool b)
{
    if (b)
    {
        // check if the previous 'outp off' is some time ago
        if (fAntiFloddingTimer.expires_at() < ba::deadline_timer::traits_type::now())
        {    
            if (IsConnected()) PostMessage(string("outp on\n"));
        }
    }
    else
    {
        if (IsConnected()) PostMessage(string("outp off\n"));
        // start a Timer, which runs out in 60sec making sure, that the 
        // camera can't be switched off&on on short time scales.
        // sending repetetive outp off is possible
        // sending repetivitve outp on is also posible
        // switching off immediately after switching on is also possible.
        fAntiFloddingTimer.expires_from_now(boost::posix_time::seconds(60));
    }
    RequestStatus();
}

int GetState() const
{
    // fState is set in ReceivedStatusHandler
    return fState;
}


};

// ------------------------------------------------------------------------

#include "DimDescriptionService.h"

class ConnectionDimAgilent : public ConnectionAgilent
{
private:

DimDescribedService fDim;

void Update(DimDescribedService &svc, vector<float> data) const
{
    svc.Update(data);
}

void UpdateDim(const vector<float> &data)
{
    Update(fDim, data);
}


public:
ConnectionDimAgilent(ba::io_service& ioservice, MessageImp &imp) :
    ConnectionAgilent(ioservice, imp),
    fDim("AGILENT_CONTROL/DATA", "F:1;F:1",
                "|U[V]: output voltage"
                "|I[A]: output current")
{
    // nothing happens here.
}
};

// ------------------------------------------------------------------------

template <class T, class S>
class StateMachineAgilent : public T, public ba::io_service, public ba::io_service::work
{
private:
S fAgilent;

int Disconnect()
{
    // Close all connections
    fAgilent.PostClose(false);

    /*
     // Now wait until all connection have been closed and
     // all pending handlers have been processed
     poll();
     */

    return T::GetCurrentState();
}

int Reconnect(const EventImp &evt)
{
    // Close all connections to supress the warning in SetEndpoint
    fAgilent.PostClose(false);

    // Now wait until all connection have been closed and
    // all pending handlers have been processed
    poll();

    if (evt.GetBool())
        fAgilent.SetEndpoint(evt.GetString());

    // Now we can reopen the connection
    fAgilent.PostClose(true);

    return T::GetCurrentState();
}

int Execute()
{
    // Dispatch (execute) at most one handler from the queue. In contrary
    // to run_one(), it doesn't wait until a handler is available
    // which can be dispatched, so poll_one() might return with 0
    // handlers dispatched. The handlers are always dispatched/executed
    // synchronously, i.e. within the call to poll_one()
    poll_one();
    
    if ( fAgilent.IsConnected() )
        return fAgilent.GetState();
    else
            return State::kDisconnected;
    }

    bool CheckEventSize(size_t has, const char *name, size_t size)
    {
        if (has==size)
            return true;

        ostringstream msg;
        msg << name << " - Received event has " << has << " bytes, but expected " << size << ".";
        T::Fatal(msg);
        return false;
    }

    int SetVerbosity(const EventImp &evt)
    {
        if (!CheckEventSize(evt.GetSize(), "SetVerbosity", 1))
            return T::kSM_FatalError;

        fAgilent.SetVerbose(evt.GetBool());

        return T::GetCurrentState();
    }

    int SetDumpStream(const EventImp &evt)
    {
        if (!CheckEventSize(evt.GetSize(), "SetDumpStream", 1))
            return T::kSM_FatalError;

        fAgilent.SetDumpStream(evt.GetBool());

        return T::GetCurrentState();
    }

    int SetOutput(const EventImp &evt)
    {
        if (!CheckEventSize(evt.GetSize(), "SetOutput", 1))
            return T::kSM_FatalError;

        fAgilent.SetOutput(evt.GetBool());

        return T::GetCurrentState();
    }


public:
    StateMachineAgilent(ostream &out=cout) :
        T(out, "AGILENT_CONTROL"), ba::io_service::work(static_cast<ba::io_service&>(*this)),
        fAgilent(*this, *this)
    {
        // ba::io_service::work is a kind of keep_alive for the loop.
        // It prevents the io_service to go to stopped state, which
        // would prevent any consecutive calls to run()
        // or poll() to do nothing. reset() could also revoke to the
        // previous state but this might introduce some overhead of
        // deletion and creation of threads and more.

        // State names
        T::AddStateName(State::kDisconnected, "Disconnected",
                     "Agilent not connected via ethernet.");

        T::AddStateName(State::kConnected, "Connected",
                     "Ethernet connection to Agilent established.");

        T::AddStateName(State::kVoltage_On, "Voltage_On",
                     "The measured output voltage is higher than 1.0V");

        T::AddStateName(State::kVoltage_Off, "Voltage_Off",
                     "The measured output voltage is lower than 1.0V");

        // Verbosity commands
        T::AddEvent("SET_VERBOSE", "B:1")
            (bind(&StateMachineAgilent::SetVerbosity, this, placeholders::_1))
            ("set verbosity state"
             "|verbosity[bool]:disable or enable verbosity for received data (yes/no)");

        T::AddEvent("DUMP_STREAM", "B:1")
            (bind(&StateMachineAgilent::SetDumpStream, this, placeholders::_1))
            (""
             "");

        // Conenction commands
        T::AddEvent("DISCONNECT", State::kConnected)
            (bind(&StateMachineAgilent::Disconnect, this))
            ("disconnect from ethernet");

        T::AddEvent("RECONNECT", "O", State::kDisconnected, State::kConnected)
            (bind(&StateMachineAgilent::Reconnect, this, placeholders::_1))
            ("(Re)connect ethernet connection to Agilent, a new address can be given"
             "|[host][string]:new ethernet address in the form <host:port>");

        T::AddEvent("OUTPUT", "B:1")
            (bind(&StateMachineAgilent::SetOutput, this, placeholders::_1))
            ("set output on or off"
             "|[state][boolean]: output setting (1;0 or 'on';'off')");

        fAgilent.StartConnect();
    }

    void SetEndpoint(const string &url)
    {
        fAgilent.SetEndpoint(url);
    }

    int EvalOptions(Configuration &conf)
    {
        fAgilent.SetVerbose(!conf.Get<bool>("quiet"));

        SetEndpoint(conf.Get<string>("addr"));

        return -1;
    }
};

// ------------------------------------------------------------------------

#include "Main.h"

template<class T, class S, class R>
int RunShell(Configuration &conf)
{
    return Main::execute<T, StateMachineAgilent<S, R>>(conf);
}

void SetupConfiguration(Configuration &conf)
{
    po::options_description control("agilent_ctrl control options");
    control.add_options()
        ("no-dim",        po_bool(),  "Disable dim services")
//        ("addr,a",        var<string>("localhost:8080"),  "network address of Agilent")
        ("addr,a",        var<string>("10.0.100.220:5025"),  "network address of Agilent")
        ("quiet,q",       po_bool(true),  "Disable printing contents of all received messages (except dynamic data) in clear text.")
        ;

    conf.AddOptions(control);
}

/*
 Extract usage clause(s) [if any] for SYNOPSIS.
 Translators: "Usage" and "or" here are patterns (regular expressions) which
 are used to match the usage synopsis in program output.  An example from cp
 (GNU coreutils) which contains both strings:
  Usage: cp [OPTION]... [-T] SOURCE DEST
    or:  cp [OPTION]... SOURCE... DIRECTORY
    or:  cp [OPTION]... -t DIRECTORY SOURCE...
 */
void PrintUsage()
{
    cout <<
        "The agilentctrl controls the FACT camera power supply.\n"
        "The powersupply is made by Agilent, so we call it 'The Agilent'. \n"
        "Since FACT uses three Agilent Power Supplies with different output voltages\n"
        "one might still not now which one ist controlled by this Program, so:\n"
        "\n"
        "This program controls the 48V Agilent."
        "\n"
        "The default is that the program is started with user intercation. "
        "All actions are supposed to arrive as DimCommands. Using the -c "
        "option, a local shell can be initialized. With h or help a short "
        "help message about the usuage can be brought to the screen.\n"
        "\n"
        "Usage: agilentctrl [-c type] [OPTIONS]\n"
        "  or:  agilentctrl [OPTIONS]\n";
    cout << endl;
}

void PrintHelp()
{
    Main::PrintHelp<StateMachineAgilent<StateMachine, ConnectionAgilent>>();
}

int main(int argc, const char* argv[])
{
    Configuration conf(argv[0]);
    conf.SetPrintUsage(PrintUsage);
    Main::SetupConfiguration(conf);
    SetupConfiguration(conf);

    if (!conf.DoParse(argc, argv, PrintHelp))
        return 127;

    //try
    {
        // No console access at all
        if (!conf.Has("console"))
        {
            if (conf.Get<bool>("no-dim"))
                return RunShell<LocalStream, StateMachine, ConnectionAgilent>(conf);
            else
                return RunShell<LocalStream, StateMachineDim, ConnectionDimAgilent>(conf);
        }
        // Cosole access w/ and w/o Dim
        if (conf.Get<bool>("no-dim"))
        {
            if (conf.Get<int>("console")==0)
                return RunShell<LocalShell, StateMachine, ConnectionAgilent>(conf);
            else
                return RunShell<LocalConsole, StateMachine, ConnectionAgilent>(conf);
        }
        else
        {
            if (conf.Get<int>("console")==0)
                return RunShell<LocalShell, StateMachineDim, ConnectionDimAgilent>(conf);
            else
                return RunShell<LocalConsole, StateMachineDim, ConnectionDimAgilent>(conf);
        }
    }
    /*catch (std::exception& e)
    {
        cerr << "Exception: " << e.what() << endl;
        return -1;
    }*/

    return 0;
}
