#include "Scheduler.h"
#include <QDebug>

static int
cmptime(const struct timeval & a, const struct timeval & b) {
   if (a.tv_sec < b.tv_sec) {
      return -1;
   } else if (a.tv_sec > b.tv_sec) {
      return 1;
   } else {
      if (a.tv_usec < b.tv_usec) {
         return -1;
      } else if (a.tv_usec > b.tv_usec) {
         return 1;
      } else {
         return 0;
      }
   }
}

Scheduler::Scheduler(const QString & devname)
 : UK1104(devname),
   cur_cs(NULL),
   old_cs(NULL),
   relay_state_known(false),
   expecting_relay_state(false),
   q(NULL),
   in_scanQueue(false) {

   sched_timer.setSingleShot(true);
   connect(&sched_timer, SIGNAL(timeout()),
           this,         SLOT(scanQueue()));
   connect(this,         SIGNAL(commandDone(UK1104Errors, QString)),
           this,         SLOT(turnCrank(UK1104Errors, QString)));
}

Scheduler::~Scheduler() {
   delete old_cs;
   delete cur_cs;
   while (q != NULL) {
      cur_cs = q;
      q = cur_cs->next;
      delete cur_cs;
   }
}

void
Scheduler::RunAt(
   struct timeval & tv,
   CommandSet * cs,
   int timeout_ms) {
   cs->sched_time = tv;
   cs->timeout_ms = timeout_ms;

   CommandSet ** qpp;
   CommandSet * qp;
   for (qpp = &q; (qp = *qpp) != NULL; qpp = &qp->next) {
      if (cmptime(cs->sched_time, qp->sched_time) < 0) {
         break;
      }
   }
   cs->next = *qpp;
   *qpp = cs;
   if (! in_scanQueue) {
      // Prime the pump...
      QTimer::singleShot(0, this, SLOT(scanQueue()));
   }
}

bool
Scheduler::IsIdle() {
   return (q == NULL) &&
          ((cur_cs == NULL) || (cur_cs->status != UK1104_PENDING));
}

void
Scheduler::scanQueue() {
   if (in_scanQueue) {
      return;
   }
   in_scanQueue = true;
   struct timeval now;
   while (q != NULL) {
      gettimeofday(&now, NULL);
      if (cmptime(now, q->sched_time) >= 0) {
         // Entry at head of queue is ready to run. Pull it off the
         // queue and run the commands...
         if (cur_cs != NULL) {
            delete old_cs;
            old_cs = cur_cs;
            cur_cs = NULL;
         }
         cur_cs = q;
         q = cur_cs->next;
         cur_cs->next = NULL;
         RunCommandSet();
      } else {
         break;
      }
   }
   if (q != NULL) {

      // The entry at the head of the queue is not yet ready to
      // run, compute a number of milliseconds in future when it will
      // be ready and schedule next timeout for then.

      struct timeval diff;
      diff.tv_sec = q->sched_time.tv_sec - now.tv_sec;
      if (q->sched_time.tv_usec < now.tv_usec) {
         diff.tv_sec -= 1;
         diff.tv_usec = (q->sched_time.tv_usec + 1000000) - now.tv_usec;
      } else {
         diff.tv_usec = q->sched_time.tv_usec - now.tv_usec;
      }
      unsigned long diff_ms = 1000 * diff.tv_sec;
      diff_ms += ((diff.tv_usec + 500) / 1000);
      if (diff_ms == 0) {
         diff_ms = 1;
      }
      sched_timer.stop();
      sched_timer.start((int) diff_ms);
   }
   in_scanQueue = false;
}

