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


/////////////////////// Local includes
#include "AtomDefDlg.hpp"
#include <libXpertMass/PolChemDef.hpp>
#include "PolChemDefWnd.hpp"
#include <libXpertMass/Isotope.hpp>


namespace MsXpS
{

namespace massxpert
{


  AtomDefDlg::AtomDefDlg(libXpertMass::PolChemDefSPtr polChemDefSPtr,
                         PolChemDefWnd *polChemDefWnd,
                         const QString &settingsFilePath,
                         const QString &applicationName,
                         const QString &description)
    : AbstractPolChemDefDependentDlg(polChemDefSPtr,
                                     polChemDefWnd,
                                     settingsFilePath,
                                     "AtomDefDlg",
                                     applicationName,
                                     description)
  {
    mp_list = msp_polChemDef->atomListPtr();

    if(!initialize())
      qFatal(
        "Fatal error at %s@%d. Failed to initialize the %s window. Program "
        "aborted.",
        __FILE__,
        __LINE__,
        m_wndTypeName.toLatin1().data());
  }


  AtomDefDlg::~AtomDefDlg()
  {
  }


  void
  AtomDefDlg::closeEvent([[maybe_unused]] QCloseEvent *event)
  {
    // No real close, because we did not ask that
    // close==destruction. Thus we only hide the dialog remembering its
    // position and size.

    mp_polChemDefWnd->m_ui.atomPushButton->setChecked(false);

    writeSettings();
  }


  void
  AtomDefDlg::readSettings()
  {
    QSettings settings(m_settingsFilePath, QSettings::IniFormat);

    settings.beginGroup(m_wndTypeName);
    restoreGeometry(settings.value("geometry").toByteArray());
    m_ui.splitter->restoreState(settings.value("splitter").toByteArray());
    settings.endGroup();
  }


  void
  AtomDefDlg::writeSettings()
  {

    QSettings settings(m_settingsFilePath, QSettings::IniFormat);

    settings.beginGroup(m_wndTypeName);
    restoreGeometry(settings.value("geometry").toByteArray());
    settings.setValue("splitter", m_ui.splitter->saveState());
    settings.endGroup();
  }


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

    // Now we need to actually show the window title (that element is empty in
    // m_ui)
    displayWindowTitle();

    // Set all the atoms to the list widget.

    for(int iter = 0; iter < mp_list->size(); ++iter)
      {
        libXpertMass::Atom *atom = mp_list->at(iter);

        m_ui.atomListWidget->addItem(atom->name());
      }

    readSettings();

    // Validator stuff for the atom symbol lineedit.
    QRegularExpression symbolRegExp("[A-Z][a-z]*");
    QValidator *validator = new QRegularExpressionValidator(symbolRegExp, this);

    m_ui.symbolLineEdit->setValidator(validator);

    // Make the connections.

    connect(m_ui.addAtomPushButton,
            SIGNAL(clicked()),
            this,
            SLOT(addAtomPushButtonClicked()));

    connect(m_ui.removeAtomPushButton,
            SIGNAL(clicked()),
            this,
            SLOT(removeAtomPushButtonClicked()));

    connect(m_ui.moveUpAtomPushButton,
            SIGNAL(clicked()),
            this,
            SLOT(moveUpAtomPushButtonClicked()));

    connect(m_ui.moveDownAtomPushButton,
            SIGNAL(clicked()),
            this,
            SLOT(moveDownAtomPushButtonClicked()));

    connect(m_ui.addIsotopePushButton,
            SIGNAL(clicked()),
            this,
            SLOT(addIsotopePushButtonClicked()));

    connect(m_ui.removeIsotopePushButton,
            SIGNAL(clicked()),
            this,
            SLOT(removeIsotopePushButtonClicked()));

    connect(m_ui.moveUpIsotopePushButton,
            SIGNAL(clicked()),
            this,
            SLOT(moveUpIsotopePushButtonClicked()));

    connect(m_ui.moveDownIsotopePushButton,
            SIGNAL(clicked()),
            this,
            SLOT(moveDownIsotopePushButtonClicked()));

