/*  Image Sequence
    Capture image sequence from an imaging device.
    
    Copyright (C) 2003 Jasem Mutlaq (mutlaqja@ikarustech.com)

    This application is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.
 */
 
#include "imagesequence.h"
#include "kstars.h"
#include "indidriver.h"
#include "indielement.h"
#include "indiproperty.h"
#include "indimenu.h"
#include "indidevice.h"
#include "indistd.h"
#include "devicemanager.h"
#include "Options.h"

#include <kdebug.h>
#include <tdemessagebox.h>
#include <tdelocale.h>
#include <klineedit.h>
#include <kprogress.h>
#include <knuminput.h>

#include <tqtimer.h>
#include <tqcombobox.h>
#include <tqpushbutton.h>
#include <tqlabel.h>
#include <tqcheckbox.h>
#include <tqstringlist.h>

#define RETRY_MAX	12
#define RETRY_PERIOD	5000

imagesequence::imagesequence(TQWidget* parent, const char* name, bool modal, WFlags fl)
: imgSequenceDlg(parent,name, modal,fl)
{
  
  ksw = (KStars *) parent;
  INDIMenu *devMenu = ksw->getINDIMenu();

  if (devMenu)
  {
   connect (devMenu, TQ_SIGNAL(newDevice()), this, TQ_SLOT(newCCD()));
   connect (devMenu, TQ_SIGNAL(newDevice()), this, TQ_SLOT(newFilter()));
  }

  seqTimer = new TQTimer(this);
  
  setModal(false);
  
  // Connect signals and slots
  connect(startB, TQ_SIGNAL(clicked()), this, TQ_SLOT(startSequence()));
  connect(stopB, TQ_SIGNAL(clicked()), this, TQ_SLOT(stopSequence()));
  connect(closeB, TQ_SIGNAL(clicked()), this, TQ_SLOT(close()));
  connect(seqTimer, TQ_SIGNAL(timeout()), this, TQ_SLOT(prepareCapture()));
  connect(CCDCombo, TQ_SIGNAL(activated(int)), this, TQ_SLOT(checkCCD(int)));
  connect(filterCombo, TQ_SIGNAL(activated(int)), this, TQ_SLOT(updateFilterCombo(int)));
  
  active 		= false;
  ISOStamp		= false;
  seqExpose		= 0;
  seqTotalCount 	= 0;
  seqCurrentCount	= 0;
  seqDelay		= 0;
  lastCCD              = 0;
  lastFilter            = 0;
  stdDevCCD             = NULL;
  stdDevFilter		= NULL;
  
}

imagesequence::~imagesequence()
{
}

bool imagesequence::updateStatus()
{
  bool result;

  result = setupCCDs();
  setupFilters();
 
  // If everything okay, let's show the dialog
  return result;
   
 }

void imagesequence::newCCD()
{
   // Only update when it's visible
   if (isVisible())
      setupCCDs();
}

void imagesequence::newFilter()
{
   // Only update when it's visible
   if (isVisible())
      setupFilters();
}

bool imagesequence::setupCCDs()
{
  bool imgDeviceFound (false);
  INDI_P *imgProp;
  INDIMenu *devMenu = ksw->getINDIMenu();
  if (devMenu == NULL)
     return false;
  
  CCDCombo->clear();
  
  for (uint i=0; i < devMenu->mgr.count(); i++)
  {
	for (uint j=0; j < devMenu->mgr.at(i)->indi_dev.count(); j++)
	{
  		imgProp = devMenu->mgr.at(i)->indi_dev.at(j)->findProp("CCD_EXPOSE_DURATION");
		if (!imgProp)
			 continue;
				 
		imgDeviceFound = true;
		 
		if (devMenu->mgr.at(i)->indi_dev.at(j)->label.isEmpty())
			devMenu->mgr.at(i)->indi_dev.at(j)->label = devMenu->mgr.at(i)->indi_dev.at(j)->name;
				
		CCDCombo->insertItem(devMenu->mgr.at(i)->indi_dev.at(j)->label);
			
	}
	
   }
         
  if (imgDeviceFound)
  {
  	CCDCombo->setCurrentItem(lastCCD);
  	currentCCD = CCDCombo->currentText();
  }
  else return false;
  
  if (!verifyCCDIntegrity())
  {
   stopSequence();
   return false;
  }
  else
  {
    INDI_P *exposeProp;
    INDI_E *exposeElem;
    
    exposeProp = stdDevCCD->dp->findProp("CCD_EXPOSE_DURATION");
    if (!exposeProp)
    {
      KMessageBox::error(this, i18n("Device does not support CCD_EXPOSE_DURATION property."));
      return false;
    }
  
    exposeElem = exposeProp->findElement("EXPOSE_DURATION");
    if (!exposeElem)
    {
      KMessageBox::error(this, i18n("CCD_EXPOSE_DURATION property is missing DURATION element."));
      return false;
    }
    
    exposureIN->setValue(exposeElem->value);
  }

  return true;

}