void
Scheduler::turnCrank(UK1104Errors status, QString result) {

   // Record everything in the cur_cs result string

   cur_cs->results.append(result);

   if ((status == UK1104_TIMEOUT) && rerun_on_timeout) {

      // Rerun the command if it timed out, but don't keep doing
      // it forever...

      ++timeout_count;
      if (timeout_count > 3) {
         rerun_on_timeout = false;
      }
      Q_ASSERT(SendCommand(running_command,
                           cur_cs->timeout_ms + (timeout_count * 20)));
      return;
   }
   if ((status == UK1104_OK) && (running_command == "RELS.GET")) {

      // We just successfully ran a get relay state command, so as a
      // side effect while coming through this code, record the
      // known relay state.

      int ndx = result.indexOf("\n");
      relay_state_known = true;
      if (ndx >= 0) {
         QString relstat = result.mid(ndx+1, 7);
         for (int i = 0; i < 4; ++i) {
            QChar relay = relstat[i*2];
            if (relay == '0') {
               relay_state[i] = false;
            } else if (relay == '1') {
               relay_state[i] = true;
            } else {
               relay_state_known = false;
            }
         }
      }

      // This RELS.GET may have been added to the command stream to
      // verify the previous command worked properly. If we have expected
      // results, check them here.

      if (expecting_relay_state) {
         expecting_relay_state = false;
         bool state_match = true;
         for (int i = 0; i < 4; ++i) {
            if (expected_relay[i] != relay_state[i]) {
               state_match = false;
               break;
            }
         }
         if (! state_match) {
            // Yikes! The command apparently didn't take effect, try
            // running it again.
            cur_cs->commands.push_front(checked_relay_command);
            RunCommandSet();
            return;
         }
      }
   }
   if ((status != UK1104_OK) || (cur_cs->commands.isEmpty())) {

      // We have already dealt with errors we can recover from, so if
      // we still seem to have an error, I guess we have to give up
      // on this CommandSet. Also, if we are out of commands to run
      // it is time to report this as complete.

      cur_cs->status = status;
      emit completedSet(cur_cs);
      QTimer::singleShot(0, this, SLOT(scanQueue()));
      return;
   }

   // Must still have more commands to run in this set, go run 'em

   RunCommandSet();
}

void
Scheduler::RunCommandSet() {
   Q_ASSERT(cur_cs != NULL);
   Q_ASSERT(cur_cs->commands.size() > 0);
   AnalyzeCommand();
   running_command = cur_cs->commands.front();
   cur_cs->commands.pop_front();
   Q_ASSERT(SendCommand(running_command, cur_cs->timeout_ms));
}

