/* BEGIN software license
 *
 * msXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright(C) 2009,...,2018 Filippo Rusconi
 *
 * http://www.msxpertsuite.org
 *
 * This file is part of the msXpertSuite project.
 *
 * The msXpertSuite project is the successor of the massXpert project. This
 * project now includes various independent modules:
 *
 * - massXpert, model polymer chemistries and simulate mass spectrometric data;
 * - mineXpert, a powerful TIC chromatogram/mass spectrum viewer/miner;
 *
 * This program 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 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 * END software license
 */


#include <qmath.h>
#include <omp.h>


/////////////////////// Qt includes
#include <QApplication>
#include <QMultiMap>


/////////////////////// Local includes
#include <libmass/MassSpectrum.hpp>
#include <minexpert/gui/ColorMapWnd.hpp>
#include <minexpert/gui/MainWindow.hpp>
#include <minexpert/nongui/MassSpecDataFileLoaderSqlite3.hpp>
#include <minexpert/nongui/MassSpecDataFileLoaderPwiz.hpp>
#include <minexpert/nongui/MassDataIntegrator.hpp>


namespace msXpSmineXpert
{


//! Initialize the widget count to 0.
int ColorMapWnd::widgetCounter = 0;


//! Construct an initialized ColorMapWnd instance.
ColorMapWnd::ColorMapWnd(QWidget *parent)
  : AbstractMultiPlotWnd{parent, "mineXpert" /*m_name*/, "Color-map" /*m_desc*/}
{
  setupWindow();
}


//! Destruct \c this ColorMapWnd instance.
ColorMapWnd::~ColorMapWnd()
{
  writeSettings();
}


//! This function does nothing.
AbstractPlotWidget *
ColorMapWnd::addPlotWidget(const QVector<double> &keyVector,
                           const QVector<double> &valVector,
                           const QString &desc,
                           const QColor &color,
                           const MassSpecDataSet *massSpecDataSet,
                           bool isMultiGraph)
{
  // This function is required because it is virtual void = 0 in the base
  // class, but we should never call it in this class.
  qFatal("Fatal error at %s@%d. Program aborted.", __FILE__, __LINE__);
}


void
ColorMapWnd::setupWindow()
{
  const QIcon transposeAxesxRangeIcon = QIcon(":/images/transpose-xy-axes.png");
  mp_transposeAxesAct =
    new QAction(transposeAxesxRangeIcon, "Transpose X and Y axes", this);
  mp_transposeAxesAct->setShortcuts(QList<QKeySequence>{
    QKeySequence(Qt::CTRL + Qt::Key_T, Qt::CTRL + Qt::Key_A)});
  mp_transposeAxesAct->setStatusTip("Transpose X and Y axes");

  // No connection of the action with a slot because that will happen only
  // upon creation of a new plot widget.

  if(mp_toolbar == Q_NULLPTR)
    qFatal(
      "Fatal error at %s@%d -- %s(). "
      "Program aborted.",
      __FILE__,
      __LINE__,
      __FUNCTION__);

  mp_toolbar->addAction(mp_transposeAxesAct);

  readSettings();
}


//! Compute a new color map for the data and display it in a new plot widget.
/*!

  Each time a new mass data file is loaded, and if it contains ion mobility
  mass spectrometry data, a new color map is computed for the newly loaded
  data. That color map is then displayed in a new plot widget. This is what
  this function does.

  \param massSpecDataSet point to the MassSpecDataSet instance that contains
  the mass data to use for the color map computation.

  \param ok pointer to boolean in which to store the result of the operation.

  \return the pointer to the newly allocated AbstractPlotWidget instance in
  which the color map was displayed. If an error occurs, Q_NULLPTR is returned.

*/
void
ColorMapWnd::initialColorMap(const MassSpecDataSet *massSpecDataSet,
                             AbstractPlotWidget *originWidget,
                             const QColor &color)
{
  // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "() - enter"
  //<< " - current thread id:" << QThread::currentThreadId()
  //<< "with massSpecDataSet:" << massSpecDataSet
  //<< "with originalWidget:" << originWidget
  //<< "with color:" << color;

  if(massSpecDataSet == Q_NULLPTR)
    qFatal("Fatal error at %s@%d. Program aborted.", __FILE__, __LINE__);

  mp_statusBarCancelPushButton->setVisible(true);

  // QThread will start the thread in which the integrator will work.
  QThread *thread = new QThread;

  // The integrator will perform the integration itself.
  MassDataIntegrator *integrator = new MassDataIntegrator(massSpecDataSet);

  // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()"
  //<< "Allocated new integrator:" << integrator;

  integrator->moveToThread(thread);

  // Calling thread->start (QThread::start()) will automatically call the
  // integrateToMz() function of the integrator.
  connect(thread, SIGNAL(started()), integrator, SLOT(fillInDtMzHash()));

  // When the integration is finished it emits resultsReadySignal and thus
  // we can stop the *real* thread in the QThread thread instance, using the
  // signal/slot mechanism. That is not like calling QThead::quit() !!!
  connect(integrator, SIGNAL(resultsReadySignal()), thread, SLOT(quit()));

  // This will be needed later.
  connect(integrator,
          &MassDataIntegrator::resultsReadySignal,
          this,
          [integrator, originWidget, color, this]() {
            initialColorMapDone(integrator, originWidget, color);
          });

  connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));

  connect(integrator,
          &MassDataIntegrator::updateFeedbackSignal,
          this,
          &AbstractMultiPlotWnd::updateFeedback);

  connect(this,
          &AbstractMultiPlotWnd::cancelOperationSignal,
          integrator,
          &MassDataIntegrator::cancelOperation);

  // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()"
  //<< " - current thread id:" << QThread::currentThreadId()
  //<< "going to thread->start()";

  thread->start();
}


