/* BEGIN software license
 *
 * MsXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright (C) 2009--2020 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
 */


/////////////////////// Std lib includes
#include <memory>


/////////////////////// Qt includes


/////////////////////// Local includes
#include "MsXpS/libXpertMassCore/Isotope.hpp"

#include "MsXpS/libXpertMassGui/IsotopicDataTableViewModel.hpp"
#include "MsXpS/libXpertMassGui/IsotopicClusterGeneratorDlg.hpp"

namespace MsXpS
{

namespace libXpertMassGui
{


/*!
\class MsXpS::libXpertMassGui::IsotopicDataTableViewModel
\inmodule libXpertMassGui
\ingroup XpertMassGuiMassCalculations
\inheaderfile IsotopicDataTableViewModel.hpp

\brief The IsotopicDataTableViewModel class provides a table view widget to
display isotopic data.
*/

/*!
\variable MsXpS::libXpertMassGui::IsotopicDataTableViewModel::mp_parent

\brief The parent widget.
*/

/*!
\variable MsXpS::libXpertMassGui::IsotopicDataTableViewModel::msp_isotopicData

\brief The isotopic data.
*/

/*!
\variable MsXpS::libXpertMassGui::IsotopicDataTableViewModel::mp_tableView

\brief The table view widget.
*/


/*!
\brief Constructs a IsotopicDataTableViewModel instance.

\list
\li \a parent_p: the parent widget.
\li \a isotopic_data_sp: the isotopic data.
\endlist
*/
IsotopicDataTableViewModel::IsotopicDataTableViewModel(
  QObject *parent_p, libXpertMassCore::IsotopicDataSPtr isotopic_data_sp)
  : QAbstractTableModel(parent_p), msp_isotopicData(isotopic_data_sp)
{
  if(!parent_p)
    qFatal("Programming error.");

  mp_parent = dynamic_cast<QWidget *>(parent_p);

  connect(this,
          &IsotopicDataTableViewModel::editCompletedSignal,
          this,
          &IsotopicDataTableViewModel::editCompleted);
}

/*!
\brief Destructs this IsotopicDataTableViewModel instance.
*/
IsotopicDataTableViewModel::~IsotopicDataTableViewModel()
{
}

/*!
\brief Returns the parent widget.
*/
const QWidget *
IsotopicDataTableViewModel::parent() const
{
  return mp_parent;
}

/*!
\brief Sets the table view widget \a table_view_p.
*/
void
IsotopicDataTableViewModel::setTableView(IsotopicDataTableView *table_view_p)
{
  if(!table_view_p)
    qFatal("Programming error.");

  mp_tableView = table_view_p;
}

/*!
\brief Returns the table view widget.
*/
IsotopicDataTableView *
IsotopicDataTableViewModel::tableView()
{
  return mp_tableView;
}

/*!
\brief Returns the count of rows in this table view.

\a parent is unused.
*/
int
IsotopicDataTableViewModel::rowCount(
  [[maybe_unused]] const QModelIndex &parent) const
{
  return msp_isotopicData->size();
}

/*!
\brief Returns the count of columns in this table view.

\a parent is unused.
*/
int
IsotopicDataTableViewModel::columnCount(
  [[maybe_unused]] const QModelIndex &parent) const
{
  return static_cast<int>(libXpertMassCore::IsotopeFields::LAST);
}

/*!
\brief Returns true if the row could be inserted at reference model index \a
parent.

The insertion occurs at the index right before \a row such that the newly
inserted row will be effectively at \a row index.

The cells of the inserted row will be set with default values.
*/
bool
IsotopicDataTableViewModel::insertRow(int row, const QModelIndex &parent)
{
  // The insertion occurs at index right *before* the row parameter.

  int first = row;
  int last  = first;

  qDebug() << "Preparing to insert before index:" << row;

  beginInsertRows(parent, first, last);

  libXpertMassCore::IsotopeQSPtr isotope_qsp =
    QSharedPointer<libXpertMassCore::Isotope>::create(
      "NOT_SET", "NOT_SET", 0.000000000, 1.00000000);

  // false: do not update the mass maps as the newly inserted isotope is fake.
  // insert above row in the isotope vector.
  bool res = msp_isotopicData->insertNewIsotope(isotope_qsp, row, false);

  endInsertRows();

  // Because we want to edit the newly inserted row, select it.
  mp_tableView->selectRow(row);

  emit modifiedSignal();

  return res;
}

/*!
\brief Returns the header data for \a section.

If \a orientation is Qt::Vertical returns the row number; if \a orientation is
Qt::Horizontal, returns the text in the header. If \a role is not
Qt::DisplayRole, returns QVariant().
*/
QVariant
IsotopicDataTableViewModel::headerData(int section,
                                       Qt::Orientation orientation,
                                       int role) const
{
  if(role != Qt::DisplayRole)
    return QVariant();

  if(orientation == Qt::Vertical)
    {
      // Return the row number.
      QString valueString;
      valueString.setNum(section);
    }
  else if(orientation == Qt::Horizontal)
    {
      // Return the header of the column.
      switch(section)
        {
          case static_cast<int>(libXpertMassCore::IsotopeFields::NAME):
            return "Element";

          case static_cast<int>(libXpertMassCore::IsotopeFields::SYMBOL):
            return "Symbol";

          case static_cast<int>(libXpertMassCore::IsotopeFields::MASS):
            return "Mass";

          case static_cast<int>(libXpertMassCore::IsotopeFields::PROBABILITY):
            return "Prob.";

          default:
            qFatal("Programming error.");
        }
    }

  // Should never get here.
  return QVariant();
}

/*!
\brief Returns the data for cell at \a index, for \a role.

If \a role is Qt::TextAlignmentRole, returns (Qt::AlignRight |
Qt::AlignVCenter).

If \a role is Qt::DisplayRole, returns the text in the cell described in \a
index in the form of a QVariant.
*/
QVariant
IsotopicDataTableViewModel::data(const QModelIndex &index, int role) const
{
  if(!index.isValid())
    return QVariant();

  if(role == Qt::TextAlignmentRole)
    {
      return int(Qt::AlignRight | Qt::AlignVCenter);
    }
  else if(role == Qt::DisplayRole)
    {
      int row    = index.row();
      int column = index.column();

      // Let's get the data for the right column and the right
      // row. Let's find the row first, so that we get to the proper
      // entity.

      libXpertMassCore::IsotopeQSPtr isotope_qsp =
        msp_isotopicData->getIsotopes().at(row);

      // Now see what's the column that is asked for. Prepare a
      // string that we'll feed with the right data before returning
      // it as a QVariant.

      QString value_string;

      if(column == static_cast<int>(libXpertMassCore::IsotopeFields::NAME))
        {
          value_string = isotope_qsp->getName();
        }
      else if(column ==
              static_cast<int>(libXpertMassCore::IsotopeFields::SYMBOL))
        {
          value_string = isotope_qsp->getSymbol();
        }
      else if(column == static_cast<int>(libXpertMassCore::IsotopeFields::MASS))
        {
          value_string = QString("%1").arg(isotope_qsp->getMass(), 0, 'f', 12);
        }
      else if(column ==
              static_cast<int>(libXpertMassCore::IsotopeFields::PROBABILITY))
        {
          value_string =
            QString("%1").arg(isotope_qsp->getProbability(), 0, 'f', 60);
        }
      else
        {
          qFatal("Programming error.");
        }

      return value_string;
    }
  // End of
  // else if(role == Qt::DisplayRole)

  return QVariant();
}

/*!
\brief Set the data to \a value for the cell described by \a index.

If \a role is not Qt::EditRole, returns false. Returns true if \a value could be
converted to the right type depending on the column of the cell, true otherwise.

The data in the cell described by \a index is read, converted to the right type
and set to the matching isotope instance. The row datum in \a index points to
the index of the isotope in the isotopica data.

If conversion from QVariant to the proper data type fails, returns false,
otherwise returns true.
*/
bool
IsotopicDataTableViewModel::setData(const QModelIndex &index,
                                    const QVariant &value,
                                    int role)
{
  if(role != Qt::EditRole)
    return false;

  if(!checkIndex(index))
    return false;

  // We want to access the isotope in the isotopic data. This is done by using
  // the index, that is, the row.

  int isotopic_data_index = index.row();
  int column              = index.column();

  // Store the original isotope pointer!
  libXpertMassCore::IsotopeQSPtr old_isotope_sp =
    msp_isotopicData->getIsotopes().at(isotopic_data_index);

  // Prepare a copy that we'll modify the proper field of.
  libXpertMassCore::IsotopeQSPtr new_isotope_sp =
    QSharedPointer<libXpertMassCore::Isotope>::create(*old_isotope_sp);

  // Now determine what is the column of the cell (that is, the field) that was
  // modified and modify the new_isotope_sp accordingly.

  bool ok = false;

  if(column == static_cast<int>(libXpertMassCore::IsotopeFields::NAME))
    {
      new_isotope_sp->setName(value.toString());
    }
  else if(column == static_cast<int>(libXpertMassCore::IsotopeFields::SYMBOL))
    {
      new_isotope_sp->setSymbol(value.toString());
    }
  else if(column == static_cast<int>(libXpertMassCore::IsotopeFields::MASS))
    {
      double new_value = value.toDouble(&ok);
      if(!ok)
        return false;

      new_isotope_sp->setMass(new_value);
    }
  else if(column ==
          static_cast<int>(libXpertMassCore::IsotopeFields::PROBABILITY))
    {
      double new_value = value.toDouble(&ok);
      if(!ok)
        return false;

      new_isotope_sp->setProbability(new_value);
    }
  else
    {
      qFatal("Programming error.");
    }

  // At this point we know the new isotope has fine data in it, just replace the
  // old with the new.

  msp_isotopicData->replace(old_isotope_sp, new_isotope_sp);

  // emit editCompleted(new_isotope_sp->toString());

  // Not sure if this is really necessary.
  emit(dataChanged(index, index));

  emit modifiedSignal();

  return true;
}

void
IsotopicDataTableViewModel::refreshAfterNewData()
{
  // This is bad code to ensure that the table view items are refreshed
  // throughout all the data set.
  QModelIndex parent;

  beginInsertRows(parent, 0, rowCount() - 1);

  endInsertRows();

  emit modifiedSignal();
}

/*!
\brief Removes all the rows from the table view.

Returns true if rows were effectively removed, false otherwise.
*/
bool
IsotopicDataTableViewModel::clearAllData()
{
  // How many rows do we have?

  int row_count = rowCount();

  if(!row_count)
    // We did not remove any Isotope from the IsotopicData
    return false;

  // The begin_index and end_index are *inclusive* in the beginRemoveRows()
  // call below.
  beginRemoveRows(QModelIndex(), 0, row_count - 1);

  // true: update the maps because we need to clear all the items there.
  msp_isotopicData->eraseIsotopes(0, row_count - 1, true);

  endRemoveRows();

  emit modifiedSignal();

  // We did remove items.
  return true;
}

/*!
\brief Returns the flags for the \a index.

The returned flags include Qt::ItemIsEditable.
*/
Qt::ItemFlags
IsotopicDataTableViewModel::flags(const QModelIndex &index) const
{
  return Qt::ItemIsEditable | QAbstractTableModel::flags(index);
}

/*!
\brief Signals that the editing of a cell is completed with final \a text.
*/
void
IsotopicDataTableViewModel::editCompleted(const QString &text)
{
  qDebug() << "After editing the new isotope:" << text;

  emit modifiedSignal();
}

/*!
\brief Inserts a new row above the current index.
*/
void
IsotopicDataTableViewModel::insertIsotopeRowAbove()
{
  // We need to maintain a perfect correlation between the data in the
  // IsotopicData object and the rows in the table view.

  int insertion_row_index = 0;

  // We want to add a row above the current index. Note that all the functions
  // dealing with models and containers insert above the index provided to the
  // insertion function.

  int row_count = rowCount();

  if(!row_count)
    {
      // qDebug() << "There are no isotopes.";

      // We do not care, because we cannot, if there is a current index.
    }
  else
    {
      // qDebug() << "There are currently" << row_count << "isotopes in the
      // table";

      // In this case, we want to know if there is a current index.

      QItemSelectionModel *selection_model = mp_tableView->selectionModel();

      QModelIndex current_index = selection_model->currentIndex();

      if(!current_index.isValid())
        {
          return;
        }

      insertion_row_index = current_index.row();

      // qDebug() << "The current index row is at index" << insertion_row_index;
    }

  // insertRow() inserts a single row before the given row in the child items of
  // the parent specified. This means that we just use that row index without
  // modifying it.

  insertRow(insertion_row_index);

  emit modifiedSignal();
}

/*!
\brief Inserts a new row below the current index.
*/
void
IsotopicDataTableViewModel::insertIsotopeRowBelow()
{
  // We need to maintain a perfect correlation between the data in the
  // IsotopicData object and the rows in the table view.

  int insertion_row_index = 0;

  // We want to add a row below the current index, but beware that all the
  // functions dealing with models and containers insert *above* the index
  // provided to the insertion function.

  int row_count = rowCount();

  if(!row_count)
    {
      // qDebug() << "There are no isotopes.";

      // We do not care, because we cannot, if there is a current index.
    }
  else
    {
      // qDebug() << "There are currently" << row_count << "isotopes in the
      // table";

      // In this case, we want to know if there is a current index.

      QItemSelectionModel *selection_model = mp_tableView->selectionModel();

      QModelIndex current_index = selection_model->currentIndex();

      if(!current_index.isValid())
        {
          return;
        }

      insertion_row_index = current_index.row();

      // qDebug() << "The current index row is at index" << insertion_row_index;
    }

  // insertRow() inserts a single row before the given row in the child items of
  // the parent specified. This means that we'll have to increment the
  // insertion_row_index by 1.

  ++insertion_row_index;

  insertRow(insertion_row_index);

  emit modifiedSignal();
}

/*!
\brief Removes all the selected rows.
*/
void
IsotopicDataTableViewModel::removeSelected()
{
  // We need to maintain a perfect correlation between the data in the
  // IsotopicData object and the rows in the table view.

  // int row_count = rowCount();
  // qDebug() << "There are currently" << row_count
  //<< "isotopes in the table view model.";

  QItemSelectionModel *selection_model = mp_tableView->selectionModel();

  QModelIndexList selected_indices = selection_model->selectedIndexes();

  // qDebug() << "The number of selected indices:" << selected_indices.size();

  // We know that the indices list might contain multiple indices for the same
  // row. We need to extract a unique list of rows for which at least one index
  // is selected. Then, we'll use that list of rows to perform the removal.

  std::set<int> selected_rows;

  for(int iter = 0; iter < selected_indices.size(); ++iter)
    {
      selected_rows.insert(selected_indices.at(iter).row());
    }

  if(!selected_rows.size())
    {
      // qDebug() << "There are not selected rows in the table view.";
      return;
    }

  int begin_index = *selected_rows.begin();

  // The end index is at position end() - 1 of the std::set.
  int end_index = *std::prev(selected_rows.end());

  // qDebug() << "The rows to be removed are in the fully inclusive index range:
  // ["
  //<< begin_index << "-" << end_index << "].";

  // The begin_index and end_index are *inclusive* in the beginRemoveRows()
  // call below.
  beginRemoveRows(QModelIndex(), begin_index, end_index);

  // true: update the maps because the remaining isotopes are valid.
  msp_isotopicData->eraseIsotopes(begin_index, end_index, true);

  endRemoveRows();

  emit modifiedSignal();

  // Now select the item that was right below the last selected removed item.
  if(!rowCount())
    {
      // Do nothing.
    }
  else
    {
      if(begin_index >= rowCount())
        {
          begin_index = rowCount() - 1;
        }
    }

  mp_tableView->selectRow(begin_index);
}


} // namespace libXpertMassGui

} // namespace MsXpS
