#include <QDebug>
#include <QProcess>
#include <QDir>
#include <QPixmap>
#include <QTimer>
#include "BookScanner.h"
#include "ConfigData.h"
#include "WatchUdev.h"
#include "CommandSet.h"
#include "wrap-gphoto.h"
#include "wrap-camera.h"
#include <sys/time.h>

BookScanner::BookScanner()
 : sched(NULL),
   watch_udev(NULL),
   config_dialog(NULL),
   wgp(NULL),
   click_count(0) {

   ui.setupUi(this);

   // Connect up the all important quit button

   connect(ui.quitButton, SIGNAL(clicked()),
           qApp,          SLOT(quit()));

   // Hook up the buttons.

   connect(ui.configButton, SIGNAL(clicked()),
           this,            SLOT(doConfig()));

   connect(ui.takePictureButton, SIGNAL(clicked()),
           this,                 SLOT(doClick()));

   connect(ui.lightsAlwaysOnCheck, SIGNAL(stateChanged(int)),
           this,                   SLOT(doLightCheck(int)));

   ui.takePictureButton->setEnabled(false);

   // Setup a UDEV monitor and have it record the properties I'll need
   // to recognize the udev events for the cameras and run gphoto2
   // as well as recognize the UK1104 device.

   watch_udev = new WatchUdev();
   watch_udev->IncludeProperty("ID_GPHOTO2");
   watch_udev->IncludeProperty("BUSNUM");
   watch_udev->IncludeProperty("DEVNUM");
   watch_udev->IncludeProperty("ID_SERIAL_SHORT");
   watch_udev->IncludeProperty("ID_VENDOR_ID");
   watch_udev->IncludeProperty("ID_MODEL_ID");
   watch_udev->IncludeProperty("DEVNAME");

   connect(watch_udev,
           SIGNAL(seeDevice(QString,QString,QString,QMap<QString, QString>)),
           this,
           SLOT(devPlug(QString,QString,QString,QMap<QString, QString>)));

   ui.firstLeftButton->setProperty("DIR", (int)SUBDIR_LEFT);
   ui.firstLeftButton->setProperty("NAV", (int)FIRST_IMAGE);
   ui.prevLeftButton->setProperty("DIR", (int)SUBDIR_LEFT);
   ui.prevLeftButton->setProperty("NAV", (int)PREV_IMAGE);
   ui.nextLeftButton->setProperty("DIR", (int)SUBDIR_LEFT);
   ui.nextLeftButton->setProperty("NAV", (int)NEXT_IMAGE);
   ui.lastLeftButton->setProperty("DIR", (int)SUBDIR_LEFT);
   ui.lastLeftButton->setProperty("NAV", (int)LAST_IMAGE);

   ui.firstRightButton->setProperty("DIR", (int)SUBDIR_RIGHT);
   ui.firstRightButton->setProperty("NAV", (int)FIRST_IMAGE);
   ui.prevRightButton->setProperty("DIR", (int)SUBDIR_RIGHT);
   ui.prevRightButton->setProperty("NAV", (int)PREV_IMAGE);
   ui.nextRightButton->setProperty("DIR", (int)SUBDIR_RIGHT);
   ui.nextRightButton->setProperty("NAV", (int)NEXT_IMAGE);
   ui.lastRightButton->setProperty("DIR", (int)SUBDIR_RIGHT);
   ui.lastRightButton->setProperty("NAV", (int)LAST_IMAGE);

   connect(ui.firstLeftButton,     SIGNAL(clicked()),
           this,                   SLOT(navigatePictures()));
   connect(ui.prevLeftButton,      SIGNAL(clicked()),
           this,                   SLOT(navigatePictures()));
   connect(ui.nextLeftButton,      SIGNAL(clicked()),
           this,                   SLOT(navigatePictures()));
   connect(ui.lastLeftButton,      SIGNAL(clicked()),
           this,                   SLOT(navigatePictures()));

   connect(ui.firstRightButton,    SIGNAL(clicked()),
           this,                   SLOT(navigatePictures()));
   connect(ui.prevRightButton,     SIGNAL(clicked()),
           this,                   SLOT(navigatePictures()));
   connect(ui.nextRightButton,     SIGNAL(clicked()),
           this,                   SLOT(navigatePictures()));
   connect(ui.lastRightButton,     SIGNAL(clicked()),
           this,                   SLOT(navigatePictures()));

   // Fixup the window sizes based on preference for preview size.

   updateImageSize(ConfigData::GetConfig()->PreferenceImagePreviewSize());

   updateImages(LeftCameraDestDirectory());
   updateImages(RightCameraDestDirectory());

   // Do the scan for existing udev devices after we return from constructor
   // and are in the event loop.

   QTimer::singleShot(0, this, SLOT(runScan()));
}