    connect(m_ui.applyAtomPushButton,
            SIGNAL(clicked()),
            this,
            SLOT(applyAtomPushButtonClicked()));

    connect(m_ui.applyIsotopePushButton,
            SIGNAL(clicked()),
            this,
            SLOT(applyIsotopePushButtonClicked()));

    connect(m_ui.validatePushButton,
            SIGNAL(clicked()),
            this,
            SLOT(validatePushButtonClicked()));

    connect(m_ui.atomListWidget,
            SIGNAL(itemSelectionChanged()),
            this,
            SLOT(atomListWidgetItemSelectionChanged()));

    connect(m_ui.isotopeListWidget,
            SIGNAL(itemSelectionChanged()),
            this,
            SLOT(isotopeListWidgetItemSelectionChanged()));

    return true;
  }


  void
  AtomDefDlg::addAtomPushButtonClicked()
  {
    // We are asked to add a new atom. We'll add it right after the
    // current item.

    // Returns -1 if the list is empty.
    int index = m_ui.atomListWidget->currentRow();

    libXpertMass::Atom *newAtom =
      new libXpertMass::Atom(tr("Type Name"), tr("Type Symbol"));

    mp_list->insert(index, newAtom);
    m_ui.atomListWidget->insertItem(index, newAtom->name());

    setModified();

    // Needed so that the setCurrentRow() call below actually set the
    // current row!
    if(index <= 0)
      index = 0;

    m_ui.atomListWidget->setCurrentRow(index);

    // Erase isotope data that might be left over by precedent current
    // atom.
    updateIsotopeDetails(0);

    // Set the focus to the lineEdit that holds the name of the atom.
    m_ui.nameLineEdit->setFocus();
    m_ui.nameLineEdit->selectAll();
  }


  void
  AtomDefDlg::removeAtomPushButtonClicked()
  {
    QList<QListWidgetItem *> selectedList =
      m_ui.atomListWidget->selectedItems();

    if(selectedList.size() != 1)
      return;

    // Get the index of the current atom.
    int index = m_ui.atomListWidget->currentRow();

    QListWidgetItem *item = m_ui.atomListWidget->takeItem(index);
    delete item;

    libXpertMass::Atom *atom = mp_list->takeAt(index);
    Q_ASSERT(atom);
    delete atom;

    setModified();

    // If there are remaining items, we want to set the next item the
    // currentItem. If not, then, the currentItem should be the one
    // preceding the atom that we removed.

    if(m_ui.atomListWidget->count() >= index + 1)
      {
        m_ui.atomListWidget->setCurrentRow(index);
        atomListWidgetItemSelectionChanged();
      }

    // If there are no more items in the atom list, remove all the items
    // from the isotopeList.

    if(!m_ui.atomListWidget->count())
      {
        m_ui.isotopeListWidget->clear();
        clearAllDetails();
      }
  }


  void
  AtomDefDlg::moveUpAtomPushButtonClicked()
  {
    // Move the current row to one index less.

    // If no atom is selected, just return.

    QList<QListWidgetItem *> selectedList =
      m_ui.atomListWidget->selectedItems();

    if(selectedList.size() != 1)
      return;

    // Get the index of the atom and the atom itself.
    int index = m_ui.atomListWidget->currentRow();

    // If the item is already at top of list, do nothing.
    if(!index)
      return;

    mp_list->move(index, index - 1);

    QListWidgetItem *item = m_ui.atomListWidget->takeItem(index);

    m_ui.atomListWidget->insertItem(index - 1, item);
    m_ui.atomListWidget->setCurrentRow(index - 1);
    atomListWidgetItemSelectionChanged();

    setModified();
  }