void
ColorMapWnd::initialColorMapDone(MassDataIntegrator *integrator,
                                 AbstractPlotWidget *originWidget,
                                 QColor color)
{
  // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "() - enter"
  //<< " - current thread id:" << QThread::currentThreadId()
  //<< "with color:" << color;

  const MassSpecDataSet *massSpecDataSet = integrator->massSpecDataSet();

  // We need to get all the relevant data out of the integrator.

  // Color map stuff
  int dtValueCount = integrator->dtValueCount();
  int mzCells      = integrator->mzCells();
  double firstKey  = integrator->firstKey();
  double lastKey   = integrator->lastKey();
  double firstVal  = integrator->firstVal();
  double lastVal   = integrator->lastVal();

  int errorCode = integrator->errorCode();

  if(dtValueCount == 0)
    {
      // This is not necessarily an error, the loaded file might not be an
      // ion mobility mass spectrometry experiment.

      if(errorCode == -1)
        qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()"
                 << "There was an error while computing the color map.";
      else
        qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()"
                 << "Loaded file is not of an ion mobility mass spectrometry "
                    "experiment.";

      return;
    }

  // qDebug() << __FILE__ << __LINE__ << __FUNCTION__
  //<< "dtValueCount:" << dtValueCount << "mzCells:" << mzCells
  //<< "firstKey:" << firstKey << "lastKey:" << lastKey
  //<< "firstVal:" << firstVal << "lastVal:" << lastVal;

  ColorMapPlotWidget *widget = new ColorMapPlotWidget(
    this, m_name, m_desc, massSpecDataSet, massSpecDataSet->fileName());

  widget->setMinimumSize(400, 150);
  mp_scrollAreaVBoxLayout->addWidget(widget);

  // We may have gotten here because the operation was cancelled. In any case,
  // reset the boolean value.
  m_isOperationCancelled = false;

  // In some seconds, hide the progress bar and set the label to an empty
  // text.
  QTimer::singleShot(3000, this, [=]() {
    this->updateFeedback(
      "", -1, false /* setVisible */, LogType::LOG_TO_STATUS_BAR);
  });

  // Now, feed the widget with some data. We only allocate enough space
  // to fit the actual dt values that we could go through (if the operation
  // was cancelled then parsedDtValues is less than dtListSize).

  widget->setColorMapSizeAndRanges(dtValueCount,
                                   mzCells,
                                   QCPRange(firstKey, lastKey),
                                   QCPRange(firstVal, lastVal),
                                   0 /* fill with 0 */);

  // The version below will create as many cells in the two axes as there
  // are pixels in the widget.

  // widget->setColorMapSizeAndRanges(widgetSize.width(), widgetSize.height(),
  // QCPRange(dtList.first(), dtList.last()), QCPRange(minMz, maxMz), 0);

  widget->setPlottingColor(color);

  QHashIterator<double, msXpSlibmass::MassSpectrum *> iterator(
    integrator->doubleMassSpecHash());

  while(iterator.hasNext())
    {
      iterator.next();

      double dt                                = iterator.key();
      msXpSlibmass::MassSpectrum *massSpectrum = iterator.value();

      for(int lter = 0; lter < massSpectrum->size(); ++lter)
        {
          double mz = massSpectrum->at(lter)->key();
          double i  = massSpectrum->at(lter)->val();

          // This is the version for which the colormap is doing the calculation
          // itself to define what cell need to be dealt with, depending on the
          // values (dt, mz).  There might be something in the cell
          // corresponding to (dt, mz), thus try to get the value and make a
          // sum.

          double prevI = widget->colorMap()->data()->data(dt, mz);
          widget->setColorMapPointData(dt, mz, prevI + i);

          // qDebug("setting data: dt=%.5f, mz=%.5f, prevI = %.5f, i=%.5f",
          // dt, mz, prevI, i);
        }
    }

  // At this point we have finished filling-up the color map.

  // The gpThermal is certainly one of the best.
  widget->setColorMapGradient(QCPColorGradient::gpThermal);
  // widget->mp_colorMap->setGradient(QCPColorGradient::gpSpectrum);

  widget->rescaleColorMapDataRange(true);
  widget->rescaleAxes();
  widget->replot();

  // Because only the master thread can update the feedback data, the loop
  // might end while we have not reached an iterationCount corresponding to
  // 100%. This might make the user uncomfortable. So we simulate the 100%
  // achievement:
  updateFeedback("Computed MZ=DT color map",
                 dtValueCount,
                 true /* setVisible */,
                 LogType::LOG_TO_BOTH,
                 0,
                 dtValueCount);

  QTimer::singleShot(3000, this, [=]() {
    this->updateFeedback(
      "", -1, false /* setVisible */, LogType::LOG_TO_STATUS_BAR);
  });

  // Make sure the widget gets the signal from the tool bar.

  connect(mp_transposeAxesAct,
          &QAction::triggered,
          widget,
          &ColorMapPlotWidget::transposeAxes);


  ///////////////// BEGIN Script availability of the new widget
  /////////////////////

  // Craft a name that differentiates this widget from the previous and next
  // ones. We use the static counter number to affix to the string
  // "massSpec".

  QString propName =
    QString("colorMapPlotWidget%1").arg(ColorMapWnd::widgetCounter);
  QString alias = "lastColorMapPlotWidget";

  QString explanation =
    QString(
      "Make available a color map plot widget under name \"%1\" "
      "with alias \"%2\".\n")
      .arg(propName)
      .arg(alias);

  // Make the new plot widget globally available under the crafted property
  // name and alias. Also, put out the explanation text. Note that by
  // providing 'this', we make the new object appear in the scripting objet
  // tree widget as a child item of this.

  // Log the history to the console.
  MainWindow *mainWindow = static_cast<MainWindow *>(parent());

  QScriptValue scriptValue;
  mainWindow->mp_scriptingWnd->publishQObject(widget,
                                              this /* parent object */,
                                              propName,
                                              alias,
                                              explanation,
                                              QString() /* help string */,
                                              scriptValue);

  ++ColorMapWnd::widgetCounter;

  ///////////////// END Script availability of the new widget //////////////////


  // At this point, immediately craft a new HistoryItem instance to append
  // to the widget's History.

  History localHistory = integrator->history();

  // At this point, immediately craft a new HistoryItem instance to append
  // to the widget's History.

  widget->rhistory().copyHistory(localHistory, true /*remove duplicates*/);

  // Finally we can delete the integrator
  // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()"
  //<< "Finally deleting the integrator object:" << integrator;

  delete integrator;
  integrator = Q_NULLPTR;

  // Log the history to the console.
  mainWindow->logConsoleMessage(widget->history().innermostRangesAsText());

  // The plot widget reacts to the Qt::Key_Delete key by calling a function
  // that emits a set of signals, of which the destroyPlotWidget signal
  // actually indirectly triggers the destruction of the plot widget. This
  // signal is now connected to the main window where a function will call
  // for the destruction of all the target plot widgets, that is all the
  // plot widgets that were created as the result of an integration
  // computation of the plot widget currently being destroyed.

  connect(widget,
          &AbstractPlotWidget::destroyPlotWidget,
          mainWindow,
          &MainWindow::destroyAlsoAllTargetPlotWidgets);

  // Ensure that there is a connection between this plot widget and this
  // containing window, such that if there is a requirement to replot, the
  // widget gets the signal.
  connect(this,
          &ColorMapWnd::replotWithAxisRange,
          widget,
          &ColorMapPlotWidget::replotWithAxisRange);

  // Make sure we get the signal requiring to redraw the background of the
  // widget, depending on it being focused or not.
  connect(this,
          &AbstractMultiPlotWnd::redrawPlotBackground,
          widget,
          &AbstractPlotWidget::redrawPlotBackground);

  // At this point make the connections that will allows triggering the
  // mass spectrum and drift spectrum calculations based on the selections
  // over the colormap.

  connect(static_cast<ColorMapPlotWidget *>(widget),
          &ColorMapPlotWidget::newMassSpectrum,
          mainWindow->mp_massSpecWnd,
          &MassSpecWnd::newMassSpectrum);

  connect(static_cast<ColorMapPlotWidget *>(widget),
          &ColorMapPlotWidget::newDriftSpectrum,
          mainWindow->mp_driftSpecWnd,
          &DriftSpecWnd::newDriftSpectrum);

  connect(static_cast<ColorMapPlotWidget *>(widget),
          &ColorMapPlotWidget::newTicChromatogram,
          mainWindow->mp_ticChromWnd,
          &TicChromWnd::newTicChromatogram);

  connect(static_cast<ColorMapPlotWidget *>(widget),
          &ColorMapPlotWidget::ticIntensitySignal,
          this,
          &ColorMapWnd::ticIntensity);

  connect(widget,
          &AbstractPlotWidget::updateStatusBarSignal,
          this,
          &AbstractMultiPlotWnd::updateStatusBar);

  connect(this,
          &AbstractMultiPlotWnd::showHelpSummarySignal,
          widget,
          &AbstractPlotWidget::showHelpSummary);

  // Now call the main window of the program to finalize the widget
  // establishment.

  mainWindow->initialColorMapDone(widget, originWidget, massSpecDataSet, color);
}