bool imagesequence::setupFilters()
{
  bool filterDeviceFound(false);
  INDI_P *filterProp;
  INDIMenu *devMenu = ksw->getINDIMenu();
  if (devMenu == NULL)
     return false;

  filterCombo->clear();
  filterPosCombo->clear();
  
  filterCombo->insertItem(i18n("None"));

  // Second step is to check for filter wheel, it is only optional.
  for (uint i=0; i < devMenu->mgr.count(); i++)
  {
	for (uint j=0; j < devMenu->mgr.at(i)->indi_dev.count(); j++)
	{
  		filterProp = devMenu->mgr.at(i)->indi_dev.at(j)->findProp("FILTER_SLOT");
		if (!filterProp)
			 continue;

		filterDeviceFound = true;
		 
		if (devMenu->mgr.at(i)->indi_dev.at(j)->label.isEmpty())
			devMenu->mgr.at(i)->indi_dev.at(j)->label = devMenu->mgr.at(i)->indi_dev.at(j)->name;
				
		filterCombo->insertItem(devMenu->mgr.at(i)->indi_dev.at(j)->label);
			
	}
	
   }

  // If we found device, let's populate filters combo with aliases assigned to filter numbers
  // In Configure INDI
  if (filterDeviceFound)
  {
    filterCombo->setCurrentItem(lastFilter);
    currentFilter = filterCombo->currentText();
    updateFilterCombo(lastFilter);
    return true;
  }
  else
	return false;

}

void imagesequence::resetButtons()
{
  startB->setEnabled(true);
  stopB->setEnabled(false);

}

void imagesequence::startSequence()
{

   if (active)
    stopSequence();
    
  // Let's find out which device has been selected and that it's connected.
  if (!verifyCCDIntegrity())
   return;
  
 
  // Get expose paramater
  active 		= true;
  ISOStamp		= ISOCheck->isChecked() ? true : false;
  seqExpose		= exposureIN->value();
  seqTotalCount 	= countIN->value();
  seqCurrentCount	= 0;
  seqDelay		= delayIN->value() * 1000;		/* in ms */
  currentCCD		= CCDCombo->currentText();
  lastCCD              = CCDCombo->currentItem();
  currentFilter         = filterCombo->currentText();
  lastFilter            = filterCombo->currentItem();
  
  fullImgCountOUT->setText( TQString("%1").arg(seqTotalCount));
  currentImgCountOUT->setText(TQString("%1").arg(seqCurrentCount));
  
  // Ok, now let's connect signals and slots for this device
  connect(stdDevCCD, TQ_SIGNAL(FITSReceived(TQString)), this, TQ_SLOT(newFITS(TQString)));
  
  // set the progress info
  imgProgress->setEnabled(true);
  imgProgress->setTotalSteps(seqTotalCount);
  imgProgress->setProgress(seqCurrentCount);
  
  stdDevCCD->batchMode    = true;
  stdDevCCD->ISOMode      = ISOStamp;
  // Set this LAST
  stdDevCCD->updateSequencePrefix(prefixIN->text());
  
  
  // Update button status
  startB->setEnabled(false);
  stopB->setEnabled(true);
  
  prepareCapture();
}