  void
  AtomDefDlg::moveDownAtomPushButtonClicked()
  {
    // Move the current row to one index less.

    // If no atom is selected, just return.

    QList<QListWidgetItem *> selectedList =
      m_ui.atomListWidget->selectedItems();

    if(selectedList.size() != 1)
      return;

    // Get the index of the atom and the atom itself.
    int index = m_ui.atomListWidget->currentRow();

    // If the item is already at bottom of list, do nothing.
    if(index == m_ui.atomListWidget->count() - 1)
      return;

    mp_list->move(index, index + 1);

    QListWidgetItem *item = m_ui.atomListWidget->takeItem(index);
    m_ui.atomListWidget->insertItem(index + 1, item);
    m_ui.atomListWidget->setCurrentRow(index + 1);
    atomListWidgetItemSelectionChanged();

    setModified();
  }


  void
  AtomDefDlg::addIsotopePushButtonClicked()
  {
    // We are asked to add a new isotope. We'll add it right after the
    // current item. Note however, that one atom has to be selected.

    QList<QListWidgetItem *> selectedList =
      m_ui.atomListWidget->selectedItems();

    if(selectedList.size() != 1)
      {
        QMessageBox::information(
          this,
          QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
          tr("Please, select an atom first."),
          QMessageBox::Ok);
        return;
      }

    // Get the index of the current atom so that we know to which atom
    // we'll add the isotope.
    int index = m_ui.atomListWidget->currentRow();

    // What's the actual atom?
    libXpertMass::Atom *atom = mp_list->at(index);
    Q_ASSERT(atom);

    // Allocate the new isotope.
    libXpertMass::Isotope *newIsotope = new libXpertMass::Isotope();

    // Get the row index of the current isotope item. Returns -1 if the
    // list is empty.
    index = m_ui.isotopeListWidget->currentRow();

    m_ui.isotopeListWidget->insertItem(
      index, newIsotope->massString(libXpertMass::ATOM_DEC_PLACES));

    // Needed so that the setCurrentRow() call below actually set the
    // current row!
    if(index <= 0)
      index = 0;

    atom->insertIsotopeAt(index, newIsotope);

    m_ui.isotopeListWidget->setCurrentRow(index);

    setModified();

    // Set the focus to the lineEdit that holds the mass of the isotope.
    m_ui.isotopeMassLineEdit->setFocus();
    m_ui.isotopeMassLineEdit->selectAll();
  }


  void
  AtomDefDlg::removeIsotopePushButtonClicked()
  {
    QList<QListWidgetItem *> selectedList =
      m_ui.isotopeListWidget->selectedItems();

    if(selectedList.size() != 1)
      return;


    // Get the index of the current atom so that we know from which atom
    // we'll remove the isotope.
    int index = m_ui.atomListWidget->currentRow();

    libXpertMass::Atom *atom = mp_list->at(index);
    Q_ASSERT(atom);

    // Get the index of the current isotope.
    index = m_ui.isotopeListWidget->currentRow();

    // First remove the item from the listwidget because that will have
    // isotopeListWidgetItemSelectionChanged() triggered and we have to
    // have the item in the isotope list in the atom! Otherwise a crash
    // occurs.
    QListWidgetItem *item = m_ui.isotopeListWidget->takeItem(index);
    delete item;

    // Remove the isotope from the atom proper.

    QList<libXpertMass::Isotope *> atomIsotopeList = atom->isotopeList();

    libXpertMass::Isotope *isotope = atomIsotopeList.at(index);

    atom->removeIsotopeAt(index);
    delete isotope;

    // If there are remaining items, we want to set the next item the
    // currentItem. If not, then, the currentItem should be the one
    // preceding the atom that we removed.

    if(m_ui.isotopeListWidget->count() >= index + 1)
      {
        m_ui.isotopeListWidget->setCurrentRow(index);
        isotopeListWidgetItemSelectionChanged();
      }

    // We have changed the isotopic constitution of the chemical
    // element, we have to recompute the mono/avg masses.

    atom->calculateMasses();

    // If there are no more items in the isotope list, remove all the
    // details.

    if(!m_ui.isotopeListWidget->count())
      {
        updateAtomMassDetails(0);
        updateIsotopeDetails(0);
      }
    else
      {
        updateAtomMassDetails(atom);
      }

    setModified();
  }


