#include "Prediction.h"

#include <boost/algorithm/string/join.hpp>

#include "Database.h"

#include "HeadersToO.h"
#include "HeadersGCN.h"

#include "EventImp.h"
#include "LocalControl.h"
#include "StateMachineDim.h"

#include "Dim.h"
#include "tools.h"
#include "Time.h"
#include "Configuration.h"

using namespace std;
using namespace Nova;

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

/*
void SetupConfiguration(Configuration &conf);
{
    po::options_description control("Makeschedule");
    control.add_options()
        ("uri", var<string>(), "Database link as in\n\tuser:password@server[:port]/database[?compress=0|1].")
        ;

    // po::positional_options_description p;
    // p.add("date", 1); // The first positional options

    conf.AddOptions(control);
    //conf.SetArgumentPositions(p);
}

void PrintUsage();
{
    cout <<
        "makeschedule - Creates an automatic schedule\n"
        "\n"
        "Usage: makeschedule [yyyy-mm-dd]\n";
    cout << endl;
}

void PrintHelp();
{
    cout << endl;
}*/

struct CheckVisibility
{
    // Input
    float moon_min     =  10;
    float moon_max     = 170;
    float sun_max      = -12;
    float zd_max       =  75;
    float current_max  = 110;

    // Output
    Nova::SolarObjects solarobj;
    Nova::ZdAzPosn position;

    double moon_dist   = -1;
    double current     = -1;

    bool valid_zd      = false;
    bool valid_current = false;
    bool valid_sun     = false;
    bool valid_moon    = false;

    bool visible       = false;

    void calc(const Nova::EquPosn &equ, const double &jd)
    {
        solarobj  = Nova::SolarObjects(jd);
        position  = Nova::GetHrzFromEqu(equ, jd);
        moon_dist = Nova::GetAngularSeparation(equ, solarobj.fMoonEqu);

        current   = FACT::PredictI(solarobj, equ);

        valid_moon    = moon_dist>moon_min && moon_dist<moon_max;
        valid_zd      = position.zd<zd_max;
        valid_sun     = solarobj.fSunHrz.alt<sun_max;
        valid_current = current<current_max;

        visible = valid_moon && valid_zd && valid_sun && valid_current;
    }

    CheckVisibility(const Nova::EquPosn &equ, const double &jd)
    {
        calc(equ, jd);
    }

    CheckVisibility(const Nova::EquPosn &equ)
    {
        calc(equ, Time().JD());
    }

    CheckVisibility(const double &ra, const double &dec, const double &jd)
    {
        Nova::EquPosn equ;
        equ.ra  = ra;
        equ.dec = dec;
        calc(equ, jd);
    }

    CheckVisibility(const double &ra, const double &dec)
    {
        Nova::EquPosn equ;
        equ.ra  = ra;
        equ.dec = dec;
        calc(equ, Time().JD());
    }
};