void imagesequence::stopSequence()
{
 
  retries              = 0;
  seqTotalCount        = 0;
  seqCurrentCount      = 0;
  active               = false;
  
  imgProgress->setEnabled(false);
  fullImgCountOUT->setText("");
  currentImgCountOUT->setText("");
  
  resetButtons();
  seqTimer->stop();
  
  if (stdDevCCD)
  {
    stdDevCCD->setCount     = 0;
    stdDevCCD->batchMode    = false;
    stdDevCCD->ISOMode      = false;
    
    stdDevCCD->disconnect( TQ_SIGNAL(FITSReceived(TQString)));
  }

}

void imagesequence::checkCCD(int ccdNum)
{
  INDI_D *idevice = NULL;
  TQString targetCCD = CCDCombo->text(ccdNum);

  INDIMenu *imenu = ksw->getINDIMenu();
  if (!imenu)
  {
    KMessageBox::error(this, i18n("INDI Menu has not been initialized properly. Restart KStars."));
    return;
  }
  
  idevice = imenu->findDeviceByLabel(targetCCD);
  
  if (!idevice)
  {
	KMessageBox::error(this, i18n("INDI device %1 no longer exists.").arg(targetCCD));
        CCDCombo->removeItem(ccdNum);
        lastCCD = CCDCombo->currentItem();
        if (lastCCD != -1)
        	checkCCD(lastCCD);
    	return;
  }
  
  if (!idevice->isOn())
  {
        KMessageBox::error(this, i18n("%1 is disconnected. Establish a connection to the device using the INDI Control Panel.").arg(targetCCD));
		
	CCDCombo->setCurrentItem(lastCCD);
    	return;
  }
  
  currentCCD = targetCCD;

}

void imagesequence::newFITS(TQString deviceLabel)
{
  // If the FITS is not for our device, simply ignore
  if (deviceLabel != currentCCD)
   return;

  seqCurrentCount++;
  imgProgress->setProgress(seqCurrentCount);
  
  currentImgCountOUT->setText( TQString("%1").arg(seqCurrentCount));
  
  // if we're done
  if (seqCurrentCount == seqTotalCount)
  {
    stdDevCCD->batchMode    = false;
    stdDevCCD->ISOMode      = false;
    retries              = 0;
    seqTotalCount        = 0;
    seqCurrentCount      = 0;
    active               = false;
    seqTimer->stop();
    
    if (stdDevCCD)
    	stdDevCCD->disconnect( TQ_SIGNAL(FITSReceived(TQString)));
    
    resetButtons();
  }
  else
   seqTimer->start(seqDelay);
   
}


bool imagesequence::verifyCCDIntegrity()
{
  
  TQString targetCCD;
  INDI_D *idevice = NULL;
  INDI_P *exposeProp;
  INDI_E *exposeElem;
  stdDevCCD = NULL;
  
  INDIMenu *imenu = ksw->getINDIMenu();
  if (!imenu)
  {
    KMessageBox::error(this, i18n("INDI Menu has not been initialized properly. Restart KStars."));
    return false;
  }
  
  targetCCD = CCDCombo->currentText();

  if (targetCCD.isEmpty())
    return false;

  // #2 Check if the device exists
  idevice = imenu->findDeviceByLabel(targetCCD);
	
	
  if (!idevice)
  {
    	KMessageBox::error(this, i18n("INDI device %1 no longer exists.").arg(targetCCD));
        CCDCombo->removeItem(CCDCombo->currentItem());
        lastCCD = CCDCombo->currentItem();
    	return false;
  }
  
  if (!idevice->isOn())
  {
	    	KMessageBox::error(this, i18n("%1 is disconnected. Establish a connection to the device using the INDI Control Panel.").arg(currentCCD));
		
    		return false;
  }
  
  stdDevCCD = idevice->stdDev;
  
  exposeProp = stdDevCCD->dp->findProp("CCD_EXPOSE_DURATION");
  if (!exposeProp)
  {
    KMessageBox::error(this, i18n("Device does not support CCD_EXPOSE_DURATION property."));
    return false;
  }
  
  exposeElem = exposeProp->findElement("EXPOSE_DURATION");
  if (!exposeElem)
  {
    KMessageBox::error(this, i18n("CCD_EXPOSE_DURATION property is missing DURATION element."));
    return false;
  }
  
  return true;
}