  void
  AtomDefDlg::moveUpIsotopePushButtonClicked()
  {
    // Move the current row to one index less.

    // If no isotope is selected, just return.

    QList<QListWidgetItem *> selectedList =
      m_ui.isotopeListWidget->selectedItems();

    if(selectedList.size() != 1)
      return;

    // Get the atom to which the isotope belongs.
    int index           = m_ui.atomListWidget->currentRow();
    libXpertMass::Atom *atom = mp_list->at(index);

    // Get the index of the current isotope item.
    index = m_ui.isotopeListWidget->currentRow();

    // If the item is already at top of list, do nothing.
    if(!index)
      return;

    // Get the isotope itself from the atom.
    libXpertMass::Isotope *isotope = atom->isotopeList().at(index);

    atom->removeIsotopeAt(index);
    atom->insertIsotopeAt(index - 1, isotope);

    QListWidgetItem *item = m_ui.isotopeListWidget->takeItem(index);
    m_ui.isotopeListWidget->insertItem(index - 1, item);
    m_ui.isotopeListWidget->setCurrentRow(index - 1);
    isotopeListWidgetItemSelectionChanged();

    setModified();
  }


  void
  AtomDefDlg::moveDownIsotopePushButtonClicked()
  {
    // Move the current row to one index less.

    // If no isotope is selected, just return.

    QList<QListWidgetItem *> selectedList =
      m_ui.isotopeListWidget->selectedItems();

    if(selectedList.size() != 1)
      return;

    // Get the atom to which the isotope belongs.
    int index           = m_ui.atomListWidget->currentRow();
    libXpertMass::Atom *atom = mp_list->at(index);

    // Get the index of the current isotope item.
    index = m_ui.isotopeListWidget->currentRow();

    // If the item is already at top of list, do nothing.
    if(index == m_ui.isotopeListWidget->count() - 1)
      return;

    // Get the isotope itself from the atom.
    libXpertMass::Isotope *isotope = atom->isotopeList().at(index);

    atom->removeIsotopeAt(index);
    atom->insertIsotopeAt(index + 1, isotope);

    QListWidgetItem *item = m_ui.isotopeListWidget->takeItem(index);
    m_ui.isotopeListWidget->insertItem(index + 1, item);
    m_ui.isotopeListWidget->setCurrentRow(index + 1);
    isotopeListWidgetItemSelectionChanged();

    setModified();
  }


  void
  AtomDefDlg::applyAtomPushButtonClicked()
  {
    // We are asked to apply the data for the atom.

    // If no atom is selected, just return.

    QList<QListWidgetItem *> selectedList =
      m_ui.atomListWidget->selectedItems();

    if(selectedList.size() != 1)
      return;

    // Get the index of the current atom item.
    int index = m_ui.atomListWidget->currentRow();

    libXpertMass::Atom *atom = mp_list->at(index);

    // We do not want more than one atom by the same name or the same
    // symbol.

    QString editName = m_ui.nameLineEdit->text();

    // The syntax [A-Z][a-z]* is automatically validated using the
    // validator(see initialize()).
    QString editSymbol = m_ui.symbolLineEdit->text();

    // If an atom is found in the list with any of these two strings,
    // and that atom is not the one that is current in the atom list,
    // then we are making a double entry, which is not allowed.

    int nameRes = libXpertMass::Atom::nameIndex(editName, *mp_list);
    if(nameRes != -1 && nameRes != index)
      {
        QMessageBox::warning(
          this,
          QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
          tr("An atom with same name exists already."),
          QMessageBox::Ok);
        return;
      }

    int symbolRes = libXpertMass::Atom::symbolIndex(editSymbol, *mp_list);
    if(symbolRes != -1 && symbolRes != index)
      {
        QMessageBox::warning(
          this,
          QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
          tr("An atom with same symbol exists already."),
          QMessageBox::Ok);
        return;
      }

    atom->setName(editName);
    atom->setSymbol(editSymbol);

    // Update the list widget item.

    QListWidgetItem *item = m_ui.atomListWidget->currentItem();
    item->setData(Qt::DisplayRole, atom->name());

    setModified();
  }