/*

 * Visibility check
 * Source eintragen
 * RELOAD SOURCES
 * Schedule eintragen
 * Send 'RESCHEDULE' interrupt

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

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

    // ------------------ Eval config ---------------------

    const string uri = conf.Get<string>("schedule-database");

    const uint16_t verbose = 1;
    const bool dry_run = true;

    Time stopwatch;

    Database connection(uri); // Keep alive while fetching rows

    cout << Time()-stopwatch << endl;

    if (verbose>0)
        cout << "Server Version: " << connection.server_version() << endl;

    const int source_key = 999;      // source_key of the source to be scheduled.

    const uint16_t contingency =   0; // The contingency to delete earlier entries from the Table
    const uint16_t duration    = 300; // The target duration of the ToO
    const uint16_t required    =  20; // If the ToO would be less than required, skip_it
    const uint16_t skip_time   =  20; // If following observation would be less than skip_time, skip it

    if (duration<required)
    {
        cerr << "Requested duration is smaller than required time." << endl;
        return 0;
    }

    Time day(2019, 1, 24, 17,  0,  0);
    Time now(2019, 1, 24, 19, 59,  0);
    Time next    = day+boost::posix_time::hours(14);
    Time restart = now+boost::posix_time::minutes(duration);

    // Safety margin
    now -= boost::posix_time::seconds(contingency);

    cout << '\n';
    cout << "Contingency:   " << contingency << " s\n";
    cout << "Start of ToO:  " << now.GetAsStr() << '\n';
    cout << "End   of ToO:  " << restart.GetAsStr() << '\n';
    cout << "Duration:      " << duration << "  min\n\n";

    cout << "Schedule:\n";

    const string queryS =
        "SELECT *,"
        " (fMeasurementTypeKey=4 AND fStart<='"+restart.GetAsStr()+"') AS `Reschedule`,"
        " (fStart>'"+restart.GetAsStr()+"') AS `Following`,"
        " (fMeasurementTypeKey=4 AND fStart BETWEEN '"+now.GetAsStr()+"' AND '"+restart.GetAsStr()+"') AS `Delete`"
        " FROM Schedule"
        " WHERE fStart BETWEEN '"+day.GetAsStr()+"' AND '"+next.GetAsStr()+"'"
        " AND fMeasurementID=0"
        " ORDER BY fStart ASC, fMeasurementID ASC";

    const mysqlpp::StoreQueryResult resS =
        connection.query(queryS).store();

    vector<string> list;

    int32_t firstdata  = -1;
    int32_t reschedule = -1;
    int32_t following  = -1;
    for (size_t i=0; i<resS.num_rows(); i++)
    {
        const mysqlpp::Row &row = resS[i];

        cout << setw(2) << i << " | ";
        cout << row["Reschedule"]     << " | ";
        cout << row["Following"]      << " | ";
        cout << row["fScheduleID"]    << " | ";
        cout << row["fStart"]         << " | ";
        if (bool(row["Delete"]))
        {
            cout << "     < delete >     | ";
            list.push_back((string)row["fScheduleID"]);
        }
        else
            cout << row["fLastUpdate"]    << " | ";
        cout << row["fMeasurementID"] << " | ";
        cout << row["fUser"]          << " | ";
        cout << row["fData"]          << " | " << setw(4);
        cout << row["fSourceKey"]     << " | ";
        cout << row["fMeasurementTypeKey"] << '\n';

        if (bool(row["Reschedule"]))
            reschedule = i;

        if (following==-1 && bool(row["Following"]))
            following = i;

        uint16_t type = row["fMeasurementTypeKey"];
        if (firstdata==-1 && type==4)
            firstdata = i;
    }

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

    if (firstdata==-1)
    {
        cerr << "No data run found." << endl;
        return 0;
    }

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

    if (reschedule>=0)
        cout << "\nLast data run before restart at " << restart.GetAsStr() << ": idx=" << reschedule << " / ID=" << resS[reschedule]["fScheduleID"] << "\n";

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

    const Time first((string)resS[firstdata]["fStart"]);          // First data run of the night
    const Time last( (string)resS[resS.num_rows()-1]["fStart"]);  // Last scheduled observation of the night

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

    if (restart<=first)
    {
        cout << "ToO ends before first data taking... skipped." << endl;
        return 0;
    }
    if (now>=last)
    {
        cout << "ToO starts after data taking... skipped." << endl;
        return 0;
    }

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

    if (now<first)
    {
        cout << "ToO starts before first data taking... rescheduling start time." << endl;
        now = first;
    }
    if (restart>last)
    {
        cout << "ToO ends after data taking... rescheduling end time!" << endl;
        restart = last;
    }

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

    if (restart<now+boost::posix_time::minutes(required))
    {
        cout << "Can not schedule more than " << required << "minutes... skipped." << endl;
        return 0;
    }

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

    if (following>=0)
    {
        const Time follow((string)resS[following]["fStart"]);
        if (follow<restart+boost::posix_time::minutes(skip_time))
        {
            cout << "Following observation would be less than " << skip_time << " min... skipping rescheduled source." << endl;
            reschedule = -1;
        }
    }

    // ====================================================================

    if (!list.empty())
    {
        cout << "\nDelete entries:\n";
        for (const auto &l : list)
            cout << l << "\n";
        cout << endl;
    }

    // ====================================================================

    vector<string> insert;

    cout << "New entries:\n";
    cout << "       | auto  | " << now.GetAsStr()     << " |       < start >     | 0 | ToO  | NULL |      | 4\n";

    insert.emplace_back("('"+now.GetAsStr()+"',0,'ToO',NULL,"+to_string(source_key)+",4)");

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

    if (reschedule>=0 && restart!=last)
    {
        cout << "       | auto  | " << restart.GetAsStr() << " |        < end >      | 0 | ToO  | NULL | " << setw(4) << resS[reschedule]["fSourceKey"] << " | 4\n";

        const string fData = (string)resS[reschedule]["fData"];
        const string fKey  = (string)resS[reschedule]["fSourceKey"];

        const string data  = resS[reschedule]["fData"] ? "'"+fData+"'" : "NULL";
        insert.emplace_back("('"+restart.GetAsStr()+"',0,'ToO',"+data+","+fKey+",4)");
    }

    // ====================================================================

    cout << Time()-stopwatch << endl;

    // ====================================================================

    if (insert.empty())
    {
        cout << "No new schedule." << endl;
        return 0;
    }

    const string queryD = string("DELETE FROM Schedule WHERE fScheduleID IN (")+boost::algorithm::join(list, ",")+")";

    const string queryI =
        "INSERT (fStart, fMeasurementID, fUser, fData, fSoureKey, fMeasurementTypeKey) "
        "VALUES\n "+string(boost::algorithm::join(insert, ",\n "));

    if (dry_run)
    {
        cout << "\n";
        if (!list.empty())
            cout << queryD << "\n\n";
        if (!insert.empty())
            cout << queryI << "\n\n";
        cout << flush;
        return 0;
    }

    // Empty interrupt to stop data taking as early as possible
    Dim::SendCommandNB("DIM_CONTROL/INTERRUPT");

    // Reload sources as early as possible
    Dim::SendCommandNB("DRIVE_CONTROL/RELOAD_SOURCES");

    // Start pointing procedure as early as possible
    //Dim::SendCommand("DRIVE_CONTROL/TRACK_WOBBLE", wobble, obs[sub].source);

    try
    {
        // ---------------------------------------------------------

        connection.query("LOCK TABLES Schedule WRITE");

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

        if (!list.empty())
        {
            const mysqlpp::SimpleResult res = connection.query(queryD).execute();
            cout << res.rows() << " row(s) deleted.\n" << endl;
        }

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

        if (!insert.empty())
        {
            auto q = connection.query(queryI);
            q.execute();
            cout << q.info() << '\n' << endl;
        }

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

        connection.query("UNLOCK TABLES");

        // ---------------------------------------------------------
    }
    catch (const exception &e)
    {
        cerr << "SQL query failed: " << e.what() << endl;
        return 7;
    }

    Dim::SendCommand("DIM_CONTROL/INTERRUPT", "reschedule");

    return 0;
}
*/
// ------------------------------------------------------------------------