bool imagesequence::verifyFilterIntegrity()
{

  TQString targetFilter;
  INDIMenu *devMenu = ksw->getINDIMenu();
  INDI_D *filterDevice (NULL);
  INDI_E *filterElem(NULL);
  if (devMenu == NULL)
     return false;

  targetFilter  = filterCombo->currentText();

  if (targetFilter.isEmpty() || targetFilter == i18n("None"))
  {
   filterPosCombo->clear();
   return false;
  }

  // #1 Check the device exists
  filterDevice = devMenu->findDeviceByLabel(targetFilter);
  if (filterDevice == NULL)
  {
    KMessageBox::error(this, i18n("INDI device %1 no longer exists.").arg(targetFilter));
    filterCombo->removeItem(filterCombo->currentItem());
    filterCombo->setCurrentItem(0);
    currentFilter = filterCombo->currentText();
    filterPosCombo->clear();
    stdDevFilter = NULL;
    return false;
  }

  // #2 Make sure it's connected
   if (!filterDevice->isOn())
  {
       KMessageBox::error(this, i18n("%1 is disconnected. Establish a connection to the device using the INDI Control Panel.").arg(targetFilter));
       filterCombo->setCurrentItem(0);
       currentFilter = filterCombo->currentText();
       filterPosCombo->clear();
       stdDevFilter = NULL;
       return false;
  }

  // #3 Make sure it has FILTER_SLOT std property by searching for its SLOT element
  filterElem = filterDevice->findElem("SLOT");
  if (filterElem == NULL)
  {
 	KMessageBox::error(this, i18n("Device does not support FILTER_SLOT property."));
        filterCombo->setCurrentItem(0);
        currentFilter = filterCombo->currentText();
        filterPosCombo->clear();
	stdDevFilter = NULL;
        return false;
  }

  stdDevFilter  = filterDevice->stdDev;
  lastFilter    = filterCombo->currentItem();
  currentFilter = targetFilter;

  // We're good
  return true;

}

void imagesequence::prepareCapture()
{
  INDI_P * tempProp(NULL);

  // Do we need to select filter First??
  if (currentFilter.isEmpty() || currentFilter == i18n("None"))
   captureImage();
  else
  {
     if (!verifyFilterIntegrity())
     {
        stopSequence();
	return;
     }

     if ( stdDevFilter && ((tempProp = stdDevFilter->dp->findProp("FILTER_SLOT")) != NULL))
     {
     connect (tempProp, TQ_SIGNAL(okState()), this, TQ_SLOT(captureImage()));
     selectFilter();
     }
     else
       kdDebug() << "Error: std Filter device lost or missing FILTER_SLOT property" << endl;
  }
    
}
  