void
ColorMapWnd::jsInitialColorMap(const MassSpecDataSet *massSpecDataSet,
                               AbstractPlotWidget *originPlotWidget,
                               const QColor &color)
{
  // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()";

  if(massSpecDataSet == Q_NULLPTR)
    qFatal("Fatal error at %s@%d. Program aborted.", __FILE__, __LINE__);

  MainWindow *mainWindow = static_cast<MainWindow *>(parent());
  mainWindow->logConsoleMessage("Computing MZ=DT color map");

  // Make sure the cancel button is visible.
  mp_statusBarCancelPushButton->setVisible(true);
  QApplication::processEvents();

  MassDataIntegrator *integrator = new MassDataIntegrator(massSpecDataSet);

  connect(integrator,
          &MassDataIntegrator::updateFeedbackSignal,
          this,
          &AbstractMultiPlotWnd::updateFeedback);

  connect(this,
          &AbstractMultiPlotWnd::cancelOperationSignal,
          integrator,
          &MassDataIntegrator::cancelOperation);

  // Fill-in the dt/mass spectrum hash. This is the hardest computing part
  // of the whole color map creation task.

  integrator->fillInDtMzHash();

  // Now go on with non-threaded function that will relay to the mainwindow.
  initialColorMapDone(integrator, originPlotWidget, color);
}


