#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 dummy = ba::placeholders;

using namespace std;
using namespace Agilent;

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

class ConnectionAgilent : public Connection
{
    boost::asio::streambuf fBuffer;

    bool fIsVerbose;
    bool fDump;

    ofstream fDumpStream;

protected:


    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;
    }

private:


    void HandleReceivedData(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;


        if (fDump)
        {
            ostringstream msg;
            msg << "--- " << Time().GetAsStr() << " --- received " << bytes_received << " bytes.";
            Dump(msg.str());
        }


        istream is(&fBuffer);

//        int status=-1;

        string buffer;
        while (getline(is, buffer, '\n'))
        {
            if (fIsVerbose)
                Out() << buffer << endl;
            if (fDump)
                Dump(buffer);

            buffer = Tools::Trim(buffer);

            if (buffer.empty())
                continue;

/*
            istringstream in(buffer);
            while (1)
            {
                float f;
                in >> f;
                if (!in)
                    break;

                resist.push_back(f);
            }
*/
        }

        vector<float> voltages;
        vector<float> currents;


/*
        if (fIsVerbose)
        {
            for (size_t i=0; i<resist.size(); i++)
                if (resist[i]>800 && resist[i]<2000)
                    Out() << setw(2) << i << " - " << setw(4) << (int)resist[i] << ": " << setprecision(1) << fixed << GetTempPT1000(resist[i]) << endl;
                else
                    Out() << setw(2) << i << " - " << setw(4) << (int)resist[i] << ": " << "----" << endl;
        }
*/
        UpdateVolt(time, voltages);
        UpdateCur( time, currents);

        StartRead();
    }

    void StartRead()
    {
        ba::async_read_until(*this, fBuffer, "\n",
                             boost::bind(&ConnectionAgilent::HandleReceivedData, this,
                                         dummy::error, dummy::bytes_transferred, 0));

        // FIXME: Add timeout here
    }

    // This is called when a connection was established
    void ConnectionEstablished()
    {
        fBuffer.prepare(10000);
        StartRead();
    }

public:
    ConnectionAgilent(ba::io_service& ioservice, MessageImp &imp) : Connection(ioservice, imp()),
        fIsVerbose(true),
        fDump(true)
    {
        SetLogStream(&imp);
    }

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

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

    void SetOutput(bool b)
    {
        if (b)
        {
            PostMessage("outp on\n");
        }
        else
        {
            PostMessage("outp off\n");
        }
    }

    void Identify()
    {
        PostMessage("*IDN?\n",6);
        PostMessage("meas:volt?\n",11);
        PostMessage("meas:curr?\n",11);
    }
};

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

#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]: FACT supply voltage"
                    "|I[A]: current consumed by the FACT camera")
    {
        // 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();

        return fAgilent.IsConnected() ? State::kConnected : 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();
    }

    int Identify()
    {
        fAgilent.Identify();
        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.");

        // 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')");

        T::AddEvent("IDENTIFY")
            (bind(&StateMachineAgilent::Identify, this))
            ("Request Agilent ID");

        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 ftmctrl controls the FSC (FACT Slow Control) board.\n"
        "\n"
        "The default is that the program is started without 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: fscctrl [-c type] [OPTIONS]\n"
        "  or:  fscctrl [OPTIONS]\n";
    cout << endl;
}

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

    /* Additional help text which is printed after the configuration
     options goes here */

    /*
     cout << "bla bla bla" << endl << endl;
     cout << endl;
     cout << "Environment:" << endl;
     cout << "environment" << endl;
     cout << endl;
     cout << "Examples:" << endl;
     cout << "test exam" << endl;
     cout << endl;
     cout << "Files:" << endl;
     cout << "files" << endl;
     cout << endl;
     */
}

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;
}
