/* 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 <QDebug>
#include <QKeyEvent>
#include <QKeySequence>
#include <QSettings>


/////////////////////// libmass includes
#include <libXpertMass/PolChemDefEntity.hpp>
#include <libXpertMass/IsotopicDataUserConfigHandler.hpp>
#include <libXpertMass/IsotopicClusterGenerator.hpp>
#include <libXpertMass/IsotopicClusterShaper.hpp>
#include <libXpertMass/IsotopicData.hpp>
#include <libXpertMass/MassPeakShaper.hpp>
#include <libXpertMass/MassPeakShaperConfig.hpp>

#include <libXpertMassGui/ColorSelector.hpp>

/////////////////////// Local includes
#include "../nongui/globals.hpp"
#include "CleavageDlg.hpp"
#include "CleaveOligomerTableViewModel.hpp"


namespace MsXpS
{

namespace massxpert
{


CleavageDlg::CleavageDlg(SequenceEditorWnd *editorWnd,
                         libXpertMass::Polymer *polymer,
                         libXpertMass::PolChemDefCstSPtr polChemDefCstSPtr,
                         const QString &configSettingsFilePath,
                         const QString &applicationName,
                         const QString &description,
                         QByteArray hash,
                         const libXpertMass::CalcOptions &calcOptions,
                         const libXpertMass::IonizeRule *ionizeRule)
  : AbstractSeqEdWndDependentDlg(editorWnd,
                                 polymer,
                                 polChemDefCstSPtr,
                                 "CleavageDlg",
                                 configSettingsFilePath,
                                 applicationName,
                                 description),
    m_polymerHash(hash),
    m_calcOptions(calcOptions),
    mp_ionizeRule(ionizeRule)
{
  if(!mp_polymer || !mcsp_polChemDef)
    qFatal("Fatal error at %s@%d. Program aborted.", __FILE__, __LINE__);

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

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


CleavageDlg::~CleavageDlg()
{
  m_oligomerList.clear();

  delete mpa_resultsString;

  delete mpa_oligomerTableViewModel;
  delete mpa_proxyModel;

  delete mpa_cleaver;

  writeSettings();
}

void
CleavageDlg::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
CleavageDlg::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
CleavageDlg::initialize()
{
  m_ui.setupUi(this);

  // 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));

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

  m_ui.massSpectrumTitleLineEdit->setText(mp_polymer->name());
  populateCleaveSpecListWidget();
  populateSelectedOligomerData();

  // 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_cleaver                = 0;
  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_G));
  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.filterPartialLineEdit;

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

  QStringList comboBoxItemList;

  comboBoxItemList.insert(
    (int)ExportResultsActions::EXPORT_TO_CLIPBOARD_OVERWRITE,
    "Overwrite clipboard (Ctrl+O)");
  comboBoxItemList.insert((int)ExportResultsActions::EXPORT_TO_CLIPBOARD_APPEND,
                          "To clipboard");
  comboBoxItemList.insert((int)ExportResultsActions::EXPORT_TO_FILE, "To file");
  comboBoxItemList.insert((int)ExportResultsActions::SELECT_FILE,
                          "Select file");

  m_ui.exportResultsComboBox->addItems(comboBoxItemList);

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

  comboBoxItemList.clear();

  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)));

  comboBoxItemList.clear();

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

  comboBoxItemList << "%e$%i$%l$%s$%m$%p\\n";
  m_ui.outputPatternComboBox->addItems(comboBoxItemList);

  connect(m_ui.outputPatternHelpPushButton,
          &QPushButton::clicked,
          this,
          &CleavageDlg::showOutputPatternDefinitionHelp);

  mpa_resultsString = new QString();

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

  readSettings();

  connect(m_ui.cleavePushButton, SIGNAL(clicked()), this, SLOT(cleave()));

  connect(m_ui.filterPartialLineEdit,
          SIGNAL(returnPressed()),
          this,
          SLOT(filterPartial()));

  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
CleavageDlg::populateSelectedOligomerData()
{
  libXpertMass::CoordinateList coordList;
  libXpertMass::Coordinates *coordinates = 0;

  if(!mp_editorWnd->mpa_editorGraphicsView->selectionIndices(&coordList))
    {
      // We get the selection indices corresponding to the virtual
      // selection, that is the sequence region encompassing the
      // first residue of the sequence up to the monomer left of
      // point.

      // In this case we want the cleavage to be performed on the
      // whole polymer sequence. Set its coordinates to the list of
      // coordinates.

      coordinates = new libXpertMass::Coordinates(0, mp_polymer->size() - 1);

      // Set the newly created coordinates to the coordinates list
      // that we'll feed into the calculation options.

      coordList.setCoordinates(*coordinates);

      // We do not need the coordinates object anymore, delete it.
      delete coordinates;
      coordinates = 0;
    }

  if(coordList.size() > 1)
    {
      QMessageBox::information(
        this,
        QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
        "Cleavage simulations with multi-region\nselection are not"
        "supported.",
        QMessageBox::Ok);

      // Set the selection data as if all of the sequence was to be
      // cleaved.

      coordinates = new libXpertMass::Coordinates(0, mp_polymer->size() - 1);

      // Set the newly created coordinates to the coordinates list
      // that we'll feed into the calculation options.

      coordList.setCoordinates(*coordinates);

      // We do not need the coordinates object anymore, delete it.
      delete coordinates;
      coordinates = 0;
    }

  // Now, if the checkbox asking for whole sequence cleavage is
  // checked, then we have to make the cleavage on the whole
  // sequence.
  if(m_ui.wholeSequenceCheckBox->isChecked())
    {
      // The cleavage is to be performed on the whole polymer
      // sequence.

      coordinates = new libXpertMass::Coordinates(0, mp_polymer->size() - 1);

      // Set the newly created coordinates to the coordinates list
      // that we'll feed into the calculation options.

      coordList.setCoordinates(*coordinates);

      // We do not need the coordinates object anymore, delete it.
      delete coordinates;
      coordinates = 0;
    }

  m_calcOptions.setCoordinateList(coordList);

  coordinates = coordList.last();

  m_ui.oligomerStartLabel->setText(QString().setNum(coordinates->start() + 1));

  m_ui.oligomerEndLabel->setText(QString().setNum(coordinates->end() + 1));


  // We have to count the incomplete cross-links.

  const libXpertMass::CrossLinkList &crossLinkList = mp_polymer->crossLinkList();

  int crossLinkPartial = 0;

  for(int iter = 0; iter < crossLinkList.size(); ++iter)
    {
      libXpertMass::CrossLink *crossLink = crossLinkList.at(iter);

      int ret = crossLink->encompassedBy(coordList);

      if(ret == libXpertMass::CROSS_LINK_ENCOMPASSED_FULL)
        {
          // 		qDebug() << __FILE__ << __LINE__
          // 			  << "CrossLink at iter:" << iter
          // 			  << "is fully encompassed";
        }
      else if(ret == libXpertMass::CROSS_LINK_ENCOMPASSED_PARTIAL)
        {
          // 		qDebug() << __FILE__ << __LINE__
          // 			  << "CrossLink at iter:" << iter
          // 			  << "is partially encompassed";

          ++crossLinkPartial;
        }
      else
        {
          // 		qDebug() << __FILE__ << __LINE__
          // 			  << "CrossLink at iter:" << iter
          // 			  << "is not encompassed at all";
        }
    }

  if(crossLinkPartial)
    {
      // Alert the user on the fact that the currently selected
      // region does not encompass all of the cross-linked material.

      QMessageBox::information(this,
                               tr("massXpert - libXpertMass::Polymer Cleavage"),
                               tr("There are incomplete cross-links"),
                               QMessageBox::Ok);
    }

  // qDebug() << __FILE__ << __LINE__
  // << "Updated coordinates.";

  return true;
}


void
CleavageDlg::populateCleaveSpecListWidget()
{
  PolChemDefRendering *polChemDefRendering =
    mp_editorWnd->polChemDefRendering();
  Q_ASSERT(polChemDefRendering);

  for(int iter = 0;
      iter < polChemDefRendering->getPolChemDef()->cleaveSpecList().size();
      ++iter)
    {
      libXpertMass::CleaveSpec *cleaveSpec =
        polChemDefRendering->getPolChemDef()->cleaveSpecList().at(iter);
      Q_ASSERT(cleaveSpec);

      m_ui.cleavageAgentListWidget->addItem(cleaveSpec->name());
    }

  return;
}

void
CleavageDlg::setupTableView()
{
  // Model stuff all thought for sorting.
  mpa_oligomerTableViewModel =
    new CleaveOligomerTableViewModel(&m_oligomerList, this);

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

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


SequenceEditorWnd *
CleavageDlg::editorWnd()
{
  return mp_editorWnd;
}

QByteArray
CleavageDlg::polymerHash() const
{
  return m_polymerHash;
}


void
CleavageDlg::cleave()
{
  // What's the cleavage agent ?
  int value                       = m_ui.cleavageAgentListWidget->currentRow();
  libXpertMass::CleaveSpec *cleaveSpec = mcsp_polChemDef->cleaveSpecList().at(value);

  // What's the asked level of partial cleavages?
  value = m_ui.partialCleavageSpinBox->value();

  CleaveOptions cleaveOptions(*cleaveSpec, value, false);

  // Set the ionization levels to the local cleavage options object.
  int valueStart = m_ui.ionizeLevelStartSpinBox->value();
  int valueEnd   = m_ui.ionizeLevelEndSpinBox->value();

  cleaveOptions.setIonizeLevels(valueStart, valueEnd);

  // The list in which we'll store all the allocated oligomers.
  OligomerList tempOligomerList;

  delete mpa_cleaver;
  mpa_cleaver = nullptr;

  // Update the mass calculation engine's configuration data
  // directly from the sequence editor window.
  m_calcOptions = mp_editorWnd->calcOptions();

  // Update the selection data from the sequence editor window.
  if(!populateSelectedOligomerData())
    return;

  mpa_cleaver = new Cleaver(
    mp_polymer, mcsp_polChemDef, cleaveOptions, m_calcOptions, *mp_ionizeRule);

  mpa_cleaver->setOligomerList(&tempOligomerList);

  if(!mpa_cleaver->cleave())
    {
      delete mpa_cleaver;

      QMessageBox::critical(this,
                            tr("massXpert - libXpertMass::Polymer Cleavage"),
                            tr("Failed to perform cleavage."),
                            QMessageBox::Ok);
      return;
    }

  // At this point we have a brand new set of oligomers in the local
  // list of oligomers (tempOligomerList). 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, -1);
    }

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

  int initialOligomers = tempOligomerList.size();

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

  // We are *transferring* the oligomers from tempOligomerList to
  // the list of m_oligomerList list oligomers of which there is a
  // pointer in the model : we are not duplicating the
  // oligomers. When the transfer from tempOligomerList to
  // m_oligomerList _via_ the model will have been done,
  // tempOligomerList will be empty and m_oligomerList will hold
  // them all, with the model having been made aware of that with
  // the beginInsertRows/endInsertRows statement pair.
  int addedOligomers =
    mpa_oligomerTableViewModel->addOligomers(tempOligomerList);

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

  if(initialOligomers != addedOligomers)
    qFatal("Fatal error at %s@%d. Aborting.", __FILE__, __LINE__);

  // Now that we have *transferred* (not copied) all the oligomers
  // in the model, we can clear the tempOligomerList without freeing the
  // instances it currently contain...
  tempOligomerList.clear();

  // 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 cleavage details so that we know how the oligos were
  // computed.
  updateCleavageDetails(m_calcOptions);
}


void
CleavageDlg::updateCleavageDetails(const libXpertMass::CalcOptions &calcOptions)
{
  if(calcOptions.capping() & libXpertMass::CAP_LEFT)
    m_ui.leftCapCheckBox->setCheckState(Qt::Checked);
  else
    m_ui.leftCapCheckBox->setCheckState(Qt::Unchecked);

  if(calcOptions.capping() & libXpertMass::CAP_RIGHT)
    m_ui.rightCapCheckBox->setCheckState(Qt::Checked);
  else
    m_ui.rightCapCheckBox->setCheckState(Qt::Unchecked);

  if(calcOptions.polymerEntities() & libXpertMass::POLYMER_CHEMENT_LEFT_END_MODIF)
    m_ui.leftModifCheckBox->setCheckState(Qt::Checked);
  else
    m_ui.leftModifCheckBox->setCheckState(Qt::Unchecked);

  if(calcOptions.polymerEntities() & libXpertMass::POLYMER_CHEMENT_RIGHT_END_MODIF)
    m_ui.rightModifCheckBox->setCheckState(Qt::Checked);
  else
    m_ui.rightModifCheckBox->setCheckState(Qt::Unchecked);

  if(calcOptions.monomerEntities() & libXpertMass::MONOMER_CHEMENT_MODIF)
    m_ui.modificationsCheckBox->setCheckState(Qt::Checked);
  else
    m_ui.modificationsCheckBox->setCheckState(Qt::Unchecked);

  if(calcOptions.monomerEntities() & libXpertMass::MONOMER_CHEMENT_CROSS_LINK)
    m_ui.crossLinksCheckBox->setCheckState(Qt::Checked);
  else
    m_ui.crossLinksCheckBox->setCheckState(Qt::Unchecked);
}


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

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


bool
CleavageDlg::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;
}

QString
CleavageDlg::lastUsedCleavageAgentName() const
{
  if(mpa_cleaver != nullptr)
    return mpa_cleaver->cleaveAgentName();
  else
    return QString("");
}

void
CleavageDlg::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
CleavageDlg::filterOptionsToggled()
{
  bool isChecked = m_ui.filteringOptionsGroupBox->isChecked();

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


void
CleavageDlg::filterPartial()
{
  // First off, we have to get the partial that is in the lineEdit.

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

  if(text.isEmpty())
    return;

  // Convert the string to a int.

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

  if(!partial && !ok)
    return;

  mpa_proxyModel->setPartialFilter(partial);
  mpa_proxyModel->setFilterKeyColumn(0);
  mpa_proxyModel->applyNewFilter();

  mp_focusWidget = m_ui.filterPartialLineEdit;
}


void
CleavageDlg::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
CleavageDlg::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
CleavageDlg::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
CleavageDlg::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
CleavageDlg::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("massXpert2 - 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
CleavageDlg::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 libXpertMass::MassPeakShaperConfig &config) {
      m_massPeakShaperConfig = config;

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

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


bool
CleavageDlg::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;
    }

  libXpertMass::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
CleavageDlg::synthesizeMassSpectra()
{

  // Clear the synthetic mass spectrum. Make sure the mass spectrum to clipboard
  // menu item is not "available".
  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);

  if(!m_massPeakShaperConfig.resolve())
    {
      QMessageBox msgBox;
      QString msg("Please, configure the mass peak shaper.");
      msgBox.setText(msg);
      msgBox.exec();

      configureMassPeakShaper();
    }

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

  OligomerList 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<libXpertMass::IsotopicData>(
      *mp_editorWnd->polChemDefRendering()->getPolChemDef()->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

  libXpertMass::IsotopicClusterGenerator isotopic_cluster_generator(
    msp_isotopicData);

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

  for(libXpertMass::OligomerSPtr oligomer_sp : selected_oligomers)
    {
      int charge           = oligomer_sp->charge();
      QString formula_text = oligomer_sp->elementalComposition();

      // qDebug() << "oligomer's elemental composition:" << formula_text
      //<< "with charge:" << charge;

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

      formula_charge_pairs.push_back(
        libXpertMass::FormulaChargePair(formula_text, charge));
    }

  // 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(
    libXpertMass::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 libXpertMass::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<libXpertMass::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(libXpertMass::IsotopicClusterChargePair &pair : isotopic_cluster_charge_pairs)
    pair.second = 1;

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

  libXpertMass::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 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->polymer()->filePath();

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


void
CleavageDlg::keyPressEvent(QKeyEvent *event)
{
  int modifiers = QGuiApplication::keyboardModifiers();

  if(event->key() == Qt::Key_P && modifiers & Qt::ControlModifier)
    {
      // The user wants to append the selected oligomers to the
      // clipboard.

      // qDebug() << __FILE__ << __LINE__
      // << "Ctrl-P";

      m_ui.exportResultsComboBox->setCurrentIndex(
        (int)ExportResultsActions::EXPORT_TO_CLIPBOARD_APPEND);
      m_ui.exportResultsComboBox->activated(
        (int)ExportResultsActions::EXPORT_TO_CLIPBOARD_APPEND);
      event->accept();
      return;
    }

  if(event->key() == Qt::Key_O && modifiers & Qt::ControlModifier)
    {
      // The user wants to overwrite the selected oligomers to the clipboard.

      // qDebug() << __FILE__ << __LINE__
      // << "Ctrl-O";

      m_ui.exportResultsComboBox->setCurrentIndex(
        (int)ExportResultsActions::EXPORT_TO_CLIPBOARD_OVERWRITE);
      m_ui.exportResultsComboBox->activated(
        (int)ExportResultsActions::EXPORT_TO_CLIPBOARD_OVERWRITE);

      event->accept();
      return;
    }

  if(event->key() == Qt::Key_F && modifiers & Qt::ControlModifier)
    {
      // The user wants to write the selected oligomers to file.

      // qDebug() << __FILE__ << __LINE__
      // << "Ctrl-F";

      m_ui.exportResultsComboBox->setCurrentIndex(
        (int)ExportResultsActions::EXPORT_TO_FILE);
      m_ui.exportResultsComboBox->activated(
        (int)ExportResultsActions::EXPORT_TO_FILE);
      event->accept();
      return;
    }

  if(event->key() == Qt::Key_S && modifiers & Qt::ControlModifier)
    {
      // The user wants to select the file in which to write the results.

      // qDebug() << __FILE__ << __LINE__
      // << "Ctrl-S";

      m_ui.exportResultsComboBox->setCurrentIndex(
        (int)ExportResultsActions::SELECT_FILE);
      m_ui.exportResultsComboBox->activated(
        (int)ExportResultsActions::SELECT_FILE);
      event->accept();
      return;
    }
}


// The results-exporting functions. ////////////////////////////////
// The results-exporting functions. ////////////////////////////////
// The results-exporting functions. ////////////////////////////////
void
CleavageDlg::showOutputPatternDefinitionHelp()
{
  QMessageBox msgBox;
  QString msg(
    "The following special variables are available to craft a formatting "
    "string\n\n");
  msg += "%f : filename\n";
  msg += "%i : sequence name (identity)\n";
  msg += "%n : oligomer name\n";
  msg += "%s : oligomer sequence\n";
  msg +=
    "%l : elided sequence, like \"LEFT...RIGH\" (first 4 left...4 last "
    "right)\n";
  msg += "%c : oligomer coordinates\n";
  msg += "%o : 0 if oligomer is not modified, 1 if it is\n";
  msg += "%p : partial cleavage\n";
  msg += "%e : cleavage agent name\n";
  msg += "%m : mono m/z\n";
  msg += "%a : avg m/z\n";
  msg += "%z : charge\n";

  msg += "\nNote that any text can be interspersed in the format string.\n";
  msg +=
    "If a '\\' is required in the output, it must be escaped like this: "
    "'\\\\'\n";
  msg +=
    "If a '%' is required in the output, it must be escaped like this: "
    "'\\%'\n";
  msg +=
    "If a newline (carriage return) is required in the output, write '\\n'\n";

  msg += "\nExample:\n\n";
  msg += "%e$%n$%s%m$%p\\n\n";
  msg += "will yield the following string:\n";
  msg += "Trypsin$0#20#z=1$SEQENCE$12458.2354$0\n";

  msgBox.setText(msg);

  msgBox.exec();
}


void
CleavageDlg::exportResults(int index)
{
  // Remember that we had set up the combobox with the following strings:
  // << tr("To Clipboard (overwrite)")
  // << tr("To Clipboard (append)")
  // << tr("To File")
  // << tr("Select File");

  if(index == 0)
    {
      exportResultsToClipboard(false /*!append*/);
    }
  else if(index == 1)
    {
      exportResultsToClipboard(true /*append*/);
    }
  else if(index == 2)
    {
      exportResultsFile();
    }
  else if(index == 3)
    {
      selectResultsFile();
    }
  else
    Q_ASSERT(0);
}


