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

/////////////////////// stdlib includes
#include <set>

/////////////////////// Qt includes
#include <QMessageBox>
#include <QSettings>
#include <QFileDialog>
#include <QTimer>


/////////////////////// libXpertMass includes
#include <MsXpS/libXpertMassCore/PolChemDef.hpp>
#include <MsXpS/libXpertMassCore/Isotope.hpp>
#include <MsXpS/libXpertMassCore/IsotopicData.hpp>
#include <MsXpS/libXpertMassCore/IsotopicDataLibraryHandler.hpp>
#include <MsXpS/libXpertMassCore/IsotopicDataUserConfigHandler.hpp>


/////////////////////// libXpertMass includes
#include <MsXpS/libXpertMassGui/IsotopicDataTableViewModel.hpp>

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


namespace MsXpS
{

namespace MassXpert
{


IsotopeDefDlg::IsotopeDefDlg(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,
                                   "IsotopeDefDlg",
                                   application_name,
                                   description)
{

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


IsotopeDefDlg::~IsotopeDefDlg()
{
}


void
IsotopeDefDlg::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.isotopePushButton->setChecked(false);

  writeSettings();
}


void
IsotopeDefDlg::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
IsotopeDefDlg::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
IsotopeDefDlg::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();

  if(!setupIsotopicDataTableView())
    {
      message("Failed to setup the Isotopic data into the table view.");
      return false;
    }

  readSettings();

  // Make the connections.

  connect(m_ui.insertIsotopeAbovePushButton,
          &QPushButton::clicked,
          mpa_isotopicDataTableViewModel,
          &libXpertMassGui::IsotopicDataTableViewModel::insertIsotopeRowAbove);


  connect(m_ui.insertIsotopeBelowPushButton,
          &QPushButton::clicked,
          mpa_isotopicDataTableViewModel,
          &libXpertMassGui::IsotopicDataTableViewModel::insertIsotopeRowBelow);

  connect(m_ui.removeSelectedPushButton,
          &QPushButton::clicked,
          mpa_isotopicDataTableViewModel,
          &libXpertMassGui::IsotopicDataTableViewModel::removeSelected);

  connect(m_ui.validatePushButton,
          &QPushButton::clicked,
          this,
          &IsotopeDefDlg::validate);

  connect(m_ui.loadDataPushButton,
          &QPushButton::clicked,
          this,
          &IsotopeDefDlg::loadData);

  connect(
    m_ui.savePushButton, &QPushButton::clicked, this, &IsotopeDefDlg::save);

  connect(
    m_ui.saveAsPushButton, &QPushButton::clicked, this, &IsotopeDefDlg::saveAs);

  connect(mpa_isotopicDataTableViewModel,
          &libXpertMassGui::IsotopicDataTableViewModel::modifiedSignal,
          this,
          [this]() { setModified(); });

  // connect(mpa_isotopicDataTableViewModel,
  //&libXpertMassGui::IsotopicDataTableViewModel::newProbabilityDataSignal,
  // this,
  //&IsotopeDefDlg::newProbabilityDataSignal);