  void
  AtomDefDlg::applyIsotopePushButtonClicked()
  {
    // We are asked to apply the data for the isotope.

    // If no isotope is selected, just return.

    QList<QListWidgetItem *> selectedList =
      m_ui.isotopeListWidget->selectedItems();

    if(selectedList.size() != 1)
      return;

    // Get the atom to which the isotope belongs.
    int index           = m_ui.atomListWidget->currentRow();
    libXpertMass::Atom *atom = mp_list->at(index);

    // Get the index of the current isotope item.
    index = m_ui.isotopeListWidget->currentRow();

    // Get the isotope itself from the atom.
    libXpertMass::Isotope *isotope = atom->isotopeList().at(index);

    QString editMass      = m_ui.isotopeMassLineEdit->text();
    QString editAbundance = m_ui.isotopeAbundanceLineEdit->text();

    bool ok           = false;
    double doubleMass = editMass.toDouble(&ok);

    if(doubleMass == 0.0 && !ok)
      {
        QMessageBox::warning(
          this,
          QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
          tr("Failed to convert %1 to a double.").arg(editMass),
          QMessageBox::Ok);
        return;
      }

    ok                     = false;
    double doubleAbundance = editAbundance.toDouble(&ok);

    if(doubleAbundance == 0.0 && !ok)
      {
        QMessageBox::warning(
          this,
          QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
          tr("Failed to convert %1 to a double.").arg(editAbundance),
          QMessageBox::Ok);
        return;
      }

    isotope->setMass(doubleMass);
    isotope->setAbundance(doubleAbundance);

    // Update the list widget item.

    QListWidgetItem *item = m_ui.isotopeListWidget->currentItem();
    QString mass          = isotope->massString(libXpertMass::ATOM_DEC_PLACES);
    item->setData(Qt::DisplayRole, mass);

    atom->calculateMasses();

    updateAtomMassDetails(atom);

    setModified();
  }


  bool
  AtomDefDlg::validatePushButtonClicked()
  {
    QStringList errorList;

    // All we have to do is validate the atom definition. For that we'll
    // go in the listwidget items one after the other and make sure that
    // everything is fine and that colinearity is perfect between the
    // atom list and the listwidget.

    int itemCount = m_ui.atomListWidget->count();

    if(itemCount != mp_list->size())
      {
        errorList << QString(
          tr("\nThe number of atoms in in the list widget \n"
             "and in the list of atoms is not identical.\n"));

        QMessageBox::warning(
          this,
          QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
          errorList.join("\n"),
          QMessageBox::Ok);
        return false;
      }

    for(int iter = 0; iter < mp_list->size(); ++iter)
      {
        QListWidgetItem *item = m_ui.atomListWidget->item(iter);

        libXpertMass::Atom *atom = mp_list->at(iter);

        if(item->text() != atom->name())
          errorList << QString(tr("\nAtom at index %1 has not the same\n"
                                  "name as the list widget item at the\n"
                                  "same index.\n")
                                 .arg(iter));

        if(!atom->validate())
          errorList << QString(
            tr("\nAtom at index %1 failed to validate.\n").arg(iter));
      }

    if(errorList.size())
      {
        QMessageBox::warning(
          this,
          QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
          errorList.join("\n"),
          QMessageBox::Ok);
        return false;
      }
    else
      {
        QMessageBox::warning(
          this,
          QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
          ("Validation: success\n"),
          QMessageBox::Ok);
      }

    return true;
  }