void imagesequence::captureImage()
{

  INDI_P * exposeProp(NULL);
  INDI_E * exposeElem(NULL);
  INDI_P * tempProp(NULL);
  
  // Let's capture a new frame in acoord with the settings
  // We need to take into consideration the following conditions:
  // A. The device has been disconnected.
  // B. The device has been lost.
  // C. The property is still busy.
  // D. The property has been lost.
  
  if ( stdDevFilter && ((tempProp = stdDevFilter->dp->findProp("FILTER_SLOT")) != NULL))
    	tempProp->disconnect( TQ_SIGNAL (okState()));

  if (!verifyCCDIntegrity())
  {
    stopSequence();
    return;
  }
  
  exposeProp = stdDevCCD->dp->findProp("CCD_EXPOSE_DURATION");
  exposeElem = exposeProp->findElement("EXPOSE_DURATION");
  
  // disable timer until it's called again by newFITS, or called for retries
  seqTimer->stop();
  
  // Make sure it's not busy, if it is then schedual.
  if (exposeProp->state == PS_BUSY)
  {
    retries++;
    
    if (retries > RETRY_MAX)
    {
      seqTimer->stop();
      KMessageBox::error(this, i18n("Device is busy and not responding."));
      stopSequence();
      retries = 0;
      return;
    }
    
    seqTimer->start(RETRY_PERIOD);
  }
  
  // Set duration if applicable. We check the property permission, min, and max values
  if (exposeProp->perm == PP_RW || exposeProp->perm == PP_WO)
  {
    if (seqExpose < exposeElem->min || seqExpose > exposeElem->max)
    {
      stopSequence();
      KMessageBox::error(this, i18n("Expose duration is invalid. %1 supports expose durations from %2 to %3 seconds only.").arg(currentCCD).arg(exposeElem->min).arg(exposeElem->max));
      return;
    }
    
    // we're okay, set it
    exposeElem->targetValue = seqExpose;
    if (exposeElem->spin_w)
    {
      exposeElem->spin_w->setValue(seqExpose);
      exposeElem->spinChanged(seqExpose);
    }
    else
     exposeElem->write_w->setText( TQString("%1").arg(seqExpose));
     
  }
      
  // We're done! Send it to the driver
  exposeProp->newText();

}

void imagesequence::updateFilterCombo(int filterNum)
{
  INDIMenu *devMenu = ksw->getINDIMenu();
  TQStringList filterList;
  INDI_E *filterElem;
  unsigned int filterMax;

  if (!verifyFilterIntegrity())
    return;

  filterList = Options::filterAlias();

  filterElem = devMenu->findDeviceByLabel(filterCombo->text(filterNum))->findElem("SLOT");
  filterMax = (int) filterElem->max; 
  
  // Clear combo
  filterPosCombo->clear();

  if (filterList.empty())
   for (unsigned int i=0; i <= filterMax; i++)
      filterList << TQString("%1").arg(i);

  // Fill filter combo
  if (filterList.count() <= filterMax)
  {
      filterPosCombo->insertStringList(filterList);
      for (unsigned int i = filterList.count() ; i <= filterMax ; i++)
          filterPosCombo->insertItem(TQString("%1").arg(i));
  } else
  {
      // filterMax < filterList.count()
      for (unsigned int i = 0 ; i <= filterMax ; i++)
        filterPosCombo->insertItem(TQString("%1").arg(filterList[i]));
   
  }
 
   filterPosCombo->setCurrentItem(((int) filterElem->value));

}

void imagesequence::selectFilter()
{

  INDI_P * filterProp(NULL);
  INDI_E * filterElem(NULL);
  INDI_D * filterDev(NULL);
  INDIMenu *devMenu = ksw->getINDIMenu();

  // Let's select a new filter in acoord with the settings
  // We need to take into consideration the following conditions:
  // A. The device has been disconnected.
  // B. The device has been lost.
  // C. The property is still busy.
  // D. The property has been lost.
  
  // We have a filter, let's check if it's valid
  if (!verifyFilterIntegrity())
   return;

  filterDev = devMenu->findDeviceByLabel(currentFilter);
  filterProp = filterDev->findProp("FILTER_SLOT");
  filterElem = filterProp->findElement("SLOT");

  // Do we need to change the filter position??
  if (filterPosCombo->currentItem() == filterElem->read_w->text().toInt())
  {
	captureImage();
	return;
  }

  if (filterProp->perm == PP_RW || filterProp->perm == PP_WO)
  {
    filterElem->targetValue = filterPosCombo->currentItem();
    if (filterElem->spin_w)
    {
      filterElem->spin_w->setValue(filterElem->targetValue);
      filterElem->spinChanged(filterElem->targetValue);
    }
    else
     filterElem->write_w->setText(TQString("%1").arg(filterElem->targetValue));
      
    // We're done! Send it to the driver
    filterProp->newText();
  }

}

#include "imagesequence.moc"