void
CleavageDlg::prepareResultsTxtString(bool append)
{
  // 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;
  bool noHeader              = false;
  libXpertMass::MassType massType = libXpertMass::MassType::MASS_NONE;

  forXpertMiner = m_ui.forXpertMinerCheckBox->isChecked();
  noHeader      = m_ui.noHeaderCheckBox->isChecked();

  if(!noHeader && !forXpertMiner && !append)
    {
      *mpa_resultsString += QString(
                              "# \n"
                              "# ---------------------------\n"
                              "# libXpertMass::Polymer sequence cleavage: %1\n"
                              "# ---------------------------\n")
                              .arg(mp_polymer->name());
    }
  else
    {
      // Then, we should ask whether the masses to be exported are
      // the mono or avg masses.

      if(m_ui.monoRadioButton->isChecked())
        massType = libXpertMass::MassType::MASS_MONO;
      else
        massType = libXpertMass::MassType::MASS_AVG;
    }

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

  QString pattern = m_ui.outputPatternComboBox->currentText();
  if(pattern == "Output pattern")
    pattern.clear();

  QString *text = m_ui.oligomerTableView->selectedOligomersAsPlainText(
    pattern, delimiter, withSequence, forXpertMiner, massType);
  *mpa_resultsString += *text;

  delete text;
}


bool
CleavageDlg::exportResultsToClipboard(bool append)
{
  // Below, if append is true, the prepared string will not start with the
  // usual stanza.
  prepareResultsTxtString(append);

  QClipboard *clipboard = QApplication::clipboard();

  // Depending on the fact that we are appending or not, we need to first
  // get the text from the clipboard or not.
  if(append)
    {
      QString clipboardText = clipboard->text(QClipboard::Clipboard);
      clipboardText.append(*mpa_resultsString);
      clipboard->setText(clipboardText, QClipboard::Clipboard);
    }
  else
    {
      clipboard->setText(*mpa_resultsString, QClipboard::Clipboard);
    }

  return true;
}


bool
CleavageDlg::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.setEncoding(QStringConverter::Utf8);

  prepareResultsTxtString();

  stream << *mpa_resultsString;

  file.close();

  return true;
}


bool
CleavageDlg::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