BookScanner::~BookScanner() {
}

void
BookScanner::doClick() {
   ++click_count;
   if (ui.lightsAlwaysOnCheck->isChecked()) {
      ProgramEntryList prog;
      ProgramEntry pe;
      pe.timespec = "0";
      pe.actionspec = tr("Turn on lights");
      prog << pe;
      StartProgram(prog);
   } else {
      StartProgram(ConfigData::GetConfig()->LightsProgram());
   }
   if (ui.downloadCheck->isChecked()) {
      ui.downloadCheck->setChecked(false);
      StartProgram(ConfigData::GetConfig()->DownloadProgram());
   } else {
      StartProgram(ConfigData::GetConfig()->CameraProgram());
   }
}

void
BookScanner::devPlug(
   QString action,
   QString /* path */,
   QString subsystem,
   QMap<QString, QString> properties) {

   if (properties.contains("BUSNUM") &&
       properties.contains("DEVNUM") &&
       properties.contains("ID_GPHOTO2") &&
       (properties.value("ID_GPHOTO2") == "1")) {

      // Looks like a libgphoto2 capable device, it is worth examining
      // other properties...

      QString bus    = properties.value("BUSNUM");
      QString dev    = properties.value("DEVNUM");
      QString serial = properties.value("ID_SERIAL_SHORT", "");
      QString vendor = properties.value("ID_VENDOR_ID", "");
      QString model  = properties.value("ID_MODEL_ID", "");
      bool left_cam = (serial == ConfigData::GetConfig()->LeftCameraSerial() &&
                       vendor == ConfigData::GetConfig()->LeftCameraVendor() &&
                       model  == ConfigData::GetConfig()->LeftCameraModel());
      bool right_cam = ((! left_cam) &&
                        serial == ConfigData::GetConfig()->RightCameraSerial() &&
                        vendor == ConfigData::GetConfig()->RightCameraVendor() &&
                        model  == ConfigData::GetConfig()->RightCameraModel());
      if (left_cam) {
         if (action == "add") {
            fetchPictures(bus, dev,
                          ConfigData::GetConfig()->LeftCameraSrcDirectory(),
                          LeftCameraDestDirectory());
         }
      } else if (right_cam) {
         if (action == "add") {
            fetchPictures(bus, dev,
                          ConfigData::GetConfig()->RightCameraSrcDirectory(),
                          RightCameraDestDirectory());
         }
      }
   }

   // And not just cameras, but also the UK1104...

   if (subsystem == "tty") {
      QString vendor = properties.value("ID_VENDOR_ID", "");
      QString model  = properties.value("ID_MODEL_ID", "");
      QString devname= properties.value("DEVNAME", "");
      if ((! devname.isEmpty()) &&
          (vendor == ConfigData::GetConfig()->UK1104Vendor()) &&
          (model == ConfigData::GetConfig()->UK1104Model())) {
         if (action != "remove") {
            openScheduler(devname);
         } else {
            delete sched;
            sched = NULL;
            ui.takePictureButton->setEnabled(false);
         }
      }
   }
}

void
BookScanner::navigatePictures() {
   QPushButton * btn = dynamic_cast<QPushButton *>(sender());
   if (btn != NULL) {
      NavigateDirection nav = (NavigateDirection)btn->property("NAV").toInt();
      if (ConfigData::GetConfig()->PreferenceIndependentButtons()) {

         // Preference is set to make the buttons independent, so
         // do the navigation only on the directory stashed in the
         // button.

         WhichSubdir dir = (WhichSubdir)btn->property("DIR").toInt();
         doNavigate(dir, nav);
      } else {

         // Preference is set to move through both directories in parallel,
         // so do both directories.

         doNavigate(SUBDIR_LEFT, nav);
         doNavigate(SUBDIR_RIGHT, nav);
      }
   }
}