void
Scheduler::AnalyzeCommand() {
   // Look at the current command (at head of queue in cur_cs) and analyze it
   // to decide what I can and cannot do with it and what ways
   // I can deal with timeout, etc.

   // relay_state_prereq - set true if I need to know the state of all the
   //                      relays before running the command (only needed
   //                      for the toggle relay command).

   bool relay_state_prereq = false;

   // rerun_on_timeout - set true if the command can be run multiple times
   //                    in a row and leave the UK1104 in the same state
   //                    (i.e. it is safe to rerun the command if we timeout
   //                    and are not sure the command actually had an
   //                    effect).

   rerun_on_timeout = false;
   timeout_count = 0;

   // verify_relay_state - set true if I need to verify the the state of the
   //                      relays following the command (used to enable check
   //                      of relay on and off commands to verify they worked).

   bool verify_relay_state = false;

   // invalid_command - set true if cmd does not look like a valid
   //                   command for the UK1104.

   invalid_command = false;

   // relay_num - if the command operates on relays, this is set to the
   //             number of the relay it operates on 1,2,3,4), or -1 means
   //             all relays. Zero means not a relay command.

   int relay_num = 0;

   QString relcom;

   QString cmd = cur_cs->commands.front();
   if (cmd.startsWith("REL")) {
      QChar whichrel = cmd.at(3);
      QChar dot = cmd.at(4);
      relay_num = -1;
      relcom = cmd.mid(5);
      if (dot == '.') {
         if (whichrel.isDigit()) {
            relay_num = whichrel.digitValue();
            if (! ((relay_num >= 1) && (relay_num <= 4))) {
               invalid_command = true;
            }
         } else if (whichrel != 'S') {
            invalid_command = true;
         }
      } else {
         invalid_command = true;
      }
      if (! invalid_command) {
         if (relcom == "ON") {
            rerun_on_timeout = true;
            verify_relay_state = true;
         } else if (relcom == "OFF") {
            rerun_on_timeout = true;
            verify_relay_state = true;
         } else if ((relcom == "TOGGLE") && (relay_num != -1)) {
            relay_state_prereq = true;
            verify_relay_state = true;
         } else if (relcom == "GET") {
            rerun_on_timeout = true;
         } else {
            invalid_command = true;
         }
      }
   } else if (cmd.startsWith("CH")) {
      QChar whichch = cmd.at(2);
      QChar dot = cmd.at(3);
      int chnum = -1;
      QString chcom = cmd.mid(4);
      if (dot == '.') {
         if (whichch.isDigit()) {
            chnum = whichch.digitValue();
            if (! ((chnum >= 1) && (chnum <= 6))) {
               invalid_command = true;
            }
         } else if (whichch != 'S') {
            invalid_command = true;
         }
      } else {
         invalid_command = true;
      }
      if (! invalid_command) {
         if (chcom == "ON") {
            rerun_on_timeout = true;
         } else if (chcom == "OFF") {
            rerun_on_timeout = true;
         } else if ((chcom == "TOGGLE") && (chnum != -1)) {
         } else if (chcom == "GET") {
            rerun_on_timeout = true;
         } else if (chcom == "GETANALOG") {
            rerun_on_timeout = true;
         } else if (chcom == "GETTEMP") {
            rerun_on_timeout = true;
         } else if (chcom.startsWith("SETMODE")) {
            QString mode = AnalyzeParens(chcom.mid(7));
            if ((mode.size() == 1) && mode.at(0).isDigit()) {
               int modeval = mode.at(0).digitValue();
               if (! ((modeval >= 1) && (modeval <= 4))) {
                  invalid_command = true;
               }
            } else {
               invalid_command = true;
            }
            if (! invalid_command) {
               rerun_on_timeout = true;
            }
         } else if (chcom.startsWith("SETTEMPRES")) {
            QString res = AnalyzeParens(chcom.mid(10));
            bool ok;
            unsigned int resval = res.toUInt(&ok, 10);
            if (! ok) resval = 0;
            if (! ((resval >= 9) && (resval <= 12))) {
               invalid_command = true;
            } else {
               rerun_on_timeout = true;
            }
         } else {
            invalid_command = true;
         }
      }
   } else if (cmd == "ABOUT") {
      rerun_on_timeout = true;
   } else if (cmd.startsWith("SETID")) {
      QString id = AnalyzeParens(cmd.mid(5));
      if (id.size() != 2) {
         invalid_command = true;
      }
   } else if (cmd.isEmpty()) {
      rerun_on_timeout = true;
   } else {
      invalid_command = true;
   }
   if ((! invalid_command) && relay_state_prereq && (! relay_state_known)) {

      // We need to know the relay state before we can run the command we
      // just analyzed, so push a RELS.GET command on the front and do
      // a new analysis of that command instead.

      cur_cs->commands.push_front("RELS.GET");
      AnalyzeCommand();
   } else if ((! invalid_command) && verify_relay_state && relay_state_known) {
      // This command needs to have a RELS.GET command run following it in
      // order to verify it did the expected things to the state of the
      // relays.
      checked_relay_command = cmd;
      expecting_relay_state = true;
      memcpy((void *)&expected_relay[0], (void *)&relay_state[0],
             sizeof(expected_relay));
      if (relay_num == -1) {
         if (relcom == "ON") {
            for (int i = 0; i < 4; ++i) { expected_relay[i] = true; }
         } else if (relcom == "OFF") {
            for (int i = 0; i < 4; ++i) { expected_relay[i] = false; }
         }
      } else {
         if (relcom == "ON") {
            expected_relay[relay_num - 1] = true;
         } else if (relcom == "OFF") {
            expected_relay[relay_num - 1] = false;
         } else if (relcom == "TOGGLE") {
            expected_relay[relay_num - 1] = ! expected_relay[relay_num - 1];
         }
      }
      cur_cs->commands.pop_front();
      if (cur_cs->commands.isEmpty() ||
          (cur_cs->commands.front() != "RELS.GET")) {
         cur_cs->commands.push_front("RELS.GET");
      }
      cur_cs->commands.push_front(cmd);
   }
}

QString
Scheduler::AnalyzeParens(QString s) {
   QString rval;
   if ((s.at(0) == '(') && (s.at(s.size() - 1) == ')')) {
      rval = s.mid(1, s.size()-2);
   }
   return rval;
}
