/* 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 <QFileDialog>
#include <QSettings>

#include <pappsomspp/core/utils.h>

/////////////////////// libXpertMass includes
#include <MsXpS/libXpertMassCore/IsotopicDataUserConfigHandler.hpp>
#include <MsXpS/libXpertMassCore/IsotopicClusterGenerator.hpp>
#include <MsXpS/libXpertMassCore/IsotopicClusterShaper.hpp>
#include <MsXpS/libXpertMassCore/IsotopicData.hpp>
#include <MsXpS/libXpertMassCore/MassPeakShaper.hpp>
#include <MsXpS/libXpertMassCore/MassPeakShaperConfig.hpp>
#include <MsXpS/libXpertMassGui/ColorSelector.hpp>


/////////////////////// Local includes
#include "../nongui/globals.hpp"
#include "FragmentationDlg.hpp"
#include "PolChemDefRendering.hpp"
#include "PolChemDefRendering.hpp"


namespace MsXpS
{

namespace MassXpert
{


FragmentationDlg::FragmentationDlg(
  SequenceEditorWnd *editor_wnd_p,
  libXpertMassCore::PolymerQSPtr polymer_sp,
  const libXpertMassCore::PolChemDefCstSPtr pol_chem_def_csp,
  const QString &config_settings_file_path,
  const QString &application_name,
  const QString &description,
  const libXpertMassCore::CalcOptions &calc_options,
  const libXpertMassCore::Ionizer &ionizer)
  : AbstractSeqEdWndDependentDlg(editor_wnd_p,
                                 polymer_sp,
                                 pol_chem_def_csp,
                                 config_settings_file_path,
                                 "CompositionsDlg",
                                 application_name,
                                 description),
    m_calcOptions(calc_options),
    m_ionizer(ionizer)
{
  if(polymer_sp == nullptr || pol_chem_def_csp == nullptr)
    qFatal() << "Programming error. Pointer cannot be nullptr.";

  if(!initialize())
    qFatal() << "Programming error. Failed to initialize dialog window.";
}


FragmentationDlg::~FragmentationDlg()
{
  m_oligomers.clear();

  delete mpa_resultsString;

  delete mpa_oligomerTableViewModel;
  delete mpa_proxyModel;

  writeSettings();
}


void
FragmentationDlg::writeSettings()
{
  QSettings settings(m_configSettingsFilePath, QSettings::IniFormat);

  settings.beginGroup(m_wndTypeName);
  settings.setValue("geometry", saveGeometry());
  settings.setValue("oligomersSplitter", m_ui.oligomersSplitter->saveState());
  settings.setValue("oligoDetailsSplitter",
                    m_ui.oligoDetailsSplitter->saveState());

  settings.endGroup();
}


void
FragmentationDlg::readSettings()
{
  QSettings settings(m_configSettingsFilePath, QSettings::IniFormat);

  settings.beginGroup(m_wndTypeName);

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

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

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

  settings.endGroup();
}


bool
FragmentationDlg::initialize()
{
  m_ui.setupUi(this);

  setWindowIcon(qApp->windowIcon());

  // Update the window title because the window title element in m_ui might be
  // either erroneous or empty.
  setWindowTitle(
    QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription));

  readSettings();

  m_ui.delimiterLineEdit->setText("$");

  populateSelectedOligomerData();
  populateFragSpecList();

  m_ui.fragmentationPatternListWidget->setSelectionMode(
    QAbstractItemView::MultiSelection);

  mpa_oligomerTableViewModel = 0;
  mpa_proxyModel             = 0;

  // Set pointers to 0 so that after the setupTableView call below
  // they'll get their proper value. We'll then be able to free all
  // that stuff in the destructor.
  mpa_proxyModel             = 0;
  mpa_oligomerTableViewModel = 0;

  setupTableView();

  // Set the default color of the mass spectra trace upon mass spectrum
  // synthesis
  QColor color("black");
  // Now prepare the color in the form of a QByteArray
  QDataStream stream(&m_colorByteArray, QIODevice::WriteOnly);
  stream << color;

  // 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->setToolTip(
    tr("AMU: atom mass unit \n"
       "PCT: percent \n"
       "PPM: part per million"));

  filterAct = new QAction(tr("Toggle Filtering"), this);
  filterAct->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_F));
  this->addAction(filterAct);
  connect(filterAct, SIGNAL(triggered()), this, SLOT(filterOptionsToggled()));

  m_ui.filteringOptionsGroupBox->addAction(filterAct);
  // When the dialog box is created it is created with the groupbox
  // unchecked.
  m_ui.filteringOptionsFrame->setVisible(false);

  // When the filtering group box will be opened, the focus will be on the
  // first widget of the groupbox:
  mp_focusWidget = m_ui.filterPatternLineEdit;

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

  QStringList comboBoxItemList;

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

  m_ui.exportResultsComboBox->addItems(comboBoxItemList);

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

  comboBoxItemList.clear();

  // The color button
  connect(m_ui.colorSelectorPushButton,
          &QPushButton::clicked,
          this,
          &FragmentationDlg::traceColorPushButtonClicked);

  comboBoxItemList.insert((int)MassSpectrumSynthesisActions::LOAD_ISOTOPIC_DATA,
                          "Load isotopic data from file");

  comboBoxItemList.insert(
    (int)MassSpectrumSynthesisActions::CONFIGURE_MASS_PEAK_SHAPER,
    "Configure the mass peak shaper");

  comboBoxItemList.insert(
    (int)MassSpectrumSynthesisActions::SYNTHESIZE_MASS_SPECTRA,
    "Synthesize the mass spectra");

  comboBoxItemList.insert(
    (int)MassSpectrumSynthesisActions::COPY_MASS_SPECTRUM_TO_CLIPBOARD,
    "Mass spectrum not yet available");

  comboBoxItemList.insert(
    (int)MassSpectrumSynthesisActions::SAVE_MASS_SPECTRUM_TO_FILE,
    "Mass spectrum not yet available");

  m_ui.massSpectrumSynthesisComboBox->addItems(comboBoxItemList);

  connect(m_ui.massSpectrumSynthesisComboBox,
          SIGNAL(activated(int)),
          this,
          SLOT(massSpectrumSynthesisMenuActivated(int)));

  mpa_resultsString = new QString();

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

  connect(m_ui.fragmentPushButton, SIGNAL(clicked()), this, SLOT(fragment()));

  connect(m_ui.filterPatternLineEdit,
          SIGNAL(returnPressed()),
          this,
          SLOT(filterPattern()));

  connect(m_ui.filterMonoMassLineEdit,
          SIGNAL(returnPressed()),
          this,
          SLOT(filterMonoMass()));

  connect(m_ui.filterAvgMassLineEdit,
          SIGNAL(returnPressed()),
          this,
          SLOT(filterAvgMass()));

  connect(m_ui.filterChargeLineEdit,
          SIGNAL(returnPressed()),
          this,
          SLOT(filterCharge()));

  connect(m_ui.filteringOptionsGroupBox,
          SIGNAL(clicked(bool)),
          this,
          SLOT(filterOptions(bool)));

  show();

  return true;
}

bool
FragmentationDlg::populateSelectedOligomerData()
{
  libXpertMassCore::IndexRangeCollection index_range_collection;

  if(!mp_editorWnd->mpa_editorGraphicsView->selectionIndices(
       index_range_collection))
    {
      QMessageBox::information(this,
                               tr("MassXpert3 - Fragmentation"),
                               tr("No oligomer is selected. "
                                  "Select an oligomer first"),
                               QMessageBox::Ok);
      return false;
    }

  qDebug() << "Got selection indices from sequence editor window:"
           << index_range_collection.indicesAsText();

  if(index_range_collection.size() > 1)
    {
      QMessageBox::information(
        this,
        tr("MassXpert3 - Fragmentation"),
        tr("Fragmentation simulations with\n"
           "multi-region selection are not "
           "supported. Using the most inclusive sequence index range."),
        QMessageBox::Ok);
    }

  libXpertMassCore::IndexRange *index_range_p =
    index_range_collection.mostInclusiveLeftRightIndexRange();
  m_fragmentIndexRange.initialize(*index_range_p);
  delete index_range_p;

  m_calcOptions.setIndexRange(m_fragmentIndexRange);

  m_ui.oligomerStartLabel->setText(
    QString().setNum(m_fragmentIndexRange.m_start + 1));
  m_ui.oligomerEndLabel->setText(
    QString().setNum(m_fragmentIndexRange.m_stop + 1));

  return true;
}


void
FragmentationDlg::populateFragSpecList()
{
  for(libXpertMassCore::FragmentationPathwaySPtr frag_pathway_sp :
      mcsp_polChemDef->getFragmentationPathwaysCstRef())
    m_ui.fragmentationPatternListWidget->addItem(frag_pathway_sp->getName());

  return;
}


void
FragmentationDlg::setupTableView()
{
  // Model stuff all thought for sorting.
  mpa_oligomerTableViewModel =
    new FragmentOligomerTableViewModel(&m_oligomers, this);

  mpa_proxyModel = new FragmentOligomerTableViewSortProxyModel(this);
  mpa_proxyModel->setSourceModel(mpa_oligomerTableViewModel);
  mpa_proxyModel->setFilterKeyColumn(-1);

  m_ui.oligomerTableView->setModel(mpa_proxyModel);
  m_ui.oligomerTableView->setParentDlg(this);
  m_ui.oligomerTableView->setOligomerCollection(&m_oligomers);
  mpa_oligomerTableViewModel->setTableView(m_ui.oligomerTableView);
}


void
FragmentationDlg::fragment()
{
  // For each fragmentation specification selected in the list of
  // available fragmentation specifications we have to construct a
  // FragmentationConfig instance and add it to the list of such frag
  // options. If no item is currently selected, just return.

  // What are the currently selected specifications ?
  QList<QListWidgetItem *> selected_frag_path_list_widget_items =
    m_ui.fragmentationPatternListWidget->selectedItems();

  if(selected_frag_path_list_widget_items.isEmpty())
    {
      QMessageBox::information(
        this,
        tr("MassXpert3 - Polymer fragmentation"),
        tr("Please select at least one fragmentation pathway."),
        QMessageBox::Ok);

      return;
    }

  // We'll need to use the specifications in the polymer chemistry
  // definition... for each frag options object.
  std::vector<MsXpS::libXpertMassCore::FragmentationPathwaySPtr> ref_frag_pathways =
    mcsp_polChemDef->getFragmentationPathwaysCstRef();

  // We'll have to store all the fragOptions in a list:
  std::vector<MsXpS::libXpertMassCore::FragmentationConfig> fragmentation_configs;

  // Finally parse all the different fragmentation specification items
  // currently selected in the list widget.

  for(int iter = 0; iter < selected_frag_path_list_widget_items.size(); ++iter)
    {
      QListWidgetItem *item = selected_frag_path_list_widget_items.at(iter);

      if(item->isSelected())
        {
          // Need to create a new fragOptions with the correct
          // fragmentation specification. First get its name from the
          // list widget. Then use that name to find the FragSpec
          // from the list of frag specs in the polymer chemistry
          // definition. Finally allocate a new frag options object
          // and set data into it.

          QString name = item->text();

          qDebug() << "Name of the fragmentation pathway:" << name;

          libXpertMassCore::FragmentationPathwayCstSPtr frag_path_sp =
            mcsp_polChemDef->getFragmentationPathwayCstSPtrByName(name);

          if(frag_path_sp == nullptr)
            return;

          qDebug() << "The fragmentation pathway as known by the polymer "
                      "chemistry definition:"
                   << frag_path_sp->toString();

          libXpertMassCore::FragmentationConfig fragmentation_config(*frag_path_sp);

          // The user might ask that some formulas be applied while
          // making the fragmentation with the selection
          // fragmentation option. Lets feed the FragmentationConfig
          // instance with the related formulas.

          if(m_ui.decompositionsMinusWaterCheckBox->checkState() == Qt::Checked)
            {
              qDebug() << "Accounting for extra fragmentation formula:"
                       << "-H2O";
              fragmentation_config.addFormula(QString("-H2O"));
            }

          if(m_ui.decompositionsMinusAmmoniaCheckBox->checkState() ==
             Qt::Checked)
            {
              qDebug() << "Accounting for extra fragmentation formula:"
                       << "-NH3";
              fragmentation_config.addFormula(QString("-NH3"));
            }

          if(!m_ui.decompositionsFormulaLineEdit->text().isEmpty())
            {
              qDebug() << "Accounting for extra fragmentation formula:"
                       << m_ui.decompositionsFormulaLineEdit->text();
              libXpertMassCore::Formula formula(
                m_ui.decompositionsFormulaLineEdit->text());

              // At this point, we should check if the formula is valid.

              libXpertMassCore::IsotopicDataCstSPtr isotopic_data_csp =
                mcsp_polChemDef->getIsotopicDataCstSPtr();

              qDebug() << "Going to validate decomposition formula"
                       << formula.getActionFormula();

              libXpertMassCore::ErrorList error_list;

              if(!formula.validate(isotopic_data_csp, &error_list))
                {
                  QMessageBox::information(
                    0,
                    tr("MassXpert3 - Fragmentation"),
                    tr("Failed to validate the decomposition formula."),
                    QMessageBox::Ok);

                  return;
                }

              fragmentation_config.addFormula(formula);
            }

          // We got the coordinates of the oligomer to fragment in the
          // mp_calcOptions from the sequence editor window.

          // Update the options from the parent window.
          m_calcOptions.initialize(*mp_editorWnd->getCalcOptions());

          // And now override the selection data by using the
          // selection-specific function below.
          if(!populateSelectedOligomerData())
            return;

          fragmentation_config.setStartIndex(m_fragmentIndexRange.m_start);
          fragmentation_config.setStopIndex(m_fragmentIndexRange.m_stop);

          // Set the ionization levels.
          int ionization_level_start = m_ui.ionizeLevelStartSpinBox->value();
          int ionization_level_stop  = m_ui.ionizeLevelEndSpinBox->value();

          if(ionization_level_start > ionization_level_stop)
            std::swap(ionization_level_start, ionization_level_stop);

          fragmentation_config.setStartIonizeLevel(ionization_level_start);
          fragmentation_config.setStopIonizeLevel(ionization_level_stop);

          // Add the frag options to the list.

          qDebug() << "Pushing back fragmentation config:"
                   << fragmentation_config.toString();

          fragmentation_configs.push_back(fragmentation_config);
        }
      else
        continue;
    }

  // At this point the list of frag options should contain the same
  // number of items as the number of items selected in the list
  // widget.

  Q_ASSERT(static_cast<int>(fragmentation_configs.size()) ==
           selected_frag_path_list_widget_items.size());

  // m_ionizer is the copy of the Ionizer in the SequenceEditorWnd. It might
  // contain an ionization status (that is, charge() or currentCharge() return
  // non-0) but we do not want this, we want to transmit an ionizer with the
  // ionization formula and the nominal charge set. The other data must be 0.

  libXpertMassCore::Ionizer ionizer(m_ionizer);
  ionizer.setLevel(0);
  ionizer.forceCurrentStateLevel(0);

  libXpertMassCore::Fragmenter fragmenter(msp_polymer,
                                      mcsp_polChemDef,
                                      fragmentation_configs,
                                      m_calcOptions,
                                      ionizer);

  qDebug()
    << "Now asking that the fragmenter fragments with calculation options:"
    << m_calcOptions.toString();

  if(!fragmenter.fragment())
    {
      QMessageBox::critical(this,
                            tr("MassXpert3 - Polymer fragmentation"),
                            tr("Failed to perform fragmentation."),
                            QMessageBox::Ok);

      return;
    }

  // The list in which all the allocated oligomers have been stored by the
  // fragmented inside of it..
  const MsXpS::libXpertMassCore::OligomerCollection &fragment_oligomers =
    fragmenter.getOligomerCollectionCstRef();

  if(!fragment_oligomers.size())
    {
      QMessageBox::information(
        this,
        tr("MassXpert3 - Polymer fragmentation"),
        tr("The fragmentation did not yield any fragment."),
        QMessageBox::Ok);

      return;
    }

  qDebug().noquote()
    << "After fragmentation, all the obtained fragment oligomers:\n"
    << fragment_oligomers.toString();

  // At this point we have a brand new set of oligomers in the local
  // list of oligomers (fragment_oligomers). We either stack these on top
  // of the previous ones, or we replace the previous ones with the
  // new ones.  Are we stacking new oligomers on top of the old
  // ones?

  if(!m_ui.stackOligomersCheckBox->isChecked())
    {
      // We are going to remove *all* the previous oligomers.
      mpa_oligomerTableViewModel->removeOligomers(0, m_oligomers.size() - 1);
    }

  // At this point we can set up the data to the treeview model.

  int existing_oligomer_count = fragment_oligomers.size();

  // We are *transferring* the oligomers from fragment_oligomers to the
  // list of oligomers that is in the model : we are not duplicating
  // the oligomers. When the transfer has been done, the fragment_oligomers will
  // be cleared,  as their owner,  fragmenter,  will be destroyed going out of
  // scope.
  int newly_added_oligomer_count =
    mpa_oligomerTableViewModel->addOligomers(fragment_oligomers);

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

  if(existing_oligomer_count != newly_added_oligomer_count)
    qFatal() << "Programming error.";

  // Set focus to the treeView.
  m_ui.oligomerTableView->setFocus();

  QString title;

  int oligomerCount = mpa_oligomerTableViewModel->rowCount();

  if(!oligomerCount)
    title = tr("Oligomers (empty list)");
  else if(oligomerCount == 1)
    title = tr("Oligomers (one item)");
  else
    title = tr("Oligomers (%1 items)").arg(oligomerCount);

  m_ui.oligomerGroupBox->setTitle(title);

  // Update the fragmentation details so that we know how the oligos were
  // computed.
  updateFragmentationDetails(m_calcOptions);
}


void
FragmentationDlg::updateFragmentationDetails(
  const libXpertMassCore::CalcOptions &calcOptions)
{
  if(static_cast<int>(calcOptions.getMonomerEntities()) &
     static_cast<int>(libXpertMassCore::Enums::ChemicalEntity::MODIF))
    m_ui.modificationsCheckBox->setCheckState(Qt::Checked);
  else
    m_ui.modificationsCheckBox->setCheckState(Qt::Unchecked);

  if(static_cast<int>(calcOptions.getPolymerEntities()) &
     static_cast<int>(libXpertMassCore::Enums::ChemicalEntity::LEFT_END_MODIF))
    m_ui.leftModifCheckBox->setCheckState(Qt::Checked);
  else
    m_ui.leftModifCheckBox->setCheckState(Qt::Unchecked);

  if(static_cast<int>(calcOptions.getPolymerEntities()) &
     static_cast<int>(libXpertMassCore::Enums::ChemicalEntity::RIGHT_END_MODIF))
    m_ui.rightModifCheckBox->setCheckState(Qt::Checked);
  else
    m_ui.rightModifCheckBox->setCheckState(Qt::Unchecked);

  if(static_cast<int>(calcOptions.getMonomerEntities()) &
     static_cast<int>(libXpertMassCore::Enums::ChemicalEntity::CROSS_LINKER))
    m_ui.crossLinksCheckBox->setCheckState(Qt::Checked);
  else
    m_ui.crossLinksCheckBox->setCheckState(Qt::Unchecked);
}


void
FragmentationDlg::updateOligomerSequence(QString *text)
{
  Q_ASSERT(text);

  m_ui.oligomerSequenceTextEdit->clear();
  m_ui.oligomerSequenceTextEdit->append(*text);
}


bool
FragmentationDlg::calculateTolerance(double mass)
{
  // Get the tolerance that is in its lineEdit.

  QString text     = m_ui.toleranceLineEdit->text();
  double tolerance = 0;
  bool ok          = false;

  if(!text.isEmpty())
    {
      // Convert the string to a double.

      ok        = false;
      tolerance = text.toDouble(&ok);

      if(!tolerance && !ok)
        return false;
    }
  else
    {
      m_tolerance = 0;
    }

  // What's the item currently selected in the comboBox?
  int index = m_ui.toleranceComboBox->currentIndex();

  if(index == 0)
    {
      // MASS_TOLERANCE_AMU
      m_tolerance = tolerance;
    }
  else if(index == 1)
    {
      // MASS_TOLERANCE_PCT
      m_tolerance = (tolerance / 100) * mass;
    }
  else if(index == 2)
    {
      // MASS_TOLERANCE_PPM
      m_tolerance = (tolerance / 1000000) * mass;
    }
  else
    Q_ASSERT(0);

  return true;
}


void
FragmentationDlg::filterOptions(bool checked)
{
  if(!checked)
    {
      mpa_proxyModel->setFilterKeyColumn(-1);

      mpa_proxyModel->applyNewFilter();

      m_ui.filteringOptionsFrame->setVisible(false);
    }
  else
    {
      m_ui.filteringOptionsFrame->setVisible(true);

      // In this case, set focus to the last focused widget in the
      // groupbox or the first widget in the groubox if this is the
      // first time the filtering is used.
      mp_focusWidget->setFocus();
    }
}


void
FragmentationDlg::filterOptionsToggled()
{
  bool isChecked = m_ui.filteringOptionsGroupBox->isChecked();

  m_ui.filteringOptionsGroupBox->setChecked(!isChecked);
  filterOptions(!isChecked);
}


void
FragmentationDlg::filterPattern()
{
  // First off, we have to get the pattern that is in the lineEdit.

  QString text = m_ui.filterPatternLineEdit->text();

  if(text.isEmpty())
    return;

  mpa_proxyModel->setPatternFilter(text);
  mpa_proxyModel->setFilterKeyColumn(0);
  mpa_proxyModel->applyNewFilter();

  mp_focusWidget = m_ui.filterPatternLineEdit;
}


void
FragmentationDlg::filterMonoMass()
{
  // First off, we have to get the mass that is in the lineEdit.

  QString text = m_ui.filterMonoMassLineEdit->text();

  if(text.isEmpty())
    return;

  // Convert the string to a double.

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

  if(!mass && !ok)
    return;

  // At this point, depending on the item that is currently selected
  // in the comboBox, we'll have to actually compute the tolerance.

  if(!calculateTolerance(mass))
    return;

  mpa_proxyModel->setMonoFilter(mass);
  mpa_proxyModel->setTolerance(m_tolerance);

  mpa_proxyModel->setFilterKeyColumn(3);
  mpa_proxyModel->applyNewFilter();

  mp_focusWidget = m_ui.filterMonoMassLineEdit;
}


void
FragmentationDlg::filterAvgMass()
{
  // First off, we have to get the mass that is in the lineEdit.

  QString text = m_ui.filterAvgMassLineEdit->text();

  if(text.isEmpty())
    return;

  // Convert the string to a double.

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

  if(!mass && !ok)
    return;

  // At this point, depending on the item that is currently selected
  // in the comboBox, we'll have to actually compute the tolerance.

  if(!calculateTolerance(mass))
    return;

  mpa_proxyModel->setAvgFilter(mass);
  mpa_proxyModel->setTolerance(m_tolerance);

  mpa_proxyModel->setFilterKeyColumn(4);
  mpa_proxyModel->applyNewFilter();

  mp_focusWidget = m_ui.filterAvgMassLineEdit;
}


void
FragmentationDlg::filterCharge()
{
  // First off, we have to get the charge that is in the lineEdit.

  QString text = m_ui.filterChargeLineEdit->text();

  if(text.isEmpty())
    return;

  // Convert the string to a int.

  bool ok    = false;
  int charge = text.toInt(&ok);

  if(!charge && !ok)
    return;

  mpa_proxyModel->setChargeFilter(charge);
  mpa_proxyModel->setFilterKeyColumn(5);
  mpa_proxyModel->applyNewFilter();

  mp_focusWidget = m_ui.filterChargeLineEdit;
}


void
FragmentationDlg::traceColorPushButtonClicked()
{

  QPushButton *colored_push_button = m_ui.colorSelectorPushButton;

  // Allow (true) the user to select a color that has been chosen already.
  QColor color = libXpertMassGui::ColorSelector::chooseColor(true);

  if(color.isValid())
    {
      QPalette palette = colored_push_button->palette();
      palette.setColor(QPalette::Button, color);
      colored_push_button->setAutoFillBackground(true);
      colored_push_button->setPalette(palette);
      colored_push_button->update();

      // Now prepare the color in the form of a QByteArray

      QDataStream stream(&m_colorByteArray, QIODevice::WriteOnly);

      stream << color;
    }
}


void
FragmentationDlg::massSpectrumSynthesisMenuActivated(int index)
{
  if(index == (int)MassSpectrumSynthesisActions::LOAD_ISOTOPIC_DATA)
    {
      loadIsotopicDataFromFile();
      return;
    }
  if(index == (int)MassSpectrumSynthesisActions::CONFIGURE_MASS_PEAK_SHAPER)
    return configureMassPeakShaper();
  else if(index == (int)MassSpectrumSynthesisActions::SYNTHESIZE_MASS_SPECTRA)
    return synthesizeMassSpectra();
  else if(index ==
          (int)MassSpectrumSynthesisActions::COPY_MASS_SPECTRUM_TO_CLIPBOARD)
    {
      if(!m_syntheticMassSpectrum.size())
        {
          m_ui.massSpectrumSynthesisComboBox->setItemText(
            (int)MassSpectrumSynthesisActions::COPY_MASS_SPECTRUM_TO_CLIPBOARD,
            "Mass spectrum not yet available");

          return;
        }

      QClipboard *clipboard = QApplication::clipboard();
      clipboard->setText(m_syntheticMassSpectrum.toString());
    }
  else if(index ==
          (int)MassSpectrumSynthesisActions::SAVE_MASS_SPECTRUM_TO_FILE)
    {
      if(!m_syntheticMassSpectrum.size())
        {
          m_ui.massSpectrumSynthesisComboBox->setItemText(
            (int)MassSpectrumSynthesisActions::SAVE_MASS_SPECTRUM_TO_FILE,
            "Mass spectrum not yet available");

          return;
        }

      QString file_path = QFileDialog::getSaveFileName(
        this,
        tr("Select file to export the mass spectrum  to"),
        QDir::homePath(),
        tr("Data files(*.xy *.XY)"));

      QFile file(file_path);

      if(!file.open(QIODevice::WriteOnly))
        {
          QMessageBox::information(0,
                                   tr("MassXpert3 - Export mass spectrum"),
                                   tr("Failed to open file in write mode."),
                                   QMessageBox::Ok);
          return;
        }

      QTextStream stream(&file);
      stream.setEncoding(QStringConverter::Utf8);

      prepareResultsTxtString();

      stream << m_syntheticMassSpectrum.toString();

      file.close();
    }
  else
    qFatal("Programming error. Should never reach this point.");
}


void
FragmentationDlg::configureMassPeakShaper()
{
  if(mp_massPeakShaperConfigDlg == nullptr)
    {
      mp_massPeakShaperConfigDlg = new libXpertMassGui::MassPeakShaperConfigDlg(
        this, m_applicationName, "Mass peak shaper configuration");

      if(mp_massPeakShaperConfigDlg == nullptr)
        qFatal("Programming error. Failed to allocate the dialog window.");
    }

  // The signal below is only emitted when checking parameters worked ok.
  connect(mp_massPeakShaperConfigDlg,
          &libXpertMassGui::MassPeakShaperConfigDlg::
            updatedMassPeakShaperConfigSignal,
          [this](const libXpertMassCore::MassPeakShaperConfig &config) {
            m_massPeakShaperConfig.initialize(config);

            // qDebug().noquote() << "Our local copy of the config:"
            //<< m_massPeakShaperConfig.toString();
          });

  mp_massPeakShaperConfigDlg->activateWindow();
  mp_massPeakShaperConfigDlg->raise();
  mp_massPeakShaperConfigDlg->show();
}


bool
FragmentationDlg::loadIsotopicDataFromFile()
{
  QString file_name = QFileDialog::getOpenFileName(
    this, tr("Load User IsoSpec table"), QDir::home().absolutePath());

  if(file_name.isEmpty())
    {
      QMessageBox::warning(
        this,
        QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
        tr("Failed to set the file name."),
        QMessageBox::Ok);

      return false;
    }

  libXpertMassCore::IsotopicDataUserConfigHandler handler;
  handler.loadData(file_name);

  msp_isotopicData = handler.getIsotopicData();

  if(msp_isotopicData == nullptr || !msp_isotopicData->size())
    {
      QMessageBox::warning(
        this,
        QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
        "Failed to load the isotopic data.",
        QMessageBox::Ok);

      return false;
    }

  return true;
}


void
FragmentationDlg::synthesizeMassSpectra()
{

  // Clear the synthetic mass spectrum. Make sure the mass spectrum to clipboard
  // menu item is not "available". Same for to file.
  m_syntheticMassSpectrum.clear();
  m_ui.massSpectrumSynthesisComboBox->setItemText(
    (int)MassSpectrumSynthesisActions::COPY_MASS_SPECTRUM_TO_CLIPBOARD,
    "Mass spectrum not yet available");

  m_ui.massSpectrumSynthesisComboBox->setItemText(
    (int)MassSpectrumSynthesisActions::SAVE_MASS_SPECTRUM_TO_FILE,
    "Mass spectrum not yet available");

  setCursor(Qt::WaitCursor);

  libXpertMassCore::ErrorList error_list;

  if(!m_massPeakShaperConfig.resolve(error_list))
  {
    QMessageBox msgBox;
    QString msg =
    QString("Mass peak shaper configuration errors:\n%1")
    .arg(libXpertMassCore::Utils::joinErrorList(error_list, "\n"));
    msgBox.setText(msg);
    msgBox.exec();

    configureMassPeakShaper();
  }

  // All the selected oligomers need to be processed such that using both
  // their formula and their charge, we create isotopic
  // clusters that we can then merge into a single mass spectrum.

  // Remember, Oligomer instances created during fragmentation do not calculate
  // the formula properly using Oligomer::elementalComposition() because
  // the Oligomer itself is not aware of the fragmentation chemistry. This
  // is the reason why Oligomer::m_formula exists: to store, during the
  // fragmentation, the state of the formula that describes the final
  // product ion.

  libXpertMassCore::OligomerCollection selected_oligomers;

  std::size_t count =
    m_ui.oligomerTableView->selectedOligomers(selected_oligomers, -1);

  if(!count)
    {
      QMessageBox msgBox;
      QString msg("Please, select at least one oligomer in the table view.");
      msgBox.setText(msg);
      msgBox.exec();

      setCursor(Qt::ArrowCursor);
      return;
    }

  // qDebug() << "Selected oligomers:" << count;

  // At this point, we need to get a handle on the isotopic data. Note how we
  // want a non-const shared pointer!

  if(msp_isotopicData == nullptr)
    msp_isotopicData = std::make_shared<libXpertMassCore::IsotopicData>(
      *mp_editorWnd->getPolChemDefRenderingCstRPtr()
         ->getPolChemDefCstSPtr()
         ->getIsotopicDataCstSPtr());

  if(msp_isotopicData == nullptr)
    qFatal() << "Programming error. The isotopic data cannot be nullptr.";

  // Great, we now we have isotopes to do the calculations!

  // And now we can instantiate a cluster generator

  libXpertMassCore::IsotopicClusterGenerator isotopic_cluster_generator(
    msp_isotopicData);

  // Construct the formula/charge pairs needed for the isotopic cluster
  // calculations.
  std::vector<libXpertMassCore::FormulaChargePair> formula_charge_pairs;

  // For debugging purposes.
  // std::size_t oligomer_count = 0;

  for(libXpertMassCore::OligomerSPtr oligomer_sp :
      selected_oligomers.getOligomersCstRef())
    {
      int charge = oligomer_sp->getIonizerCstRef().charge();

      // qDebug() << formula_text << charge << oligomer_sp->mono();

      // We do not want to calculate isotopic clusters for fragments that have a
      // minus part in their formula. This might happen when applying formulas
      // like -H20 or -NH3 to ions that do not contain enough such atoms in
      // their formula for the overall formula to keep being only positive. One
      // could end with something like (specifically with immonium ions that are
      // very small) CxHyNz-Ow

      // Do not use Oligomer::elementalComposition() !! See above, for
      // explanations.
      libXpertMassCore::Formula formula(oligomer_sp->getFormulaCstRef());

      // We want to check the formula "fresh", store the symbol/count pairs and
      // reset (although not really needed since we have created the formula at
      // the code line above).
      libXpertMassCore::ErrorList error_list;
      if(!formula.validate(msp_isotopicData, true, true, &error_list))
        qFatal() << "Programming error."
                       << QString("The formula failed to validate with errors:")
                            .arg(
                              libXpertMassCore::Utils::joinErrorList(error_list));

      if(formula.hasNetMinusPart())
        {
          qDebug()
            << "The oligomer's formula has a net minus part: skipping it.";
          continue;
        }

      // Instantiate on-the-fly the formula/charge pair that is fed to the
      // generator.

      formula_charge_pairs.push_back(
        libXpertMassCore::FormulaChargePair(formula.getActionFormula(), charge));

      //++oligomer_count;
    }

  // qDebug() << "Prepared" << oligomer_count << "formula/charge pairs";

  // At this point, we can copy all the formula/charge pairs to the cluster
  // generator.

  isotopic_cluster_generator.setFormulaChargePairs(formula_charge_pairs);

  isotopic_cluster_generator.setIsotopicDataType(
    libXpertMassCore::IsotopicDataType::LIBRARY_CONFIG);

  double cumulated_probabilities = m_ui.cumulatedProbsDoubleSpinBox->value();
  isotopic_cluster_generator.setMaxSummedProbability(cumulated_probabilities);
  double normalization_intensity =
    m_ui.normalizeIntensityDoubleSpinBox->value();

  // Not needed here because it will be taken into account at peak shaping
  // time
  // isotopic_cluster_generator.setNormalizationIntensity(normalization_intensity);
  // qDebug() << "Set the generator's normalization intensity:"
  //<< normalization_intensity;

  // And now perform the work. For each formula/charge pair, the generator will
  // create an isotopic cluster shaped trace associated to the corresponding
  // charge in a libXpertMassCore::IsotopicClusterChargePair. All these pairs are
  // stored in a vector.

  count = isotopic_cluster_generator.run();

  if(!count)
    {
      qDebug() << "Failed to create any isotopic cluster.";

      QMessageBox::information(
        this,
        QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
        "Failed to compute a single isotopic cluster.",
        QMessageBox::Ok);

      setCursor(Qt::ArrowCursor);
      return;
    }

  // qDebug() << "The number of clusters generated:" << count;

  std::vector<libXpertMassCore::IsotopicClusterChargePair>
    isotopic_cluster_charge_pairs =
      isotopic_cluster_generator.getIsotopicClusterChargePairs();

  // At this point we should use these pairs to create a shape for each. But
  // first reset to 1 the charge because that charge was already accounted for
  // at the generation of the cluster. We do not want to divide m/z again by
  // charge. If charge had been 1, that would be no problem, but let's say the
  // charge was 2, if we did maintain that charge to a value of 2, then we would
  // get a tetra-protonated species cluster.

  // Note how we ask a reference to the pair that is iterated into, otherwise we
  // would get a copy and we would lose the local charge modification.
  for(libXpertMassCore::IsotopicClusterChargePair &pair :
      isotopic_cluster_charge_pairs)
    pair.second = 1;

  qDebug();

  // Now instantiate the isotopic cluster shaper and set the clusters' data in
  // it.

  libXpertMassCore::IsotopicClusterShaper isotopic_cluster_shaper(
    isotopic_cluster_charge_pairs, m_massPeakShaperConfig);

  isotopic_cluster_shaper.setNormalizeIntensity(normalization_intensity);

  // And now run the shaper.
  m_syntheticMassSpectrum = isotopic_cluster_shaper.run();

  if(!m_syntheticMassSpectrum.size())
    {
      qDebug() << "The synthetic mass spectrum has not a single data point.";
      setCursor(Qt::ArrowCursor);
      return;
    }

  m_ui.massSpectrumSynthesisComboBox->setItemText(
    (int)MassSpectrumSynthesisActions::COPY_MASS_SPECTRUM_TO_CLIPBOARD,
    "Mass spectrum to clipboard");

  m_ui.massSpectrumSynthesisComboBox->setItemText(
    (int)MassSpectrumSynthesisActions::SAVE_MASS_SPECTRUM_TO_FILE,
    "Mass spectrum to file");

  setCursor(Qt::ArrowCursor);

  QString trace_title = m_ui.massSpectrumTitleLineEdit->text();

  if(trace_title.isEmpty())
    trace_title = mp_editorWnd->getSequenceName();

  if(trace_title.isEmpty())
    trace_title = mp_editorWnd->getPolymerSPtr()->getFilePath();

  emit displayMassSpectrumSignal(
    trace_title,
    m_colorByteArray,
    std::make_shared<const pappso::Trace>(m_syntheticMassSpectrum));
}


// The results-exporting functions. ////////////////////////////////
// The results-exporting functions. ////////////////////////////////
// The results-exporting functions. ////////////////////////////////
void
FragmentationDlg::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
    Q_ASSERT(0);
}


void
FragmentationDlg::prepareResultsTxtString()
{
  // Set the delimiter to the char/string that is in the
  // corresponding text line edit widget.

  QString delimiter = m_ui.delimiterLineEdit->text();
  // If delimiter is empty, the function that will prepare the
  // string will put the default character, that is '$'.

  mpa_resultsString->clear();

  // We only put the header info if the user does not want to have
  // the data formatted for XpertMiner, which only can get the data
  // in the format :
  //
  // mass <delim> charge <delim> name <delim> coords
  //
  // Note that name and coords are optional.
  bool forXpertMiner = false;
  libXpertMassCore::Enums::MassType mass_type;

  forXpertMiner = m_ui.forXpertMinerCheckBox->isChecked();

  if(!forXpertMiner)
    {
      *mpa_resultsString += QObject::tr(
        "# \n"
        "# ---------------------------\n"
        "# Fragmentation: \n"
        "# ---------------------------\n");
    }

  if(m_ui.monoRadioButton->isChecked())
    mass_type = libXpertMassCore::Enums::MassType::MONO;
  else
    mass_type = libXpertMassCore::Enums::MassType::AVG;

  bool withSequence = m_ui.withSequenceCheckBox->isChecked();

  QString *text = m_ui.oligomerTableView->describeOligomerAsPlainText(
    delimiter, withSequence, forXpertMiner, mass_type);

  *mpa_resultsString += *text;

  delete text;
}


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

  QClipboard *clipboard = QApplication::clipboard();

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

  return true;
}


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

  QFile file(m_resultsFilePath);

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

  QTextStream stream(&file);
  stream.setEncoding(QStringConverter::Utf8);

  prepareResultsTxtString();

  stream << *mpa_resultsString;

  file.close();

  return true;
}


bool
FragmentationDlg::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;
}
//////////////////////////////////// The results-exporting functions.
//////////////////////////////////// The results-exporting functions.
//////////////////////////////////// The results-exporting functions.

} // namespace MassXpert

} // namespace MsXpS
