/* BEGIN software license
 *
 * MsXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright(C) 2009,...,2026 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 <QCloseEvent>
#include <QDebug>
#include <QSettings>

/////////////////////// libXpertMass includes
#include <MsXpS/libXpertMassCore/Oligomer.hpp>


/////////////////////// Local includes
#include "MzLabMatchBasedActionsDlg.hpp"
#include "MzLabWnd.hpp"
#include "MzLabInputOligomerTableViewDlg.hpp"
#include "MzLabOutputOligomerTableViewDlg.hpp"
#include "../nongui/OligomerPair.hpp"


namespace MsXpS
{

namespace MassXpert
{


MzLabMatchBasedActionsDlg::MzLabMatchBasedActionsDlg(
  QWidget *parent, const QString &configSettingsFilePath)
  : QDialog{parent}, m_configSettingsFilePath{configSettingsFilePath}
{
  if(!parent)
    qFatal("Fatal error at %s@%d. Program aborted.", __FILE__, __LINE__);

  mp_mzLabWnd = static_cast<MzLabWnd *>(parent);

  m_ui.setupUi(this);

  setWindowTitle(tr("massXpert: mz Lab - Match-based Actions"));

  // The tolerance when filtering mono/avg masses...
  QStringList stringList;

  stringList << tr("AMU") << tr("PCT") << tr("PPM");

  m_ui.toleranceComboBox->insertItems(0, stringList);

  m_ui.toleranceComboBox->setCurrentIndex(0);

  m_ui.toleranceComboBox->setToolTip(
    tr("AMU: atom mass unit \n"
       "PCT: percent \n"
       "PPM: part per million"));

  m_ui.toleranceLineEdit->setText("1");

  connect(m_ui.performMatchWorkPushButton,
          SIGNAL(clicked()),
          this,
          SLOT(performMatchWorkPushButton()));

  connect(m_ui.helpPushButton, SIGNAL(clicked()), this, SLOT(helpPushButton()));

  QSettings settings(mp_mzLabWnd->configSettingsFilePath(),
                     QSettings::IniFormat);
  settings.beginGroup("mz_lab_wnd_match_based_actions");
  restoreGeometry(settings.value("geometry").toByteArray());
  settings.endGroup();
}


MzLabMatchBasedActionsDlg::~MzLabMatchBasedActionsDlg()
{
}


void
MzLabMatchBasedActionsDlg::closeEvent(QCloseEvent *event)
{
  QSettings settings(mp_mzLabWnd->configSettingsFilePath(),
                     QSettings::IniFormat);
  settings.beginGroup("mz_lab_wnd_match_based_actions");
  settings.setValue("geometry", saveGeometry());
  settings.endGroup();

  QDialog::closeEvent(event);
}

double
MzLabMatchBasedActionsDlg::calculateTolerance(double value)
{
  int index    = m_ui.toleranceComboBox->currentIndex();
  QString text = m_ui.toleranceLineEdit->text();

  bool ok        = false;
  double nominal = qAbs(text.toDouble(&ok));

  if(!nominal && !ok)
    return -1;

  if(index == 0)
    {
      // MASS_TOLERANCE_AMU

      return nominal;
    }
  else if(index == 1)
    {
      // MASS_TOLERANCE_PCT

      return (nominal * (value / 100));
    }
  else if(index == 2)
    {
      // MASS_TOLERANCE_PPM

      return (nominal * (value / 1000000));
    }
  else
    Q_ASSERT(0);

  return -1;
}


void
MzLabMatchBasedActionsDlg::fillMolecularMassList(
  const libXpertMassCore::OligomerCollection &oligomerList,
  QList<double> *listM,
  libXpertMassCore::Enums::MassType &massType)
{
  Q_ASSERT(listM);

  double mass = 0;

  for(int iter = 0; iter < oligomerList.size(); ++iter)
    {
      libXpertMassCore::OligomerSPtr oligomer_sp = oligomerList.at(iter);
      mass = oligomer_sp->molecularMass(massType);

      if(mass < 0)
        qFatal("Fatal error at %s@%d. Program aborted. Mass was:%.5f",
               __FILE__,
               __LINE__,
               mass);

      listM->append(mass);
    }
}


void
MzLabMatchBasedActionsDlg::performMatchWorkPushButton()
{
  // We have to get the name of two lists. The first list's item
  // should be matched to the second list's items. The user might ask
  // that the second list's items be modified to match the first
  // list's ones.

  MzLabInputOligomerTableViewDlg *dlg1 = 0;
  MzLabInputOligomerTableViewDlg *dlg2 = 0;

  if(!mp_mzLabWnd->inputListsDlg(&dlg1, &dlg2))
    {
      QMessageBox::warning(this,
                           tr("massXpert: mz Lab"),
                           tr("This feature performs matches between "
                              "two input lists.\n"
                              "Please, select two input lists first."),
                           QMessageBox::Ok);
      return;
    }


  // The general idea is to first create for each m/z--z list a
  // corresponding list of M values(that is to first deionize the
  // m/z into M). Next, the comparison will be performed on the M
  // lists. This will ensure that all peaks are matched even if
  // simulation peaks are for z=1 and z=2, for example, and the
  // measured m/z has peaks with z=3.

  // The m/z--z lists and the M lists will retain a complete
  // correlation, that is the order of their components will not be
  // modified, and the their number neither. Which means that, for
  // example, the item in m/z list 1 [index 0] will correspond to
  // the item in M list [index 0].

  // The comparison of M list 1 with M list 2 will possibly yield
  // matches which will be reported in an output list of which the
  // items will retain links to the initial m/z lists 1 and 2. This
  // way, it will be possible to trace back the matched m/z items
  // for a given match.

  // The dlg1 holds the list that has the measured data(that is
  // factual m/z values that cannot be changed in any way). The dlg2
  // holds the theoretical m/z values that might undergo chemical
  // modifications in order to seek matches in the dlg1 list.

  // The two lists might not have the same item count.

  libXpertMassCore::Enums::MassType massType1 = dlg1->massType();
  libXpertMassCore::Enums::MassType massType2 = dlg2->massType();

  if(massType1 != massType2)
    {
      int ret =
        QMessageBox::question(this,
                              tr("MassXpert3 - mz Lab"),
                              tr("The lists to match are not of the same "
                                 "mass type.\n\nContinue?"),
                              QMessageBox::Yes | QMessageBox::No);

      if(ret == QMessageBox::No)
        return;
    }


  // For clearness...
  libXpertMassCore::Enums::MassType massType = massType1;

  //    qDebug() << __FILE__ << __LINE__
  //             << "massType:" << massType;

  // Define first the oligomer shell that we'll use later.
  OligomerPairSPtr oligomer_pair_sp = nullptr;

  // What kind of match are we willing?
  bool trueMatches = m_ui.trueMatchesCheckBox->isChecked();
  // qDebug() << __FILE__ << __LINE__
  //          << "trueMatches:" << trueMatches;

  bool falseMatches = m_ui.falseMatchesCheckBox->isChecked();
  // qDebug() << __FILE__ << __LINE__
  //          << "falseMatches:" << falseMatches;

  // Create the two lists that will hold the double values.

  // First list.

  const libXpertMassCore::OligomerCollection &listMz1 = dlg1->oligomerList();
  QList<double> listM1;
  fillMolecularMassList(listMz1, &listM1, massType);
  Q_ASSERT(listMz1.size() == listM1.size());

  // Second list.

  const libXpertMassCore::OligomerCollection &listMz2 = dlg2->oligomerList();
  QList<double> listM2;
  fillMolecularMassList(listMz2, &listM2, massType);
  Q_ASSERT(listMz2.size() == listM2.size());

  // Allocate the new list of OligomerPair instances into which
  // we'll append OligomerPairs instances corresponding to
  // successful matches between oligomers.

  std::vector<OligomerPairSPtr> oligomer_pairs;

  // At this point we have the two lists with molecular masses. We
  // can start doing the matches.

  for(int iter = 0; iter < listM1.size(); ++iter)
    {
      double mass1 = listM1.at(iter);

      double tolerance = calculateTolerance(mass1);

      double lowerBorder = mass1 - tolerance;
      double upperBorder = mass1 + tolerance;

      bool foundMatch = false;

      for(int jter = 0; jter < listM2.size(); ++jter)
        {
          double mass2 = listM2.at(jter);

          if(mass2 >= lowerBorder && mass2 <= upperBorder)
            {
              // qDebug() << __FILE__ << __LINE__
              //          << "M1 at index:" << iter
              //          << "matches"
              //          << "M2 at index:" << jter;

              // At this point we can create a new OligomerPair.
              if(trueMatches)
                {
                  OligomerPairSPtr oligomer_pair_sp =
                    std::make_shared<OligomerPair>(listMz1.at(iter),
                                                   listMz2.at(jter),
                                                   massType,
                                                   qAbs(mass2 - mass1),
                                                   true /* isMatching */,
                                                   "NOT_SET");

                  oligomer_pairs.push_back(oligomer_pair_sp);
                }

              // Whatever asked by the user (trueMatches or not), we
              // must tell the following code that a match was
              // found.
              foundMatch = true;
            }
        }

      // Now that we have finished dealing with mass1, let's check
      // if a match was found. If not and the non-matches are
      // requested, store a locally allocated oligomerPair.

      if(!foundMatch && falseMatches)
        {
          // Use the oligomer identical to the oligomer
          // that had no match in the listMz2.

          OligomerPairSPtr oligomer_pair_sp =
            std::make_shared<OligomerPair>(listMz1.at(iter),
                                           listMz1.at(iter),
                                           massType,
                                           0,
                                           false /* isMatching */,
                                           "NOT_SET");

          oligomer_pairs.push_back(oligomer_pair_sp);
        }
    }
  // End of
  // for (int iter = 0; iter < listM1.size(); ++iter)


  // qDebug() << __FILE__ << __LINE__
  //          << "Appended oligomer pair:"
  //          << oligomerPair->oligomer1()->name()
  //          << oligomerPair->oligomer2()->name()
  //          << "checksum:" << oligomerPair->oligomer1()->
  //   polymer()->checksum();

  //    qDebug() << __FILE__ << __LINE__
  //             << "Found pairs:" << oligomer_pairs->size();

  // Allocate the new dialog, we pass

  QString dialogName = tr("%1 vs %2").arg(dlg1->name()).arg(dlg2->name());

  // Ownership of oligomer_pairs is going to be transferred to the
  // dialog being created (note that to the dialog and not the table
  // view model in the dialog's table view).

  MzLabOutputOligomerTableViewDlg *dlg =
    new MzLabOutputOligomerTableViewDlg(this,
                                        std::move(oligomer_pairs),
                                        massType1,
                                        dlg1,
                                        dlg2,
                                        dlg1->name(),
                                        dlg2->name());
  dlg->show();
}


void
MzLabMatchBasedActionsDlg::helpPushButton()
{
  // Explain to the user what's the point of having matches
  // performed.

  QString msg(
    tr("To perform a match operation one must have\n"
       "two lists of (m/z,z) pairs already available.\n\n"
       "One list must be selected in the Catalogue 1\n"
       "and the other list must be selected in the Catalogue 2.\n\n"
       "There are two kinds of \"match work\" operations : \n"
       "One that aims at finding matches between (m/z,z) pairs\n"
       "in each of the two lists and one that aims at finding\n"
       "(m/z,z) pairs that have no match in the other list.\n\n"
       "How are match (or non-match) works performed ?\n"
       "One match work is performed by searching in\n"
       "the first list (Catalogue 1) all the (m/z,z) pairs \n"
       "that are also in the second list (Catalogue 2).\n\n"
       "One non-match work is performed by searching in the\n"
       "first list (Catalogue 1) all the (m/z,z) pairs that are\n"
       "not found in the second list (Catalogue 2).\n"));

  QMessageBox::warning(this, tr("massXpert: mz Lab"), msg, QMessageBox::Ok);
}


} // namespace MassXpert

} // namespace MsXpS