void
BookScanner::runScan() {

   // Query udev for an existing UK1104 device and pretend it was
   // just added (in case it is plugged in before the program
   // is started).

   QStringList subsystems;
   QStringList properties;
   QString id_model = "ID_MODEL_ID=";
   id_model.append(ConfigData::GetConfig()->UK1104Model());
   QString id_vend = "ID_VENDOR_ID=";
   id_vend.append(ConfigData::GetConfig()->UK1104Vendor());
   subsystems << "tty";
   properties << id_model;
   properties << id_vend;
   watch_udev->ScanDevices(subsystems, properties);
}

void
BookScanner::doConfig() {
   if (config_dialog == NULL) {
      config_dialog = new ConfigDialog;
   }

   QString uk1104_vendor = ConfigData::GetConfig()->UK1104Vendor();
   QString uk1104_model = ConfigData::GetConfig()->UK1104Model();
   QString uk1104_id = ConfigData::GetConfig()->UK1104IdCode();

   QString dest_dir = ConfigData::GetConfig()->DestinationDirectory();
   QString left_dir = ConfigData::GetConfig()->LeftCameraSubdir();
   QString right_dir = ConfigData::GetConfig()->RightCameraSubdir();

   QSize preview_size = ConfigData::GetConfig()->PreferenceImagePreviewSize();

   config_dialog->PopUp();

   if ((uk1104_vendor != ConfigData::GetConfig()->UK1104Vendor()) ||
       (uk1104_model != ConfigData::GetConfig()->UK1104Model()) ||
       (uk1104_id != ConfigData::GetConfig()->UK1104IdCode())) {
      // PUT CODE HERE - identification for the uk1104 has changed,
      // if we have one open, need to close it and in any case need
      // to start a rescan in case the device is plugged in already.
   }

   if ((dest_dir != ConfigData::GetConfig()->DestinationDirectory()) ||
       (left_dir != ConfigData::GetConfig()->LeftCameraSubdir()) ||
       (right_dir != ConfigData::GetConfig()->RightCameraSubdir())) {
      updateImages(LeftCameraDestDirectory());
      updateImages(RightCameraDestDirectory());
   }

   if (preview_size != ConfigData::GetConfig()->PreferenceImagePreviewSize()) {
      preview_size = ConfigData::GetConfig()->PreferenceImagePreviewSize();
      updateImageSize(preview_size);
   }
}

void
BookScanner::resizeMe() {
   resize(minimumSizeHint());
}

void
BookScanner::doLightCheck(int new_state) {
   if ((sched != NULL) && (sched->IsIdle())) {
      QStringList doit;
      if (new_state == Qt::Unchecked) {
         doit = ConfigData::GetConfig()->UK1104TurnOffLights();
      } else if (new_state == Qt::Checked) {
         doit = ConfigData::GetConfig()->UK1104TurnOnLights();
      }
      if (! doit.isEmpty()) {
         CommandSet * cs = new CommandSet(1, doit);
         struct timeval now;
         gettimeofday(&now, NULL);
         sched->RunAt(now, cs, 500);
      }
   }
}

void
BookScanner::openScheduler(QString devname) {

   // Create the driver for the UK1104 relay controller and
   // hook up the Click! button to make it diddle the relays
   // that are on the USB +5V power line.

   sched = new Scheduler(devname);
   if ((sched != NULL) && sched->ValidConnection()) {
      // PUT CODE HERE - need to query the UK1104IdCode() to see
      // if it matches before I start using this particular device.

      // Build a CommandSet for initializer and run it through the
      // Scheduler.

      CommandSet * cs = new CommandSet(2,
                               ConfigData::GetConfig()->UK1104Initialize());
      struct timeval now;
      gettimeofday(&now, NULL);
      sched->RunAt(now, cs, 500);

      ui.takePictureButton->setEnabled(true);
   } else {
      delete sched;
      sched = NULL;
      ui.takePictureButton->setEnabled(false);
   }
}

