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


/////////////////////// libXpertMass includes
#include <MsXpS/libXpertMassCore/PolChemDef.hpp>
#include <MsXpS/libXpertMassCore/Monomer.hpp>


/////////////////////// Local includes
#include "MonomerDefDlg.hpp"
#include "PolChemDefWnd.hpp"


namespace MsXpS
{

namespace MassXpert
{


MonomerDefDlg::MonomerDefDlg(libXpertMassCore::PolChemDefSPtr pol_chem_def_sp,
                             PolChemDefWnd *pol_chem_def_wnd_p,
                             const QString &settings_file_path,
                             const QString &application_name,
                             const QString &description)
  : AbstractPolChemDefDependentDlg(pol_chem_def_sp,
                                   pol_chem_def_wnd_p,
                                   settings_file_path,
                                   "MonomerDefDlg",
                                   application_name,
                                   description),
    mref_monomers(pol_chem_def_sp->getMonomersRef())
{
  if(!initialize())
    qFatal() << "Failed to initialize the dialog window.";
}


MonomerDefDlg::~MonomerDefDlg()
{
}


void
MonomerDefDlg::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_pol_chem_def_wnd_p->m_ui.monomerPushButton->setChecked(false);

  writeSettings();
}


void
MonomerDefDlg::readSettings()
{
  QSettings settings(m_settings_file_path, QSettings::IniFormat);

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


void
MonomerDefDlg::writeSettings()
{

  QSettings settings(m_settings_file_path, QSettings::IniFormat);

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


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

  setWindowIcon(qApp->windowIcon());

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

  // Set all the monomers to the list widget.
  for(libXpertMassCore::MonomerSPtr monomer_sp : mref_monomers)
    m_ui.monomerListWidget->addItem(monomer_sp->getName());

  readSettings();

  // Get the monomer code length and create a validator for use later.

  m_ui.codeLengthSpinBox->setValue(msp_polChemDef->getCodeLength());

  QString regExp =
    QString("[A-Z][a-z]{0,%1}").arg(msp_polChemDef->getCodeLength() - 1);

  QRegularExpression codeRegExp(regExp);
  QValidator *validator = new QRegularExpressionValidator(codeRegExp, this);

  m_ui.codeLineEdit->setValidator(validator);


  // Make the connections.

  connect(m_ui.addMonomerPushButton,
          SIGNAL(clicked()),
          this,
          SLOT(addMonomerPushButtonClicked()));

  connect(m_ui.removeMonomerPushButton,
          SIGNAL(clicked()),
          this,
          SLOT(removeMonomerPushButtonClicked()));

  connect(m_ui.moveUpMonomerPushButton,
          SIGNAL(clicked()),
          this,
          SLOT(moveUpMonomerPushButtonClicked()));

  connect(m_ui.moveDownMonomerPushButton,
          SIGNAL(clicked()),
          this,
          SLOT(moveDownMonomerPushButtonClicked()));

  connect(m_ui.applyMonomerPushButton,
          SIGNAL(clicked()),
          this,
          SLOT(applyMonomerPushButtonClicked()));

  connect(m_ui.formulaLineEdit,
          SIGNAL(textChanged(const QString &)),
          this,
          SLOT(formulaLineEditTextChanged(const QString &)));

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

  connect(m_ui.monomerListWidget,
          SIGNAL(itemSelectionChanged()),
          this,
          SLOT(monomerListWidgetItemSelectionChanged()));

  connect(m_ui.codeLengthSpinBox,
          SIGNAL(valueChanged(int)),
          this,
          SLOT(codeLengthSpinBoxValueChanged(int)));

  connect(m_ui.calculateMassDifferencesPushButton,
          SIGNAL(clicked()),
          this,
          SLOT(calculateMassDifferences()));

  return true;
}


void
MonomerDefDlg::addMonomerPushButtonClicked()
{
  // We are asked to add a new monomer. We'll add it right after the
  // current item.

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

  libXpertMassCore::MonomerSPtr monomer_sp =
    std::make_shared<libXpertMassCore::Monomer>(
      msp_polChemDef, tr("Type Name"), tr("Type Code"), tr("Type Formula"));

  mref_monomers.insert(mref_monomers.begin() + index, monomer_sp);
  m_ui.monomerListWidget->insertItem(index, monomer_sp->getName());

  setModified();

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

  m_ui.monomerListWidget->setCurrentRow(index);

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


void
MonomerDefDlg::removeMonomerPushButtonClicked()
{
  QList<QListWidgetItem *> selectedList =
    m_ui.monomerListWidget->selectedItems();

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

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

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

  mref_monomers.erase(mref_monomers.begin() + index);

  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 monomer that we removed.

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

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

  if(!m_ui.monomerListWidget->count())
    {
      clearAllDetails();
    }
}


void
MonomerDefDlg::moveUpMonomerPushButtonClicked()
{
  // Move the current row to one index less.

  // If no monomer is selected, just return.

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

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

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

  auto it = mref_monomers.begin() + index;
  // If the item is already at top of list, do nothing.
  if(it == mref_monomers.begin())
    return;
  std::iter_swap(it, it - 1); // Swap with the previous element

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

  m_ui.monomerListWidget->insertItem(index - 1, item);
  m_ui.monomerListWidget->setCurrentRow(index - 1);
  monomerListWidgetItemSelectionChanged();

  setModified();
}


void
MonomerDefDlg::moveDownMonomerPushButtonClicked()
{
  // Move the current row to one index less.

  // If no monomer is selected, just return.

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

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

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

  auto it = mref_monomers.begin() + index;
  // If the item is already at bottom of list, do nothing.
  if(it == mref_monomers.end())
    return;
  std::iter_swap(it, it + 1); // Swap with the next element

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

  setModified();
}


void
MonomerDefDlg::applyMonomerPushButtonClicked()
{
  // We are asked to apply the data for the monomer.

  // If no monomer is selected, just return.

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

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

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

  libXpertMassCore::MonomerSPtr monomer_sp = mref_monomers.at(index);

  // We do not want more than one monomer 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 editCode = m_ui.codeLineEdit->text();

  QString editFormula = m_ui.formulaLineEdit->text();

  // If a monomer is found in the member ref to vector with any of these two
  // strings, and that monomer is not the one that is current in that vector,
  // then we are making a double entry, which is not allowed.

  std::vector<libXpertMassCore::MonomerSPtr>::iterator the_iterator =
    std::find_if(mref_monomers.begin(),
                 mref_monomers.end(),
                 [editName](const libXpertMassCore::MonomerSPtr iter_monomer_sp) {
                   return iter_monomer_sp->getName() == editName;
                 });

  if(the_iterator != mref_monomers.end() &&
     std::distance(mref_monomers.begin(), the_iterator) != index)
    {
      QMessageBox::warning(this,
                           tr("MassXpert3 - Monomer definition"),
                           tr("A monomer with same name "
                              "exists already."),
                           QMessageBox::Ok);
      return;
    }

  the_iterator =
    std::find_if(mref_monomers.begin(),
                 mref_monomers.end(),
                 [editCode](const libXpertMassCore::MonomerSPtr iter_monomer_sp) {
                   return iter_monomer_sp->getCode() == editCode;
                 });

  if(the_iterator != mref_monomers.end() &&
     std::distance(mref_monomers.begin(), the_iterator) != index)
    {
      QMessageBox::warning(this,
                           tr("MassXpert3 - Monomer definition"),
                           tr("A monomer with same code "
                              "exists already."),
                           QMessageBox::Ok);
      return;
    }

  // At this point, validate the formula:

  libXpertMassCore::Formula formula(editFormula);

  libXpertMassCore::IsotopicDataCstSPtr isotopic_data_csp =
    msp_polChemDef->getIsotopicDataCstSPtr();
  libXpertMassCore::ErrorList error_list;

  if(!formula.validate(isotopic_data_csp, &error_list))
    {
      QMessageBox::warning(this,
                           tr("MassXpert3 - Monomer definition"),
                           tr("The formula failed to validate."),
                           QMessageBox::Ok);
      return;
    }

  // Finally we can update the monomer's data:

  monomer_sp->setName(editName);
  monomer_sp->setCode(editCode);
  monomer_sp->setFormula(editFormula);

  // Update the list widget item.

  QListWidgetItem *item = m_ui.monomerListWidget->currentItem();
  item->setData(Qt::DisplayRole, monomer_sp->getName());

  setModified();
}


bool
MonomerDefDlg::validatePushButtonClicked()
{
  QStringList errorList;

  // All we have to do is validate the monomer 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
  // monomer list and the listwidget.

  std::size_t item_count = m_ui.monomerListWidget->count();

  if(item_count != mref_monomers.size())
    {
      errorList << QString(
        tr("\nThe number of monomers in the "
           "list widget \n"
           "and in the list of monomers is "
           "not identical.\n"));

      QMessageBox::warning(this,
                           tr("MassXpert3 - Monomer definition"),
                           errorList.join("\n"),
                           QMessageBox::Ok);
      return false;
    }

  for(std::size_t iter = 0; iter < mref_monomers.size(); ++iter)
    {
      QListWidgetItem *item = m_ui.monomerListWidget->item(iter);

      libXpertMassCore::MonomerSPtr monomer_sp = mref_monomers.at(iter);

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

      MsXpS::libXpertMassCore::ErrorList error_list;

      if(!monomer_sp->validate(&error_list))
        errorList << QString(
                       "\nMonomer at index %1  failed to validate.\n"
                       "Errors: %2\n\n")
                       .arg(iter)
                       .arg(libXpertMassCore::Utils::joinErrorList(error_list));
    }

  if(errorList.size())
    {
      QMessageBox::warning(this,
                           tr("MassXpert3 - Monomer definition"),
                           errorList.join("\n"),
                           QMessageBox::Ok);
      return false;
    }
  else
    {
      QMessageBox::warning(this,
                           tr("MassXpert3 - Monomer definition"),
                           ("Validation: success\n"),
                           QMessageBox::Ok);
    }

  return true;
}


void
MonomerDefDlg::monomerListWidgetItemSelectionChanged()
{
  // The monomer item has changed. Update the details for the monomer.

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

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

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

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

  // Note that this should not occur, I guess, but once I got this crash, so
  // test that the returned index is not -1.
  if(index >= 0)
    {
      libXpertMassCore::MonomerSPtr monomer_sp = mref_monomers.at(index);
      if(monomer_sp == nullptr)
        qFatal() << "Programming error.";

      // qDebug() << "Currently selected monomer:" << monomer->name();

      // Set the data of the monomer to their respective widgets.
      updateMonomerDetails(monomer_sp);
    }
}


void
MonomerDefDlg::codeLengthSpinBoxValueChanged(int value)
{
  // We have to update the validator for the monomer code line edit.

  if(value < 1)
    return;

  // We need to update the monomer code RegExp validator with the new
  // codeLength value.

  QString regExp = QString("[A-Z][a-z]{0,%1}").arg(value - 1);
  //    qDebug() << "new regExp:" << regExp;

  QRegularExpression codeRegExp(regExp);
  QValidator *validator = new QRegularExpressionValidator(codeRegExp, this);

  m_ui.codeLineEdit->setValidator(validator);

  // And now inform the whole polymer definition that the codeLength
  // value has changed.

  msp_polChemDef->setCodeLength(value);
}


void
MonomerDefDlg::updateMonomerDetails(libXpertMassCore::MonomerSPtr monomer_sp)
{
  if(monomer_sp != nullptr)
    {
      m_ui.nameLineEdit->setText(monomer_sp->getName());
      m_ui.codeLineEdit->setText(monomer_sp->getCode());
      m_ui.formulaLineEdit->setText(monomer_sp->getFormula());
    }
  else
    {
      m_ui.nameLineEdit->setText("");
      m_ui.codeLineEdit->setText("");
      m_ui.formulaLineEdit->setText("");
    }
}


void
MonomerDefDlg::clearAllDetails()
{
  m_ui.nameLineEdit->setText("");
  m_ui.codeLineEdit->setText("");
  m_ui.formulaLineEdit->setText("");
}


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

void
MonomerDefDlg::formulaLineEditTextChanged(const QString &string)
{
  // Each time the formula of the monomer is edited, we compute the
  // masses of the formula. If the computation failed, we set "Bad
  // formula" in the line edit widgets of the masses.

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


  libXpertMassCore::Formula formula(string);

  // qDebug() << "Formula of the monomer:" << string;

  double mono = 0;
  double avg  = 0;
  bool ok     = false;

  formula.accountMasses(ok, isotopic_data_csp, mono, avg);

  if(!ok)
    {
      m_ui.monoMassLineEdit->setText(tr("Bad formula"));
      m_ui.avgMassLineEdit->setText(tr("Bad formula"));

      return;
    }

  m_ui.monoMassLineEdit->setText(
    QString::number(mono, 'f', libXpertMassCore::ATOM_DEC_PLACES));
  m_ui.avgMassLineEdit->setText(
    QString::number(avg, 'f', libXpertMassCore::ATOM_DEC_PLACES));
}


void
MonomerDefDlg::calculateMassDifferences()
{
  // Allocate a new QStringList object in which all the mass
  // differences between any two monomers in the definitions are
  // below the threshold set into the thresholdLineEdit.

  bool ok = false;

  double threshold = m_ui.thresholdLineEdit->text().toDouble(&ok);

  if(!threshold && !ok)
    {
      QMessageBox::warning(this,
                           tr("MassXpert3 - Monomer definition"),
                           tr("Threshold value is not valid"),
                           QMessageBox::Ok);
      return;
    }

  bool isMonoMass = m_ui.monoMassRadioButton->isChecked();

  libXpertMassCore::Enums::MassType monoOrAvg;

  if(isMonoMass)
    monoOrAvg = libXpertMassCore::Enums::MassType::MONO;
  else
    monoOrAvg = libXpertMassCore::Enums::MassType::AVG;

  QStringList diffList =
    msp_polChemDef->differenceBetweenMonomers(threshold, monoOrAvg);

  if(!diffList.size())
    return;

  QMessageBox::information(this,
                           tr("MassXpert3 - Monomer definition"),
                           diffList.join("\n"),
                           QMessageBox::Ok);

  return;
}


} // namespace MassXpert

} // namespace MsXpS