void
ColorMapWnd::ticIntensity(const MassSpecDataSet *massSpecDataSet,
                          const QString &msg,
                          History history)
{
  // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()";

  if(massSpecDataSet == Q_NULLPTR)
    qFatal("Fatal error at %s@%d. Program aborted.", __FILE__, __LINE__);

  mp_statusBarCancelPushButton->setVisible(true);

  AbstractPlotWidget *senderPlotWidget =
    static_cast<AbstractPlotWidget *>(QObject::sender());

  // QThread will start the thread in which the integrator will work.
  QThread *thread = new QThread;

  // The integrator will perform the integration itself.
  MassDataIntegrator *integrator =
    new MassDataIntegrator(massSpecDataSet, history);

  integrator->moveToThread(thread);

  // Calling thread->start (QThread::start()) will automatically call the
  // integrateToTicIntensity() function of the integrator.
  connect(
    thread, SIGNAL(started()), integrator, SLOT(integrateToTicIntensity()));

  // When the integration is finished it emits resultsReadySignal and thus
  // we can stop the *real* thread in the QThread thread instance, using the
  // signal/slot mechanism. That is not like calling QThead::quit() !!!
  connect(integrator, SIGNAL(resultsReadySignal()), thread, SLOT(quit()));

  // This will be needed later, when we'll create a data plot relation
  // object relating the sender plot widget with the newly created one (in
  // ticIntensityDone).
  connect(integrator,
          &MassDataIntegrator::resultsReadySignal,
          this,
          [integrator, msg, senderPlotWidget, this]() {
            ticIntensityDone(integrator, msg, senderPlotWidget);
          });

  connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));

  connect(integrator,
          &MassDataIntegrator::updateFeedbackSignal,
          this,
          &AbstractMultiPlotWnd::updateFeedback);

  connect(this,
          &AbstractMultiPlotWnd::cancelOperationSignal,
          integrator,
          &MassDataIntegrator::cancelOperation);

  // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()"
  //<< " - current thread id:" << QThread::currentThreadId();

  thread->start();
}


void
ColorMapWnd::ticIntensityDone(MassDataIntegrator *integrator,
                              QString msg,
                              AbstractPlotWidget *senderPlotWidget)
{
  // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()";

  // De-setup the feedback.
  QTimer::singleShot(2000, this, [=]() {
    updateFeedback("", -1, false /* setVisible */, LogType::LOG_TO_STATUS_BAR);
  });

  // Make sure the cancel button is hidden.
  mp_statusBarCancelPushButton->setVisible(false);

  // Set the proper value in the plot widget right away so that it can be
  // queried in another context.
  senderPlotWidget->setLastTicIntensity(integrator->ticIntensity());

  // Now we can dipslay the results in the status bar:
  statusBar()->showMessage(
    msg + QString(": TIC int.: %1").arg(integrator->ticIntensity()));

  // Finally we can destroy the integrator.
  delete integrator;
}


} // namespace msXpSmineXpert