void
BookScanner::fetchPictures(
   QString bus,
   QString dev,
   QString srcdir,
   QString destdir) {

   QString port("usb:");
   port.append(bus);
   port.append(",");
   port.append(dev);

   WrapGphoto2 wg;
   WrapCamera * wcp = wg.OpenCameraOnPort(port.toAscii().constData());
   if (wcp != NULL) {
      QStringList files = wcp->FilesInDirectory(srcdir);
      for (int i = 0; i < files.size(); ++i) {
         QString file = files.at(i);
         if (wcp->FetchFile(srcdir, file, destdir, file)) {
            if (! wcp->DeleteFile(srcdir, file)) {
               qDebug() << "file deletion failed";
            }
         } else {
            qDebug() << "file download failed";
         }
      }
   } else {
      qDebug() << "OpenCameraOnPort failed";
   }
   delete wcp;

   updateImages(destdir);
}

void
BookScanner::updateImages(QString dirname) {
   QDir dir(dirname);
   QStringList filt;
   bool do_left;
   if (dirname == LeftCameraDestDirectory()) {
      do_left = true;
      filt = ConfigData::GetConfig()->LeftCameraImageFilters();
   } else if (dirname == RightCameraDestDirectory()) {
      do_left = false;
      filt = ConfigData::GetConfig()->RightCameraImageFilters();
   }
   dir.setNameFilters(filt);
   dir.setSorting(QDir::Name|QDir::Reversed);
   QStringList names = dir.entryList();
   if (names.isEmpty()) {
      return;
   }
   QString newname = names.first();
   QString fullname = dir.absoluteFilePath(newname);
   if (do_left) {
      if (last_left_image != newname) {
         updateLeftImage(newname);
      }
   } else {
      if (last_right_image != newname) {
         updateRightImage(newname);
      }
   }
}

void
BookScanner::updateLeftImage(QString name) {
   last_left_image = name;
   QString fullname =
      QDir(LeftCameraDestDirectory()).absoluteFilePath(name);
   ui.leftImage->setScaledContents(true);
   QPixmap img(fullname);
   ui.leftImage->setPixmap(img);
   ui.leftLabel->setText(name);
   ui.leftLabel->setText(QString(tr("Left ")) + name);
}

void
BookScanner::updateRightImage(QString name) {
   last_right_image = name;
   QString fullname =
      QDir(RightCameraDestDirectory()).absoluteFilePath(name);
   ui.rightImage->setScaledContents(true);
   QPixmap img(fullname);
   ui.rightImage->setPixmap(img);
   ui.rightLabel->setText(QString(tr("Right ")) + name);
}

void
BookScanner::logTime(QString msg) {
   static struct timeval prev_tv = {0,0};
   struct timeval tv;
   gettimeofday(&tv, NULL);
   qDebug() << msg <<
      QString("timestamp %1.%2")
         .arg((unsigned long)tv.tv_sec)
            .arg((unsigned long)tv.tv_usec, 6, 10, QChar('0'));
   if (prev_tv.tv_sec != 0) {
      struct timeval elapsed;
      if (tv.tv_usec < prev_tv.tv_usec) {
         tv.tv_sec -= 1;
         tv.tv_usec += 1000000;
      }
      elapsed.tv_usec = tv.tv_usec - prev_tv.tv_usec;
      elapsed.tv_sec = tv.tv_sec - prev_tv.tv_sec;
      qDebug() << msg <<
         QString("elapsed %1.%2")
            .arg((unsigned long)elapsed.tv_sec)
               .arg((unsigned long)elapsed.tv_usec, 6, 10, QChar('0'));
   }
   prev_tv = tv;
}

QString
BookScanner::LeftCameraDestDirectory() {
   QString rval = ConfigData::GetConfig()->DestinationDirectory();
   rval.append("/");
   rval.append(ConfigData::GetConfig()->LeftCameraSubdir());
   return rval;
}

QString
BookScanner::RightCameraDestDirectory() {
   QString rval = ConfigData::GetConfig()->DestinationDirectory();
   rval.append("/");
   rval.append(ConfigData::GetConfig()->RightCameraSubdir());
   return rval;
}