class StateMachineToO : public StateMachineDim
{
private:
    string   fUri;
    Database fConnection;
    uint16_t fVerbose;
    bool     fDryRun;

    Time     fKeepAliveDeadline;
    uint16_t fKeepAliveInterval;

    bool ScheduleImp(const string &name, const double &ra, const double &dec)
    {
        fDryRun = true;

        Time stopwatch;

        // This is just a desperate try which will most likely fail
        if (!fConnection.connected())
            fConnection.reconnect();

        /*
         +-----------------+---------------------+------+-----+---------+----------------+
         | Field           | Type                | Null | Key | Default | Extra          |
         +-----------------+---------------------+------+-----+---------+----------------+
         | fSourceKEY      | int(11)             | NO   | PRI | NULL    | auto_increment |
         | fSourceName     | varchar(30)         | NO   |     | NULL    |                |
         | fRightAscension | double(10,6)        | NO   |     | NULL    |                |
         | fDeclination    | double(10,6)        | NO   |     | NULL    |                |
         | fEpochKEY       | tinyint(4)          | YES  |     | NULL    |                |
         | fFlux           | float               | YES  |     | NULL    |                |
         | fSlope          | float               | YES  |     | NULL    |                |
         | fSourceTypeKey  | int(11)             | NO   | MUL | NULL    |                |
         | fWobbleOffset   | float               | NO   |     | 0.6     |                |
         | fWobbleAngle0   | int(11)             | NO   |     | 90      |                |
         | fWobbleAngle1   | int(11)             | NO   |     | -90     |                |
         | fMagnitude      | float(4,2)          | YES  |     | NULL    |                |
         | fIsToO          | tinyint(3) unsigned | NO   |     | 0       |                |
         +-----------------+---------------------+------+-----+---------+----------------+
         */

        const double min_dist = 0.1;
        const double wobble_offset = 0.6;
        const double camera_radius = 2.3;
        const double magnitude_max = 4.5;

        double wobble_angle  = 0;

        /*
         Mag Cnt
         -2  1
         -1  3
          0  11
          1  33
          2  121
          3  321
          4  871
          5  1364
          6  404
          7  12
        */

        // The wobble position lay in the plane whihc is normal the to the plane source-center-star
        // and goes through

        const string query0 =
            "SELECT fSourceKEY, fRightAscension, fDeclination, fMagnitude,\n"
            " ADIST(fDeclination, fRightAscension*15, "+to_string(dec)+", "+to_string(ra)+") AS Dist\n"
            " FROM Source\n"
            " WHERE (fSourceTypeKey=2 OR fSourceTypeKey=3) AND fMagnitude<"+to_string(magnitude_max)+"\n"
            " HAVING Dist<"+to_string(camera_radius+wobble_offset)+"\n"
            " ORDER BY fMagnitude ASC";

        Out() << query0 << endl;

        const mysqlpp::StoreQueryResult res0 = fConnection.query(query0).store();

        Out() << Time()-stopwatch << endl;

        Info("Found "+to_string(res0.num_rows())+" stars in the camera field-of-view with magnitude less than "+to_string(magnitude_max));

        for (size_t i=0; i<::min<size_t>(10, res0.num_rows()); i++)
        {
            const mysqlpp::Row &row = res0[i];

            // TVector3 souce, star;
            // source.SetMagThetaPhi(1, rad(90-dec), rad(ra));
            // star.SetMagThetaPhi(  1, rad(90-dec), rad(ra));
            // 
            // star.RotateZ(  -source.Phi());
            // source.RotateZ(-source.Phi());
            // 
            // star.RotateY(  180-source.Theta());
            // source.RotateY(180-source.Theta());
            // 
            // rho = star.Phi();

            const double rad = M_PI/180;

            const double dec0 = rad * dec;
            const double ra0  = rad * ra;

            const double dec1 = rad * row["fDeclination"];
            const double ra1  = rad * row["fRightAscension"] * 15;

            const double s2 = sin(ra0-ra1);
            const double c2 = cos(ra0-ra1);

            const double s0 = cos(dec0);
            const double c0 = sin(dec0);

            const double c1 = cos(dec1);
            const double s1 = sin(dec1);

            const double k0 =           -s2*c1;
            const double k1 = s0*s1 - c0*c2*c1;

            const double rho = atan(k0/k1) / rad; // atan2(k0, k1)/rad

            if (i==0)
                wobble_angle = rho;

            Info(" "+Tools::Form("Mag=%5.2f  Dist=%5.1f  Phi=%6.1f", (double)row["fMagnitude"], (double)row["Dist"], rho));
        }

        if (res0.num_rows())
        {
            Info("The brightest star (M="+string(res0[0]["fMagnitude"])+") at distance is visible at a wobble angle of "+Tools::Form("%.2f", wobble_angle)+"\u00b0");
            Info("Wobble angles determined as "+Tools::Form("%.2f", wobble_angle-90)+"\u00b0 and "+Tools::Form("%.2f", wobble_angle+90)+"\u000b");
        }
        else
        {
            Info("Using default wobble angles.");
        }

        const string query1 =
            "SELECT fSourceKey, fSourceName\n"
            " FROM Source\n"
            " WHERE ADIST(fDeclination, fRightAscension*15, "+to_string(dec)+", "+to_string(ra)+")<"+to_string(min_dist)+"\n"
            " ORDER BY fSourceKey ASC";

        const mysqlpp::StoreQueryResult res1 = fConnection.query(query1).store();

        if (res1.num_rows())
        {
            Warn("The following sources in the source table have a distance less than "+to_string(min_dist)+"\u00b0");

            for (size_t i=0; i<res1.num_rows(); i++)
            {
                const mysqlpp::Row &row = res1[i];
                Warn(" "+string(row["fSourceName"])+" ["+to_string(uint32_t(row["fSourceKey"]))+"]");
            }
        }

        Out() << Time()-stopwatch << endl;

        int32_t source_key = -1;

        const string query2 =
            "SELECT fSourceKey FROM Source WHERE fSourceName='"+name+"'";

        const mysqlpp::StoreQueryResult res2 = fConnection.query(query2).store();

        if (res2.num_rows())
        {
            source_key = res2[0]["fSourceKey"];
            Info("A source with the same name (key="+to_string(source_key)+") was found in the Source table.");
        }
/*
        if (source_key<0)
        {
            const string query =
                "INSERT INTO Source\n"
                " (fSourceName, fRightAscension, fDeclination, fWobbleAngle0, fWobbleAngle1, fSourceTypeKey, fIsToO) VALUES\n"
                " ('"+name+"', "+to_string(ra/15.)+", "+to_string(dec)+", "+to_string(wobble_angle-90)+", "+to_string(wobble_angle+90)+", 1, 1)";

            if (!fDryRun)
            {
                auto q = fConnection.query(query);
                q.execute();
                Info(q.info());

                source_key = q.insert_id();

                Info(string(fDryRun?"[dry-run] ":"")+"The new source got the key "+to_string(source_key));
            }
            else
                Out() << query << endl;

        }
*/
        Out() << Time()-stopwatch << endl;

        /*
         +---------------------+--------------+------+-----+-------------------+-----------------------------+
         | Field               | Type         | Null | Key | Default           | Extra                       |
         +---------------------+--------------+------+-----+-------------------+-----------------------------+
         | fScheduleID         | int(11)      | NO   | PRI | NULL              | auto_increment              |
         | fStart              | datetime     | NO   | MUL | NULL              |                             |
         | fLastUpdate         | timestamp    | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
         | fMeasurementID      | int(11)      | NO   |     | NULL              |                             |
         | fUser               | varchar(32)  | NO   |     | NULL              |                             |
         | fData               | varchar(256) | YES  |     | NULL              |                             |
         | fSourceKey          | smallint(6)  | YES  |     | NULL              |                             |
         | fMeasurementTypeKey | tinyint(4)   | NO   |     | NULL              |                             |
         +---------------------+--------------+------+-----+-------------------+-----------------------------+
         */

        const uint16_t contingency =   1; // The contingency to delete earlier entries from the Table
        const uint16_t duration    =  60; // The target duration of the ToO
        const uint16_t required    =  20; // If the ToO would be less than required, skip_it
        const uint16_t skip_time   =  20; // If following observation would be less than skip_time, skip it

        if (duration<required)
        {
            Error("Requested duration is smaller than required time.");
            return false;
        }

        const Time now;

        const Time sunset  = now.GetPrevSunSet();
        const Time sunrise = now.GetNextSunRise();

        if (sunset>sunrise || sunrise>sunset+boost::posix_time::hours(24))
        {
            Error("Unable to schedule a ToO during daytime.");
            return false;
        }

        // Safety margin
        Time schedtime = now-boost::posix_time::seconds(contingency);
        Time restart   = now+boost::posix_time::minutes(duration);

        Out() << '\n';
        Out() << "Contingency:   " << contingency << " s\n";
        Out() << "Start of ToO:  " << now.GetAsStr() << '\n';
        Out() << "End   of ToO:  " << restart.GetAsStr() << '\n';
        Out() << "Duration:      " << duration << " min\n\n";

        Out() << "Schedule:\n";

        const string queryS =
            "SELECT *,\n"
            " (fMeasurementTypeKey=4 AND fStart<='"+restart.GetAsStr()+"') AS `Reschedule`,\n"
            " (fStart>'"+restart.GetAsStr()+"') AS `Following`,\n"
            " (fMeasurementTypeKey=4 AND fStart BETWEEN '"+schedtime.GetAsStr()+"' AND '"+restart.GetAsStr()+"') AS `Delete`\n"
            " FROM Schedule\n"
            " WHERE fStart BETWEEN '"+sunset.GetAsStr()+"' AND '"+sunrise.GetAsStr()+"'\n"
            " AND fMeasurementID=0\n"
            " ORDER BY fStart ASC, fMeasurementID ASC";

        const mysqlpp::StoreQueryResult resS = fConnection.query(queryS).store();

        if (resS.num_rows()<2)
        {
            Error("Available schedule is too short to be evaluated.");
            return false;
        }

        vector<string> list;

        int32_t firstdata  = -1;
        int32_t reschedule = -1;
        int32_t following  = -1;
        for (size_t i=0; i<resS.num_rows(); i++)
        {
            const mysqlpp::Row &row = resS[i];

            Out() << setw(2) << i << " | ";
            Out() << row["Reschedule"]     << " | ";
            Out() << row["Following"]      << " | ";
            Out() << row["fScheduleID"]    << " | ";
            Out() << row["fStart"]         << " | ";
            if (bool(row["Delete"]))
            {
                Out() << "     < delete >     | ";
                list.push_back((string)row["fScheduleID"]);
            }
            else
                Out() << row["fLastUpdate"]    << " | ";
            Out() << row["fMeasurementID"] << " | ";
            Out() << row["fUser"]          << " | ";
            Out() << row["fData"]          << " | " << setw(4);
            Out() << row["fSourceKey"]     << " | ";
            Out() << row["fMeasurementTypeKey"] << '\n';

            if (bool(row["Reschedule"]))
                reschedule = i;

            if (following==-1 && bool(row["Following"]))
                following = i;

            const uint16_t type = row["fMeasurementTypeKey"];
            if (firstdata==-1 && type==4)
                firstdata = i;
        }

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

        if (firstdata==-1)
        {
            Warn("No data run found.");
            return false;
        }

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

        if (reschedule>=0)
            Out() << "\nLast data run before restart at " << restart.GetAsStr() << ": idx=" << reschedule << " / ID=" << resS[reschedule]["fScheduleID"] << endl;

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

        const Time first((string)resS[firstdata]["fStart"]);          // First data run of the night
        const Time last( (string)resS[resS.num_rows()-1]["fStart"]);  // Last scheduled observation of the night

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

        if (restart<=first)
        {
            Warn("ToO ends before first data taking... skipped.");
            return false;
        }
        if (schedtime>=last)
        {
            Warn("ToO starts after data taking... skipped.");
            return false;
        }

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

        if (schedtime<first)
        {
            Info("ToO starts before first data taking... rescheduling start time.");
            schedtime = first;
        }
        if (restart>last)
        {
            Info("ToO ends after data taking... rescheduling end time!");
            restart = last;
        }

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

        if (restart<schedtime+boost::posix_time::minutes(required))
        {
            Warn("Could not schedule more than the required "+to_string(required)+" minutes... skipped.");
            return false;
        }

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

        if (following>=0)
        {
            const Time follow((string)resS[following]["fStart"]);
            if (follow<restart+boost::posix_time::minutes(skip_time))
            {
                Info("Following observation would be less than "+to_string(skip_time)+" min... skipping rescheduled source.");
                reschedule = -1;
            }
        }

        // ====================================================================

        if (!list.empty())
        {
            Out() << "\nDelete entries:\n";
            for (const auto &l : list)
                Out() << l << "\n";
            Out() << endl;
        }

        // ====================================================================

        Info("Source will be scheduled.");

        if (source_key<0)
        {
            const string query =
                "INSERT INTO Source\n"
                " (fSourceName, fRightAscension, fDeclination, fWobbleAngle0, fWobbleAngle1, fSourceTypeKey, fIsToO) VALUES\n"
                " ('"+name+"', "+to_string(ra/15.)+", "+to_string(dec)+", "+to_string(wobble_angle-90)+", "+to_string(wobble_angle+90)+", 1, 1)";

            if (!fDryRun)
            {
                auto q = fConnection.query(query);
                q.execute();
                Info(q.info());

                source_key = q.insert_id();

                Info(string(fDryRun?"[dry-run] ":"")+"The new source got the key "+to_string(source_key));
            }
            else
                Out() << query << endl;

        }

        Out() << Time()-stopwatch << endl;

        // ====================================================================

        vector<string> insert;

        Out() << "New entries:\n";
        Out() << "           | auto  | " << schedtime.GetAsStr()     << " |       < start >     | 0 | ToO  | nodrs,grb | " << setw(4) << source_key << " | 4\n";

        insert.emplace_back("('"+schedtime.GetAsStr()+"',0,'ToO','nodrs:true,grb:true',"+to_string(source_key)+",4)");

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

        if (reschedule>=0 && restart!=last)
        {
            Out() << "           | auto  | " << restart.GetAsStr() << " |        < end >      | 0 | ToO  |   NULL    | " << setw(4) << resS[reschedule]["fSourceKey"] << " | 4\n";

            const string fData = (string)resS[reschedule]["fData"];
            const string fKey  = (string)resS[reschedule]["fSourceKey"];

            const string data  = resS[reschedule]["fData"] ? "'"+fData+"'" : "NULL";
            insert.emplace_back("('"+restart.GetAsStr()+"',0,'ToO',"+data+","+fKey+",4)");
        }

        // ====================================================================

        Out() << Time()-stopwatch << endl;

        // ====================================================================

        const string queryD = string("DELETE FROM Schedule WHERE fScheduleID IN (")+boost::algorithm::join(list, ",")+")";

        const string queryI =
            "INSERT INTO Schedule\n (fStart,fMeasurementID,fUser,fData,fSoureKey,fMeasurementTypeKey)\n"
            "VALUES\n "+string(boost::algorithm::join(insert, ",\n "));

        if (fDryRun)
        {
            Out() << "\n";
            if (!list.empty())
                Out() << queryD << "\n\n";
            if (!insert.empty())
                Out() << queryI << "\n\n";
            Out() << flush;
            return false;
        }

        // Empty interrupt to stop data taking as early as possible
        Dim::SendCommandNB("DIM_CONTROL/INTERRUPT");

        // Reload sources as early as possible
        Dim::SendCommandNB("DRIVE_CONTROL/RELOAD_SOURCES");

        // Start pointing procedure as early as possible
        //Dim::SendCommand("DRIVE_CONTROL/TRACK_WOBBLE", wobble, obs[sub].source);

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

        fConnection.query("LOCK TABLES Schedule WRITE");

        // ---------------------------------------------------------
        /*
         if (!list.empty())
         {
         const mysqlpp::SimpleResult res = fConnection.query(queryD).execute();
         Info(to_string(res.rows())+" row(s) deleted from Schedule.");
         }

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

         if (!insert.empty())
         {
         auto q = fConnection.query(queryI);
         q.execute();
         Info(q.info());
         }
         */
        // ---------------------------------------------------------

        fConnection.query("UNLOCK TABLES");

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

        Dim::SendCommand("DIM_CONTROL/INTERRUPT", "reschedule");

        ostringstream out;
        out << Time()-stopwatch;
        Info(out);

        return true;
    }