  return true;
}


// VALIDATION
bool
IsotopeDefDlg::validate()
{
  libXpertMassCore::ErrorList error_list;

  msp_polChemDef->getIsotopicDataSPtr()->validate(&error_list);

  if(error_list.size())
    {

      qDebug() << "Validation failed.";

      QString joined_errors;

      for(auto error : error_list)
        {
          qDebug() << error;

          joined_errors.append(error + "\n");
        }

      QMessageBox::warning(
        this,
        QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
        joined_errors,
        QMessageBox::Ok);
      return false;
    }
  else
    message("Validation succeeded.", 5000);

  return true;
}

// LOADING
bool
IsotopeDefDlg::loadData()
{
  // Load isotopic data from file.

  // Either there are data already or not. We need to clear the isotopic data if
  // some are alredy in there.

  if(msp_polChemDef->getIsotopicDataSPtr()->size())
    {
      message("The Isotopic data are going to be cleared.");

      // We need to ask the model to remove all the isotope items.

      mpa_isotopicDataTableViewModel->clearAllData();
    }


  // Provide the user with the opportunity to define a file name.

  QString file_path = QFileDialog::getOpenFileName(
    this, tr("Load the Isotopic data from file"), QDir::homePath());

  if(file_path.isEmpty())
    {
      message("The file name is empty.", 5000);
      return false;
    }

  // We need to instantiate the proper isotopic data handler. In our present
  // case that has to be user config handler, we do not want the library
  // handler.

  libXpertMassCore::IsotopicDataUserConfigHandler isotopic_data_handler(
    msp_polChemDef->getIsotopicDataSPtr());

  if(!isotopic_data_handler.loadData(file_path))
    {
      message("Failed to load the isotopic data from the file.", 5000);
      return false;
    }

  mpa_isotopicDataTableViewModel->refreshAfterNewData();

  return true;
}


// SAVING
bool
IsotopeDefDlg::save()
{

  // We are in the context of the polymer chemistry definition, which means,
  // saving that definition involves saving the isotopic data to a file named
  // "isotopic-data.dat" in the polymer chemistry definition's directory.

  // Only work if there is at least one isotope in the data!

  if(!msp_polChemDef->getIsotopicDataSPtr()->size())
    {
      message("The Isotopic data are empty.");
      return false;
    }

  if(!validate())
    {
      message("The Isotopic data could not be validated.", 5000);
      return false;
    }

  // We need to instantiate the proper isotopic data handler. In our present
  // case that can be user config handler, we do not need the library handler.

  libXpertMassCore::IsotopicDataUserConfigHandler isotopic_data_handler(
    msp_polChemDef->getIsotopicDataSPtr());

  bool res = false;

  // Do we have a file name?

  QString local_file_name = msp_polChemDef->getIsotopicDataFilePath();

  qDebug() << "Isotopic data file path in the polymer chemistry definition: "
           << local_file_name;

  if(local_file_name.isEmpty())
    res = isotopic_data_handler.writeData();
  else
    res = isotopic_data_handler.writeData(local_file_name);

  if(!res)
    message("Saving of the data failed. Maybe use SaveAs?", 5000);
  else
    message("Saving of the data succeeded", 5000);

  return res;
}


bool
IsotopeDefDlg::saveAs()
{
  // Only work if there is at least one isotope in the data!

  if(!msp_polChemDef->getIsotopicDataSPtr()->size())
    {
      message("The Isotopic data are empty.", 5000);
      return false;
    }

  if(!validate())
    {
      message("The Isotopic data could not be validated.", 5000);
      return false;
    }

  // We need to instantiate the proper isotopic data handler. In our present
  // case that has to be user config handler, we do not want the library
  // handler.

  libXpertMassCore::IsotopicDataUserConfigHandler isotopic_data_handler(
    msp_polChemDef->getIsotopicDataSPtr());

  // Provide the user with the opportunity to define a file name.

  QString local_file_path = QFileDialog::getSaveFileName(
    this, tr("Save the Isotopic data to file"), QDir::homePath());

  if(local_file_path.isEmpty())
    {
      message("The file name is empty.", 5000);
      return false;
    }

  bool res = isotopic_data_handler.writeData(local_file_path);

  qDebug() << "The new file name:" << isotopic_data_handler.getFileName();

  message(QString("Saving of the data %1.").arg(res ? "succeeded" : "failed"));

  // At this point, if the saving was successful, store the filename in the
  // polymer chemistry definition.

  if(res)
    msp_polChemDef->setIsotopicDataFilePath(local_file_path);

  return res;
}


void
IsotopeDefDlg::message(const QString &message, int timeout)
{
  m_ui.messageLineEdit->setText(message);

  QTimer::singleShot(timeout, [this]() { m_ui.messageLineEdit->setText(""); });
}


bool
IsotopeDefDlg::setupIsotopicDataTableView()
{

  // If we opened this dialog without having loaded any specific polymer
  // chemistry definition, then msp_polChemDef->getIsotopicDataSPtr() ==
  // nullptr. In that case, what we should do is provide the user with a
  // convenience copy of the IsoSpec tables isotopic data which the user will be
  // able to modify as they care.

  if(msp_polChemDef->getIsotopicDataSPtr() == nullptr ||
     msp_polChemDef->getIsotopicDataSPtr().get() == nullptr)
    {

      msp_polChemDef->setIsotopicDataSPtr(
        std::make_shared<libXpertMassCore::IsotopicData>());

      // Silently load the IsoSpec's isotopic data tables.  For this, we can can
      // allocate the proper handler but first we need to allocate the isotopic
      // data.

      libXpertMassCore::IsotopicDataLibraryHandler isotopic_data_handler(
        msp_polChemDef->getIsotopicDataSPtr());

      if(!isotopic_data_handler.loadData())
        return false;
    }

  // Now we can link the data to the table view model.

  mpa_isotopicDataTableViewModel =
    new libXpertMassGui::IsotopicDataTableViewModel(
      this, msp_polChemDef->getIsotopicDataSPtr());

  m_ui.isotopicDataTableView->setModel(mpa_isotopicDataTableViewModel);

  m_ui.isotopicDataTableView->setIsotopicData(
    msp_polChemDef->getIsotopicDataSPtr());

  m_ui.isotopicDataTableView->setParent(this);
  mpa_isotopicDataTableViewModel->setTableView(m_ui.isotopicDataTableView);

  connect(m_ui.isotopicDataTableView,
          &libXpertMassGui::IsotopicDataTableView::selectionChangedSignal,
          this,
          &IsotopeDefDlg::selectionChanged);

  connect(m_ui.isotopicDataTableView,
          &libXpertMassGui::IsotopicDataTableView::allSelectedIndicesSignal,
          this,
          &IsotopeDefDlg::allSelectedIndices);

  connect(m_ui.isotopicDataTableView,
          &libXpertMassGui::IsotopicDataTableView::selectedRowsChangedSignal,
          this,
          &IsotopeDefDlg::selectedRowsChanged);


  return true;
}


void
IsotopeDefDlg::selectionChanged(
  const QItemSelection &selected,
  [[maybe_unused]] const QItemSelection &deselected)
{
  // The selected param provides a list of items being selected. In fact, this
  // is most a single item list because it provides a list of the table view
  // indices that have changed since last signal.
  QModelIndexList selected_indices = selected.indexes();
}


void
IsotopeDefDlg::allSelectedIndices(
  [[maybe_unused]] QModelIndexList selected_indices)
{
  // The selected_indices param provides a list of the effectively committed
  // selected item (not those that are ongoing).
  //
  // Attention the selected indices might belong to the same row! In this case
  // this might not be interesting for given applications. For example, if we
  // want to compute the average mass corresponding to different isotopes, we
  // would need to have a selection of multiple rows, not multiple cells
  // (indices) in the same row!

  // int list_size = selected_indices.size();
  // qDebug() << "Number of selected items:" << list_size;

  // I think this is a bug, but the selection does update when selecting some
  // other cells. The only way to make that update happen is to use
  // layoutChanged().
  mpa_isotopicDataTableViewModel->layoutChanged();
}


void
IsotopeDefDlg::selectedRowsChanged(std::set<int> rows_set)
{
  // This slot is connected to the selectedRowsChangedSignal() that originates
  // in the table view as soon as the selection changes. We craft there the list
  // of unique row indices that host selected cells.

  qDebug() << "The number of selected rows:" << rows_set.size();

  if(rows_set.size())
    updateMassAndProbDetails(rows_set);
}


void
IsotopeDefDlg::updateMassAndProbDetails(std::set<int> rows_set)
{
  // This function gets called as soon a the selected rows have changed.

  // We want to update some details about the selected isotopes only if they
  // match the set of isotopes of a single element (no way that we calculate the
  // average mass of isotopes of different chemical elements, for example!).

  // First make sure the isotopes in the set of selected rows is always the
  // same.
  std::size_t selected_rows_count = rows_set.size();
  qDebug() << "The number of selected rows:" << selected_rows_count;

  // Start fresh on the line edit widgets.

  m_ui.avgMassLineEdit->clear();
  m_ui.probabilityLineEdit->clear();

  // Get to know which symbol(s) are found in the selected rows.
  std::set<QString> symbol_set;

  for(auto row_index : rows_set)
    {
      // Row index of the currently iterated modelindex that matches the index
      // of the isotope in the msp_isotopicData->m_isotopes.

      // qDebug() << "Currently iterated row index:" << row_index
      //<< "With symbol: "
      //<< msp_polChemDef->getIsotopicDataSPtr()
      //->getIsotopes()
      //.at(row_index)
      //->getSymbol();

      symbol_set.insert(msp_polChemDef->getIsotopicDataSPtr()
                          ->getIsotopes()
                          .at(row_index)
                          ->getSymbol());
    }

  // At this point, we should have a single item in the set.

  if(symbol_set.size() > 1)
    {
      message(
        "The selection encompasses more than one symbol. Not updating the "
        "mass/prob details.");

      return;
    }

  // We cannot proceed if there are not symbols in the set!
  if(!symbol_set.size())
    return;

  // At this point we know that all the selected rows hold isotopes by the very
  // same symbol which is needed to perform the computations at hand. We do not
  // want to compute the values on *all* the isotopes of the symbol because here
  // we want to provide feedback to the user only on the currently selected
  // isotopes.

  // Compute the iterator range for the selected rows.
  const QList<libXpertMassCore::IsotopeQSPtr> &reference_isotopes =
    msp_polChemDef->getIsotopicDataSPtr()->getIsotopes();

  //  The rows_set argument is a set of integers describing the rows being
  //  selected as their index.

  libXpertMassCore::IsotopeListCstIterator the_begin_iterator =
    reference_isotopes.cbegin() + *(rows_set.cbegin());

  libXpertMassCore::IsotopeListCstIterator the_end_iterator =
    the_begin_iterator + rows_set.size();

  qDebug() << "Distance: "
           << std::distance(the_begin_iterator, the_end_iterator);

  libXpertMassCore::IsotopeListCstIteratorPair iter_pair(the_begin_iterator,
                                                     the_end_iterator);

  // Now actually ask the details.
  libXpertMassCore::ErrorList error_list;

  double cumulated_probabilities =
    msp_polChemDef->getIsotopicDataSPtr()->getCumulatedProbabilities(
      iter_pair, &error_list);
  if(error_list.size())
    {
      QString joined_errors;

      for(auto error : error_list)
        joined_errors.append(error + "\n");

      QMessageBox::warning(
        this,
        QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription),
        joined_errors,
        QMessageBox::Ok);

      return;
    }