void
BookScanner::doNavigate(WhichSubdir dir, NavigateDirection nav) {
   QString dirname;
   QString curr_img;
   QStringList filt;
   if (dir == SUBDIR_LEFT) {
      dirname = LeftCameraDestDirectory();
      curr_img = last_left_image;
      filt = ConfigData::GetConfig()->LeftCameraImageFilters();
   } else if (dir == SUBDIR_RIGHT) {
      dirname = RightCameraDestDirectory();
      curr_img = last_right_image;
      filt = ConfigData::GetConfig()->RightCameraImageFilters();
   }
   if (! dirname.isEmpty()) {
      QDir d(dirname);
      d.setNameFilters(filt);
      d.setSorting(QDir::Name);
      QStringList names = d.entryList();
      int my_ndx = names.indexOf(curr_img);
      int new_ndx = -1;
      if (my_ndx != -1) {
         switch (nav) {
         case FIRST_IMAGE:
            new_ndx = 0;
            break;
         case LAST_IMAGE:
            new_ndx = names.count() - 1;
            break;
         case NEXT_IMAGE:
            new_ndx = my_ndx + 1;
            break;
         case PREV_IMAGE:
            new_ndx = my_ndx - 1;
            break;
         }
         if (new_ndx >= 0) {
            if (new_ndx > (names.count() - 1)) {
               new_ndx = names.count() - 1;
            }
            QString newname = names.at(new_ndx);
            if (dir == SUBDIR_LEFT) {
               updateLeftImage(newname);
            } else {
               updateRightImage(newname);
            }
         }
      }
   }
}

void
BookScanner::updateImageSize(QSize new_size) {
   ui.leftImage->setMinimumSize(new_size);
   ui.leftImage->setMaximumSize(new_size);
   ui.rightImage->setMinimumSize(new_size);
   ui.rightImage->setMaximumSize(new_size);

   QTimer::singleShot(0, this, SLOT(resizeMe()));
}

void
BookScanner::StartProgram(const ProgramEntryList & prog) {
   struct timeval now;
   gettimeofday(&now, NULL);
   for (int i = 0; i < prog.size(); ++i) {
      QString timespec = prog.at(i).timespec;
      QString actionspec = prog.at(i).actionspec;
      bool ok;
      bool add_download_time = timespec.endsWith("+N");
      if (add_download_time) {
         timespec = timespec.left(timespec.size()-2);
      }
      unsigned long msec = timespec.toULong(&ok);
      struct timeval tv;
      tv.tv_usec = now.tv_usec + 1000 * (msec % 1000);
      tv.tv_sec = now.tv_sec + (msec / 1000);
      while (tv.tv_usec > 1000000) {
         tv.tv_usec -= 1000000;
         tv.tv_sec += 1;
      }
      if (add_download_time) {

         // By the convention I invented for the camera microcode, it
         // calculates the estimate of download time in tenths of a second
         // (since it has no floating point and whole seconds are a little
         // too crude), therefore I need to do the matching calculation here
         // in tenths of seconds as well.

         int download_tenths =
            ConfigData::GetConfig()->DownloadConstTenthsOfSecond();
         download_tenths +=
            (ConfigData::GetConfig()->DownloadOneImageTenthsOfSecond() *
             click_count);
         click_count = 0;
         long download_msecs = download_tenths * 100;
         tv.tv_usec += 1000 * (download_msecs % 1000);
         tv.tv_sec += (download_msecs / 1000);
         while (tv.tv_usec > 1000000) {
            tv.tv_usec -= 1000000;
            tv.tv_sec += 1;
         }
      }
      QStringList uk1104_commands;
      if (actionspec == tr("Turn on lights")) {
         uk1104_commands = ConfigData::GetConfig()->UK1104TurnOnLights();
      } else if (actionspec == tr("Turn off lights")) {
         uk1104_commands = ConfigData::GetConfig()->UK1104TurnOffLights();
      } else if (actionspec == tr("Turn on USB")) {
         uk1104_commands = ConfigData::GetConfig()->UK1104TurnOnUSB();
      } else if (actionspec == tr("Turn off USB")) {
         uk1104_commands = ConfigData::GetConfig()->UK1104TurnOffUSB();
      }
      if (sched != NULL) {
         sched->RunAt(tv, new CommandSet(3, uk1104_commands), 500);
      }
   }
}