    bool Schedule(const string &name, const double &ra, const double &dec)
    {
        try
        {
            return ScheduleImp(name, ra, dec);
        }
        catch (const exception &e)
        {
            Error(string("SQL query failed: ")+e.what());
            fConnection.disconnect();
            return false;
        }
    }

    // ========================================================================


    // ========================================================================

    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 << ".";
        Fatal(msg);
        return false;
    }

    int ScheduleGCN(const EventImp &evt)
    {
        if (!CheckEventSize(evt.GetSize(), "ScheduleGCN", 2+4+3*8))
            return kSM_FatalError;

        const ToO::DataGRB &grb = evt.Ref<ToO::DataGRB>();

        const GCN::PaketType_t *ptr=GCN::kTypes;
        for (; ptr->type>=0 && ptr->type!=grb.type; ptr++);
        if (ptr->type==-1)
        {
            Warn("Unknown Packet Type: "+to_string(ptr->type));
            return GetCurrentState();
        }

        ostringstream out;
        out << "Received: '" << ptr->name << "' ID=" << grb.type << " NUM=" << grb.trigid << " RA=" << grb.ra/15. << "h DEC=" << grb.dec << "\u00b0 ERR=" << grb.err;

        Info(out);
        Info(ptr->description);

        const CheckVisibility check(grb.ra, grb.dec);

        Info(string("Source is")+(check.visible?" ":" NOT ")+"visible.");

        Info(string("Sun altitude:   ")+(check.valid_sun    ?"OK    ":"failed")+" ["+Tools::Form("%5.1f", check.solarobj.fSunHrz.alt)+"\u00b0]");
        Info(string("Moon distance:  ")+(check.valid_moon   ?"OK    ":"failed")+" ["+Tools::Form("%5.1f", check.moon_dist)+"\u00b0]");
        Info(string("Zenith angle:   ")+(check.valid_zd     ?"OK    ":"failed")+" ["+Tools::Form("%5.1f", check.position.zd)+"\u00b0]");
        Info(string("Current:        ")+(check.valid_current?"OK    ":"failed")+" ["+Tools::Form("%5.1f", check.current)+" \u00b5A]");

        const string name = ptr->instrument+"#"+to_string(grb.trigid);
        Info("Source name set to '"+name+"'");

        /*
         * Swift: https://gcn.gsfc.nasa.gov/swift.html
         relevante Paket-Typen:
         60   BAT_Alert
         97   BAT_QL_Pos
         61   BAT_Pos
         62   BAT_Pos_Nack
         Es kann wohl vorkommen, dass ein Alert danach anders klassifiziert ist
         -> BAT_Trans (84) und es steht dass dann in der GRB notice eine catalog
         ID im comment steht. Da ist es mir noch nicht so ganz klar, woran man
         echte Alerts erkennt.

         * Fermi: https://gcn.gsfc.nasa.gov/fermi.html
         relevante Paket-Typen:
         110   GBM_Alert           P        B               B                B&A
         111   GBM_Flt_Pos         P        B               B                B&A
         112   GBM_Gnd_Pos         P        B               B                B&A
         115   GBM_Final_Pos       P        B               B                B&A
         Bei GBM müssen wir usn überlegen wie wir es mit den Positionsupdates
         machen - das können auch mal ein paar Grad sein. siehe zB
         https://gcn.gsfc.nasa.gov/other/568833894.fermi

         Wenn ich das richtig sehe, kommt bei GBM_Alert noch keine
         Position, sndern erst mit GBM_Flt_Pos (in dem Bsp kommt das 3 Sek
         später)

         * INTEGRAL: https://gcn.gsfc.nasa.gov/integral.html
         wenn ich es richtig verstehe, sind die interessanten Alerts die WAKEUP
         (tw kommt ein Positionsupdate via OFFLINE oder REFINED). Es gibt auch
         noch WEAK, aber das sind scheinbar subthreshold alerts - würde ich
         jetzt erst mal weglassen. Im letzten Jahr gab es 8 WAKEUP alerts. 2017
         sogar nur 3, 2016 7, 2015 waren es einige mehr, aber das war V404_Cyg -
         in dem Fall steht dann aber ein Object-Name dabei - ansonsten steht as
         "Possbibly real GRB event" - zT steht auch, dass es coincident mit GBM
         events ist

         * KONUS: https://gcn.gsfc.nasa.gov/konus.html
         Ist mir noch etwas unklar, was das genau ist - die schicken Lichtkurven
         von GRBs und Transients - muss man nochmal genauer schauen.

         * MAXI: https://gcn.gsfc.nasa.gov/maxi.html
         wenn ich das richtig verstehe, sind das nicht nur GRBs -> müsste man
         nochmal genauer schauen bevor man da was implementiert

         * AGILE: https://gcn.gsfc.nasa.gov/agile.html
         Wahrscheinlich eher nicht relevant, da steht was von 30min nach dem
         Burst kommt das 'Wakeup'. Die relevanten Paket-Typen wären 100
         (Wakeup_Pos), 101 (Ground_Pos) und 102 (Refined_Pos)

         * AMON: https://gcn.gsfc.nasa.gov/amon.html
         sind bisher nur neutrinos - da macht MAGIC glaub ich schon automatic
         Follow-Up - müssen wir also kucken was Sinn macht

         * Ligo-Virgo GW: https://gcn.gsfc.nasa.gov/lvc.html
         da ist natürlich die Error-Region groß - müsste man schauen, was man
         da will

         Fazit: am relevantesten sind Fermi/GBM und Swift/BAT. Evtl könnte
         INTEGRAL noch Sinn machen und über AMON und LVC sollte man nachdenken,
         ob/was man haben will.
*/

        Schedule(name, grb.ra, grb.dec);

        return fConnection.connected() ? ToO::State::kConnected : ToO::State::kDisconnected;
        //return GetCurrentState();
    }

    int Execute()
    {
        Time now;
        if (now>fKeepAliveDeadline)
        {
            static int ernum = 0;
            try
            {
                // Unfortunately, reconnecting [Takes about 1s] is
                // the only non-blocking way to ensure an open
                // connection. Ping would be nice but is blocking.
                fConnection = Database(fUri);
                ernum = 0;
            }
            catch (const mysqlpp::ConnectionFailed &e)
            {
                if (ernum!=e.errnum())
                    Error(e.what());
                ernum = e.errnum();
            }

            fKeepAliveDeadline += boost::posix_time::seconds(fKeepAliveInterval);
        }

        return fConnection.connected() ? ToO::State::kConnected : ToO::State::kDisconnected;
    }