  m_ui.probabilityLineEdit->setText(
    QString::number(cumulated_probabilities, 'f', 6));

  double mono_mass = msp_polChemDef->getIsotopicDataSPtr()->getMonoMass(
    libXpertMassCore::IsotopeListCstIteratorPair(the_begin_iterator,
                                             the_end_iterator),
    &error_list);

  if(error_list.size())
    {
      QString joined_errors;

      for(auto error : error_list)
        joined_errors.append(error + "\n");

      QMessageBox::warning(this,
                           QString("%1 "
                                   "- "
                                   "%2")
                             .arg(m_applicationName)
                             .arg(m_windowDescription),
                           joined_errors,
                           QMessageBox::Ok);

      return;
    }

  m_ui.monoMassLineEdit->setText(QString::number(mono_mass, 'f', 12));

  double avg_mass = msp_polChemDef->getIsotopicDataSPtr()->computeAvgMass(
    libXpertMassCore::IsotopeListCstIteratorPair(the_begin_iterator,
                                             the_end_iterator),
    &error_list);

  if(error_list.size())
    {
      QString joined_errors;

      for(auto error : error_list)
        joined_errors.append(error + "\n");

      QMessageBox::warning(this,
                           QString("%1 "
                                   "- "
                                   "%2")
                             .arg(m_applicationName)
                             .arg(m_windowDescription),
                           joined_errors,
                           QMessageBox::Ok);

      return;
    }

  m_ui.avgMassLineEdit->setText(QString::number(avg_mass, 'f', 12));
}

} // namespace MassXpert

} // namespace MsXpS