  void
  AtomDefDlg::atomListWidgetItemSelectionChanged()
  {
    // The atom item has changed. Empty the isotope list and update its
    // contents. Update the details for the atom.

    // The list is a single-item-selection list.

    QList<QListWidgetItem *> selectedList =
      m_ui.atomListWidget->selectedItems();

    if(selectedList.size() != 1)
      return;

    // Get the index of the current atom.
    int index = m_ui.atomListWidget->currentRow();

    libXpertMass::Atom *atom = mp_list->at(index);
    Q_ASSERT(atom);

    // Set the data of the atom to their respective widgets.
    updateAtomIdentityDetails(atom);
    updateAtomMassDetails(atom);

    // The list of isotopes
    m_ui.isotopeListWidget->clear();

    for(int iter = 0; iter < atom->isotopeList().size(); ++iter)
      {
        libXpertMass::Isotope *isotope = atom->isotopeList().at(iter);

        m_ui.isotopeListWidget->addItem(
          isotope->massString(libXpertMass::ATOM_DEC_PLACES));
      }

    if(!m_ui.isotopeListWidget->count())
      updateIsotopeDetails(0);
    else
      {
        // And now select the first row in the isotope list widget.
        m_ui.isotopeListWidget->setCurrentRow(0);
      }
  }


  void
  AtomDefDlg::isotopeListWidgetItemSelectionChanged()
  {
    // The isotope item has changed. Update the details for the isotope.

    // The list is a single-item-selection list.

    QList<QListWidgetItem *> selectedList =
      m_ui.isotopeListWidget->selectedItems();

    if(selectedList.size() != 1)
      return;

    // Get the index of the current atom.
    int index = m_ui.atomListWidget->currentRow();

    // Find the isotope object in the list of isotopes.
    libXpertMass::Atom *atom = mp_list->at(index);
    Q_ASSERT(atom);

    // Get the index of the current isotope.
    index = m_ui.isotopeListWidget->currentRow();

    // Get the isotope that is currently selected from the atom's list
    // of isotopes.
    libXpertMass::Isotope *isotope = atom->isotopeList().at(index);
    Q_ASSERT(isotope);

    // Set the data of the isotope to their respective widgets.
    updateIsotopeDetails(isotope);
  }


  void
  AtomDefDlg::updateAtomIdentityDetails(libXpertMass::Atom *atom)
  {
    if(atom)
      {
        m_ui.nameLineEdit->setText(atom->name());
        m_ui.symbolLineEdit->setText(atom->symbol());
      }
    else
      {
        m_ui.nameLineEdit->setText("");
        m_ui.symbolLineEdit->setText("");
      }
  }


  void
  AtomDefDlg::updateAtomMassDetails(libXpertMass::Atom *atom)
  {
    if(atom)
      {
        m_ui.monoMassLineEdit->setText(
          atom->monoString(libXpertMass::ATOM_DEC_PLACES));
        m_ui.avgMassLineEdit->setText(
          atom->avgString(libXpertMass::ATOM_DEC_PLACES));
      }
    else
      {
        m_ui.monoMassLineEdit->setText("");
        m_ui.avgMassLineEdit->setText("");
      }
  }


  void
  AtomDefDlg::updateIsotopeDetails(libXpertMass::Isotope *isotope)
  {
    if(isotope)
      {
        m_ui.isotopeMassLineEdit->setText(
          isotope->massString(libXpertMass::ATOM_DEC_PLACES));
        m_ui.isotopeAbundanceLineEdit->setText(
          isotope->abundanceString(libXpertMass::ATOM_DEC_PLACES));
      }
    else
      {
        m_ui.isotopeMassLineEdit->setText("");
        m_ui.isotopeAbundanceLineEdit->setText("");
      }
  }


  void
  AtomDefDlg::clearAllDetails()
  {
    m_ui.nameLineEdit->setText("");
    m_ui.symbolLineEdit->setText("");

    m_ui.monoMassLineEdit->setText("");
    m_ui.avgMassLineEdit->setText("");

    m_ui.isotopeMassLineEdit->setText("");
    m_ui.isotopeAbundanceLineEdit->setText("");
  }


  // VALIDATION
  bool
  AtomDefDlg::validate()
  {
    return validatePushButtonClicked();
  }

} // namespace massxpert

} // namespace MsXpS
