/* 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
 */


/////////////////////// Qt includes
#include <QMessageBox>
#include <QFileDialog>
#include <QApplication>
#include <QClipboard>


/////////////////////// Local includes
#include <massxpert/gui/MzCalculationDlg.hpp>
#include <massxpert/nongui/PolChemDef.hpp>
#include <massxpert/gui/SpectrumCalculationDlg.hpp>


namespace msXpSmassXpert
{

MzCalculationDlg::MzCalculationDlg(QWidget *parent,
                                   const QString &configSettingsFilePath,
                                   const PolChemDef *polChemDef,
                                   const msXpSlibmass::IonizeRule *ionizeRule,
                                   double mono,
                                   double avg)
  : QDialog(parent),
    m_configSettingsFilePath{configSettingsFilePath},
    mp_polChemDef{polChemDef},
    m_atomList{mp_polChemDef->atomList()}
{
  Q_ASSERT(parent);
  Q_ASSERT(polChemDef);
  Q_ASSERT(ionizeRule);

  m_ionizeRule = *ionizeRule;

  m_ui.setupUi(this);

  setupTreeView();

  if(mono != 0)
    m_ui.monoLineEdit->setText(
      QString("%1").arg(mono, 0, 'f', msXpS::OLIGOMER_DEC_PLACES));

  if(avg != 0)
    m_ui.avgLineEdit->setText(
      QString("%1").arg(avg, 0, 'f', msXpS::OLIGOMER_DEC_PLACES));

  if(mono || avg)
    {
      // If we are setting masses directly from here, then that means
      // that the formula line edit must be disabled.
      m_ui.formulaCheckBox->setCheckState(Qt::Unchecked);
      m_ui.startFormulaLineEdit->setDisabled(true);
    }
  else
    {
      // No masses are being preseeded. So let the user the full
      // opportunity ot enter a formula.
      m_ui.formulaCheckBox->setCheckState(Qt::Checked);
      m_ui.startFormulaLineEdit->setDisabled(false);
    }

  m_ui.srcIonizeRuleChargeSpinBox->setRange(1, 1000000000);
  m_ui.srcIonizeRuleLevelSpinBox->setRange(0, 1000000000);

  m_ui.destIonizeRuleChargeSpinBox->setRange(1, 1000000000);
  m_ui.destIonizeRuleStartLevelSpinBox->setRange(0, 1000000000);
  m_ui.destIonizeRuleEndLevelSpinBox->setRange(0, 1000000000);


  m_ui.srcIonizeRuleFormulaLineEdit->setText(m_ionizeRule.formula());
  m_ui.srcIonizeRuleChargeSpinBox->setValue(m_ionizeRule.charge());
  m_ui.srcIonizeRuleLevelSpinBox->setValue(m_ionizeRule.level());

  m_ui.destIonizeRuleFormulaLineEdit->setText(m_ionizeRule.formula());
  m_ui.destIonizeRuleChargeSpinBox->setValue(m_ionizeRule.charge());
  m_ui.destIonizeRuleStartLevelSpinBox->setValue(m_ionizeRule.level());
  m_ui.destIonizeRuleEndLevelSpinBox->setValue(m_ionizeRule.level());


  // The results-exporting menus. ////////////////////////////////

  QStringList comboBoxItemList;

  comboBoxItemList << tr("To Clipboard") << tr("To File") << tr("Select File")
                   << tr("Calculate spectrum");

  m_ui.exportResultsComboBox->addItems(comboBoxItemList);

  connect(m_ui.exportResultsComboBox,
          SIGNAL(activated(int)),
          this,
          SLOT(exportResults(int)));

  mpa_resultsString = new QString();

  //////////////////////////////////// The results-exporting menus.


  QSettings settings(m_configSettingsFilePath, QSettings::IniFormat);

  settings.beginGroup("mz_calculation_dlg");

  restoreGeometry(settings.value("geometry").toByteArray());

  m_ui.ponderableSplitter->restoreState(
    settings.value("ponderableSplitter").toByteArray());

  settings.endGroup();

  connect(m_ui.calculatePushButton, SIGNAL(clicked()), this, SLOT(calculate()));

  connect(m_ui.formulaCheckBox,
          SIGNAL(toggled(bool)),
          this,
          SLOT(formulaCheckBoxToggled(bool)));
}


MzCalculationDlg::~MzCalculationDlg()
{
  delete mpa_resultsString;

  delete mpa_mzTreeViewModel;

  delete mpa_mzProxyModel;

  freeIonizableList();

  while(!m_oligomerList.isEmpty())
    delete m_oligomerList.takeFirst();
}


void
MzCalculationDlg::closeEvent(QCloseEvent *event)
{
  if(event)
    printf("%s", "");

  QSettings settings(m_configSettingsFilePath, QSettings::IniFormat);

  settings.beginGroup("mz_calculation_dlg");

  settings.setValue("geometry", saveGeometry());

  settings.setValue("ponderableSplitter", m_ui.ponderableSplitter->saveState());

  settings.endGroup();
}


void
MzCalculationDlg::setupTreeView()
{
  // Model stuff all thought for sorting.
  mpa_mzTreeViewModel = new MzCalculationTreeViewModel(&m_ionizableList, this);

  mpa_mzProxyModel = new MzCalculationTreeViewSortProxyModel(this);
  mpa_mzProxyModel->setSourceModel(mpa_mzTreeViewModel);

  m_ui.mzTreeView->setModel(mpa_mzProxyModel);
  m_ui.mzTreeView->setParentDlg(this);
  mpa_mzTreeViewModel->setTreeView(m_ui.mzTreeView);
}


void
MzCalculationDlg::formulaCheckBoxToggled(bool checked)
{
  // If checked, then the user is responsible for entering a valid
  // formula.

  if(checked)
    {
      m_ui.startFormulaLineEdit->setDisabled(true);
    }
  else
    {
      m_ui.startFormulaLineEdit->setDisabled(false);
    }
}


void
MzCalculationDlg::calculate()
{
  // The calculation can be accomplished in two different manners:
  //
  // 1. Starting from a formula, in which case it will be necessary
  // to know what is the ionization status of the analyte for which
  // the formula is entered. This status is found in the source
  // ionizeRule. Once this is done, the masses can be computed for
  // the formula and are programmatically set to the mono and avg
  // m/z line edit widgets.
  //
  // 2. Starting from mono and avg m/z values immediately. Uncheck
  // the Formula check box in this case.

  // First get all the data for the source ionizable.

  // Because this function can be called multiple times from the
  // same dialog window, make sure that the formula is cleared.
  m_formula.clear();

  // The source ionizeRule. Start with this because we'll need right
  // away.
  msXpSlibmass::IonizeRule srcIonizeRule;
  if(!getSrcIonizeRuleData(&srcIonizeRule))
    return;

  // Are we being given a formula to start with?

  bool fromFormula = false;

  if(m_ui.formulaCheckBox->checkState() == Qt::Checked)
    {
      QString text = m_ui.startFormulaLineEdit->text();

      if(text.isEmpty())
        {
          // Then the masses must be null.
          m_ui.monoLineEdit->setText("0.000");
          m_ui.avgLineEdit->setText("0.000");

          return;
        }

      msXpSlibmass::Formula formula(text);

      // Do not bother storing all the atoms in the formula, this is
      // only a check (false param below).  m_atomList is a reference to
      // the atom list of the polymer chemistry definition currently
      // used in the caller calculator window.

      if(!formula.validate(m_atomList, false, true))
        {
          m_ui.monoLineEdit->setText("0.000");
          m_ui.avgLineEdit->setText("0.000");

          QMessageBox::warning(this,
                               tr("massXpert - m/z Ratio Calculator"),
                               tr("%1@%2\n"
                                  "Failed to validate the formula.")
                                 .arg(__FILE__)
                                 .arg(__LINE__),
                               QMessageBox::Ok);

          return;
        }

      // qDebug() << __FILE__ << __LINE__
      //          << "Formula:" << formula.formula() << "validated." ;

      // At this point we know we could actually validate the formula,
      // so do that work with the member formula:

      // We want to validate the formula and in the mean time construct
      // the list of all the AtomCount objects(first true), and since
      // the formula is reused we also ensure that that list is reset
      // (second true). m_atomList is a reference to the atom list of
      // the polymer chemistry definition currently used in the caller
      // calculator window.

      m_formula.setFormula(text);
      m_formula.validate(m_atomList, true, true);

      // Calculation of the m/z corresponding to the formula requires
      // dividing the mass obtained for the formula by the product
      // (level * unitary) of the ionizerule.

      int charge = srcIonizeRule.charge() * srcIonizeRule.level();

      double monoMass = 0;
      double avgMass  = 0;

      // It is impossible that we have an error here, since the formula
      // was previously validated.
      if(!m_formula.accountMasses(m_atomList, &monoMass, &avgMass, 1))
        qFatal("Fatal error at %s@%d.Aborting.", __FILE__, __LINE__);

      if(charge)
        {
          monoMass /= charge;
          avgMass /= charge;
        }

      // Great, now show the data.

      QString mz;

      mz.setNum(monoMass, 'f', msXpS::OLIGOMER_DEC_PLACES);
      // qDebug() << __FILE__ << __LINE__
      //          << "monoMass:" << monoMass;

      m_ui.monoLineEdit->setText(mz);

      mz.setNum(avgMass, 'f', msXpS::OLIGOMER_DEC_PLACES);
      // qDebug() << __FILE__ << __LINE__
      //          << "avgMass:" << avgMass;

      m_ui.avgLineEdit->setText(mz);

      fromFormula = true;
    }

  // Whatever the way we get the mon/avg mz values, make a
  // ponderable with them.
  msXpSlibmass::Ponderable *ponderable = getSourcePonderable();
  if(!ponderable)
    return;

  // Create an Ionizable with empty masses but with proper
  // IonizeRule. Note that we check if the IonizeRule has a non-0
  // level, in which case we tell so to the caller with a true
  // boolean.
  Ionizable ionizable(mp_polChemDef,
                      "NOT_SET",
                      *ponderable,
                      srcIonizeRule,
                      (srcIonizeRule.level() ? true : false));

  // At this point the ionizable is fully qualified.

  // No longer in use.
  delete ponderable;

  // Now, because we want to be able to compute elemental
  // compositions for each destination ionization level, we have to
  // kraft an elemental representation of what the molecule
  // is. This, only if the mz values above were calculated with a
  // provided formula.
  if(fromFormula)
    {
      // The text string in the startFormulaLineEdit widget
      // corresponds to the molecular entity already ionized with
      // srcIonizeRule (if level is non-zero). So, to go back to
      // non-ionized elemental composition, we have to remove from
      // the formula the equivalent atoms of the source ionizerule
      // multiplied by ionization level.

      // qDebug() << __FILE__ << __LINE__
      //          << "Formula before deionization:" << m_formula.text();

      m_formula.accountInList(
        srcIonizeRule.formula(), m_atomList, -1 * srcIonizeRule.level());

      // qDebug() << __FILE__ << __LINE__
      //          << "Formula after deionization:" << m_formula.text();
    }

  // At this point, the source ionizable is filled with data. And we
  // know the data are correct because everything was validated.

  // The destination ionizeRule(note that the m_level member datum
  // is not set as it will be generated during the for loop later;
  // this does not impede validation of the IonizeRule).
  msXpSlibmass::IonizeRule destIonizeRule;
  if(!getDestIonizeRuleData(&destIonizeRule))
    return;

  // Get the destination start/end ionization levels and make sure
  // they are in an increasing order.
  int startLevel = m_ui.destIonizeRuleStartLevelSpinBox->value();
  int endLevel   = m_ui.destIonizeRuleEndLevelSpinBox->value();

  if(startLevel > endLevel)
    {
      int temp   = endLevel;
      endLevel   = startLevel;
      startLevel = temp;
    }

  // Empty the treeView model, so that we can put fresh data in
  // it. Same for the oligomer list!

  mpa_mzTreeViewModel->removeAll();

  while(!m_oligomerList.isEmpty())
    delete m_oligomerList.takeFirst();

  for(int level = startLevel; level < endLevel + 1; ++level)
    {
      destIonizeRule.setLevel(level);

      // Reionize the ionizable.
      if(ionizable.ionize(destIonizeRule) == -1)
        {
          QMessageBox::warning(this,
                               tr("massXpert - m/z Ratio Calculator"),
                               tr("%1@%2\n"
                                  "Failed to perform reionization.")
                                 .arg(__FILE__)
                                 .arg(__LINE__),
                               QMessageBox::Ok);

          return;
        }

      Ionizable *newIonizable = new Ionizable(ionizable);

      mpa_mzTreeViewModel->addIonizable(newIonizable);

      // Kraft an oligomer, that we'll set apart in case the user
      // wants to compute a spectrum to simulate the charge envelope
      // peaks.

      Oligomer *oligomer = new Oligomer(ionizable, CalcOptions(), -1, -1);

      // qDebug() << __FILE__ << __LINE__
      //          << "oligomer->charge:" << oligomer->charge();

      // If we are working from a formula, then compute the new
      // formula for the present ionization status and create a
      // property with that new formula to append to the list of
      // properties in the oligomer.

      if(fromFormula)
        {
          msXpSlibmass::Formula newFormula(m_formula);

          newFormula.accountInList(
            destIonizeRule.formula(), m_atomList, destIonizeRule.level());

          // qDebug() << __FILE__ << __LINE__
          //          << "Formula after reionization (level)"
          //          << level
          //          << newFormula.text();


          StringProp *prop =
            new StringProp("ELEMENTAL_COMPOSITION", newFormula.text());

          oligomer->appendProp(static_cast<Prop *>(prop));
        }

      m_oligomerList.append(oligomer);
    }
}


bool
MzCalculationDlg::getSrcIonizeRuleData(msXpSlibmass::IonizeRule *ionizeRule)
{
  Q_ASSERT(ionizeRule);

  ionizeRule->setFormula(m_ui.srcIonizeRuleFormulaLineEdit->text());
  ionizeRule->setCharge(m_ui.srcIonizeRuleChargeSpinBox->value());
  ionizeRule->setLevel(m_ui.srcIonizeRuleLevelSpinBox->value());

  if(!ionizeRule->validate(mp_polChemDef->atomList()))
    {
      QMessageBox::warning(this,
                           tr("massXpert - m/z Ratio Calculator"),
                           tr("%1@%2\n"
                              "Failed to validate initial ionization rule.")
                             .arg(__FILE__)
                             .arg(__LINE__),
                           QMessageBox::Ok);

      return false;
    }

  return true;
}


bool
MzCalculationDlg::getDestIonizeRuleData(msXpSlibmass::IonizeRule *ionizeRule)
{
  Q_ASSERT(ionizeRule);

  ionizeRule->setFormula(m_ui.destIonizeRuleFormulaLineEdit->text());
  ionizeRule->setCharge(m_ui.destIonizeRuleChargeSpinBox->value());
  // The level is dealt with in another function, as it might
  // encompass more than one single value(see calculate()). Note
  // that if level is 0, this does not invalidate the ionizeRule.

  if(!ionizeRule->validate(mp_polChemDef->atomList()))
    {
      QMessageBox::warning(this,
                           tr("massXpert - m/z Ratio Calculator"),
                           tr("%1@%2\n"
                              "Failed to validate target ionization rule.")
                             .arg(__FILE__)
                             .arg(__LINE__),
                           QMessageBox::Ok);

      return false;
    }

  return true;
}


msXpSlibmass::Ponderable *
MzCalculationDlg::getSourcePonderable()
{
  QString text;

  double mono = 0;
  double avg  = 0;

  bool ok = false;

  // Mono mass
  text = m_ui.monoLineEdit->text();

  if(text.isEmpty())
    mono = 0;
  else
    {
      mono = text.toDouble(&ok);

      if(!mono && !ok)
        {
          QMessageBox::warning(this,
                               tr("massXpert - m/z Ratio Calculator"),
                               tr("%1@%2\n"
                                  "Failed to read mono mass.")
                                 .arg(__FILE__)
                                 .arg(__LINE__),
                               QMessageBox::Ok);

          return 0;
        }
    }

  // Avg mass
  text = m_ui.avgLineEdit->text();

  if(text.isEmpty())
    avg = 0;
  else
    {
      avg = text.toDouble(&ok);

      if(!avg && !ok)
        {
          QMessageBox::warning(this,
                               tr("massXpert - m/z Ratio Calculator"),
                               tr("%1@%2\n"
                                  "Failed to read mono mass.")
                                 .arg(__FILE__)
                                 .arg(__LINE__),
                               QMessageBox::Ok);

          return 0;
        }
    }

  // At this point we can allocate the Ponderable:
  return new msXpSlibmass::Ponderable(mono, avg);
}


void
MzCalculationDlg::freeIonizableList()
{
  while(!m_ionizableList.isEmpty())
    delete m_ionizableList.takeFirst();
}


// The results-exporting functions. ////////////////////////////////
// The results-exporting functions. ////////////////////////////////
// The results-exporting functions. ////////////////////////////////
void
MzCalculationDlg::exportResults(int index)
{
  // Remember that we had set up the combobox with the following strings:
  // << tr("To &Clipboard")
  // << tr("To &File")
  // << tr("&Select File");

  if(index == 0)
    {
      exportResultsClipboard();
    }
  else if(index == 1)
    {
      exportResultsFile();
    }
  else if(index == 2)
    {
      selectResultsFile();
    }
  else if(index == 3)
    {
      calculateSpectrum();
    }
  else
    Q_ASSERT(0);
}


void
MzCalculationDlg::prepareResultsTxtString()
{
  mpa_resultsString->clear();

  *mpa_resultsString += QObject::tr(
    "# \n"
    "# ---------------------------\n"
    "# m/z Calculations: \n"
    "# ---------------------------\n");

  QString value;

  *mpa_resultsString += QObject::tr(
    "\nSource conditions:\n"
    "------------------\n");

  *mpa_resultsString +=
    QObject::tr("Mono mass: %1").arg(m_ui.monoLineEdit->text());

  *mpa_resultsString +=
    QObject::tr(" - Avg mass: %1\n").arg(m_ui.avgLineEdit->text());

  *mpa_resultsString += QObject::tr("Ionization formula: %1\n")
                          .arg(m_ui.srcIonizeRuleFormulaLineEdit->text());

  *mpa_resultsString += QObject::tr("Ionization charge: %1 - ")
                          .arg(m_ui.srcIonizeRuleChargeSpinBox->value());

  *mpa_resultsString += QObject::tr("Ionization level: %1\n")
                          .arg(m_ui.srcIonizeRuleLevelSpinBox->value());

  *mpa_resultsString += QObject::tr(
    "\nDestination conditions:\n"
    "-----------------------\n");

  *mpa_resultsString += QObject::tr("Ionization formula: %1\n")
                          .arg(m_ui.destIonizeRuleFormulaLineEdit->text());

  *mpa_resultsString += QObject::tr("Ionization charge: %1 - ")
                          .arg(m_ui.destIonizeRuleChargeSpinBox->value());

  *mpa_resultsString += QObject::tr("Start level: %1 - ")
                          .arg(m_ui.destIonizeRuleStartLevelSpinBox->value());

  *mpa_resultsString += QObject::tr("End level: %1\n\n\n")
                          .arg(m_ui.destIonizeRuleEndLevelSpinBox->value());

  MzCalculationTreeViewModel *model =
    static_cast<MzCalculationTreeViewModel *>(m_ui.mzTreeView->model());
  Q_ASSERT(model);

  int rowCount = model->rowCount();
  //   qDebug() << __FILE__ << __LINE__ << "rowCount" << rowCount;
  if(!rowCount)
    return;

  QString mzString;

  for(int iter = 0; iter < rowCount; ++iter)
    {
      QModelIndex currentIndex =
        model->index(iter, MZ_CALC_LEVEL_COLUMN, QModelIndex());
      Q_ASSERT(currentIndex.isValid());

      bool ok = false;

      QString valueString =
        model->data(currentIndex, Qt::DisplayRole).toString();

      double value = valueString.toDouble(&ok);

      if(!value && !ok)
        qFatal("Fatal error at %s@%d. Program aborted.", __FILE__, __LINE__);

      mzString += QObject::tr("Level: %1 -- ")
                    .arg(value, 0, 'f', msXpS::PH_PKA_DEC_PLACES);

      currentIndex = model->index(iter, MZ_CALC_MONO_COLUMN, QModelIndex());
      Q_ASSERT(currentIndex.isValid());

      ok = false;

      valueString = model->data(currentIndex, Qt::DisplayRole).toString();

      value = valueString.toDouble(&ok);

      if(!value && !ok)
        qFatal("Fatal error at %s@%d. Program aborted.", __FILE__, __LINE__);

      mzString += QObject::tr("Mono: %1 -- ")
                    .arg(value, 0, 'f', msXpS::OLIGOMER_DEC_PLACES);

      currentIndex = model->index(iter, MZ_CALC_AVG_COLUMN, QModelIndex());
      Q_ASSERT(currentIndex.isValid());

      ok = false;

      valueString = model->data(currentIndex, Qt::DisplayRole).toString();

      value = valueString.toDouble(&ok);

      if(!value && !ok)
        qFatal("Fatal error at %s@%d. Program aborted.", __FILE__, __LINE__);

      mzString +=
        QObject::tr("Avg: %1").arg(value, 0, 'f', msXpS::OLIGOMER_DEC_PLACES);

      mzString += ("\n");
    }

  *mpa_resultsString += mzString;
}


bool
MzCalculationDlg::exportResultsClipboard()
{
  prepareResultsTxtString();

  QClipboard *clipboard = QApplication::clipboard();

  clipboard->setText(*mpa_resultsString, QClipboard::Clipboard);

  return true;
}


bool
MzCalculationDlg::exportResultsFile()
{
  if(m_resultsFilePath.isEmpty())
    {
      if(!selectResultsFile())
        return false;
    }

  QFile file(m_resultsFilePath);

  if(!file.open(QIODevice::WriteOnly | QIODevice::Append))
    {
      QMessageBox::information(this,
                               tr("massXpert - Export Data"),
                               tr("Failed to open file in append mode."),
                               QMessageBox::Ok);
      return false;
    }

  QTextStream stream(&file);
  stream.setCodec("UTF-8");

  prepareResultsTxtString();

  stream << *mpa_resultsString;

  file.close();

  return true;
}


bool
MzCalculationDlg::selectResultsFile()
{
  m_resultsFilePath =
    QFileDialog::getSaveFileName(this,
                                 tr("Select File To Export Data To"),
                                 QDir::homePath(),
                                 tr("Data files(*.dat *.DAT)"));

  if(m_resultsFilePath.isEmpty())
    return false;

  return true;
}


bool
MzCalculationDlg::calculateSpectrum()
{
  SpectrumCalculationDlg *dlg =
    new SpectrumCalculationDlg(this,
                               m_configSettingsFilePath,
                               mp_polChemDef->atomList(),
                               SPECTRUM_CALCULATION_MODE_SPECTRUM);

  // We now have to prepare the land for the calculations.

  dlg->setOligomerList(&m_oligomerList);

  // We really need that the dialog be set modal because we want to
  // be able to access all the atom and oligomer data freely and
  // durably.
  dlg->setModal(true);

  dlg->show();

  return true;
}

//////////////////////////////////// The results-exporting functions.
//////////////////////////////////// The results-exporting functions.
//////////////////////////////////// The results-exporting functions.

} // namespace msXpSmassXpert