public:
    StateMachineToO(ostream &out=cout) : StateMachineDim(out, "SCHEDULER"),
        fConnection(""), fKeepAliveDeadline(Time())
    {
        AddStateName(ToO::State::kDisconnected, "Disconnected",
                     "The Dim DNS is reachable, but the required subsystems are not available.");

        AddStateName(ToO::State::kConnected, "Connected",
                     "All needed subsystems are connected to their hardware, no action is performed.");

        AddEvent("GCN", "S:1;I:1;D:1;D:1;D:1")
            (bind(&StateMachineToO::ScheduleGCN, this, placeholders::_1))
            (""
             "|type[int16]:TypeID (see HeaderGCN.h)"
             "|num[uint32]:Right ascension"
             "|ra[deg]:Right ascension"
             "|dec[deg]:Declination"
             "|err[deg]:Error radius");
    }

    /*
    bool GetConfig(Configuration &conf, const string &name, const string &sub, uint16_t &rc)
    {
        if (conf.HasDef(name, sub))
        {
            rc = conf.GetDef<uint16_t>(name, sub);
            return true;
        }

        Error("Neither "+name+"default nor "+name+sub+" found.");
        return false;
    }*/

    int EvalOptions(Configuration &conf)
    {
        fVerbose = !conf.Get<bool>("quiet");
        fDryRun = conf.Get<bool>("dry-run");
        fKeepAliveInterval = conf.Get<uint16_t>("keep-alive");
        fUri = conf.Get<string>("schedule-database");
        return -1;
    }
};


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

#include "Main.h"

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

void SetupConfiguration(Configuration &conf)
{
    po::options_description control("Scheduler");
    control.add_options()
        ("quiet,q", po_bool(), "")
        ("dry-run", po_bool(), "Switch off any alteration of the database")
        ("keep-alive", var<uint16_t>(uint16_t(300)), "Interval in seconds to ping (reconnect) the database server")
        ("schedule-database", var<string>(), "Database link as in\n\tuser:password@server[:port]/database[?compress=0|1].")
        ;

    conf.AddOptions(control);
}

void PrintUsage()
{
    cout <<
        "The scheduler program is able to schedule a new observation.\n"
        "\n"
        "Usage: scheduler [-c type] [OPTIONS]\n"
        "  or:  scheduler [OPTIONS]\n";
    cout << endl;
}

void PrintHelp()
{
    Main::PrintHelp<StateMachineToO>();
}

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;

    if (!conf.Has("console"))
        return RunShell<LocalStream>(conf);

    if (conf.Get<int>("console")==0)
        return RunShell<LocalShell>(conf);
    else
        return RunShell<LocalConsole>(conf);

    return 0;
}

