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


/////////////////////// Local includes
#include <massxpert/gui/FragmentationDlg.hpp>


namespace msXpSmassXpert
{

FragmentationDlg::FragmentationDlg(SequenceEditorWnd *editorWnd,
                                   Polymer *polymer,
                                   const PolChemDef *polChemDef,
                                   const QString &configSettingsFilePath,
                                   const CalcOptions &calcOptions,
                                   const msXpSlibmass::IonizeRule *ionizeRule)
  : AbstractSeqEdWndDependentDlg(editorWnd,
                                 polymer,
                                 const_cast<PolChemDef *>(polChemDef),
                                 configSettingsFilePath,
                                 "CompositionsDlg",
                                 "Composition calculations"),
    m_calcOptions{calcOptions},
    mp_ionizeRule{ionizeRule}
{
  if(!mp_polymer || !mp_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__);
}


FragmentationDlg::~FragmentationDlg()
{
  // Delete the oligomers in the list of such instances.

  while(!m_oligomerList.isEmpty())
    delete m_oligomerList.takeFirst();

  delete mpa_resultsString;

  delete mpa_oligomerTableViewModel;
  delete mpa_proxyModel;

  delete mpa_fragmenter;
}


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

  readSettings();

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

  populateSelectedOligomerData();
  populateFragSpecList();

  m_ui.fragmentationPatternListWidget->setSelectionMode(
    QAbstractItemView::MultiSelection);

  mpa_oligomerTableViewModel = 0;
  mpa_proxyModel             = 0;

  // 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_fragmenter             = 0;
  mpa_proxyModel             = 0;
  mpa_oligomerTableViewModel = 0;

  setupTableView();

  // 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_F));
  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.filterPatternLineEdit;


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

  QStringList comboBoxItemList;

  comboBoxItemList << tr("To Clipboard") << tr("To File") << tr("Select File");

  m_ui.exportResultsComboBox->addItems(comboBoxItemList);

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

  mpa_resultsString = new QString();

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

  connect(m_ui.fragmentPushButton, SIGNAL(clicked()), this, SLOT(fragment()));

  connect(m_ui.filterPatternLineEdit,
          SIGNAL(returnPressed()),
          this,
          SLOT(filterPattern()));

  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
FragmentationDlg::populateSelectedOligomerData()
{
  CoordinateList coordList;

  bool res = mp_editorWnd->mpa_editorGraphicsView->selectionIndices(&coordList);

  if(!res)
    {
      QMessageBox::information(this,
                               tr("massxpert - Fragmentation"),
                               tr("No oligomer is selected. "
                                  "Select an oligomer first"),
                               QMessageBox::Ok);
      return false;
    }

  if(coordList.size() > 1)
    {
      QMessageBox::information(this,
                               tr("massxpert - Fragmentation"),
                               tr("Fragmentation simulations with\n"
                                  "multi-region selection are not "
                                  "supported."),
                               QMessageBox::Ok);
      return false;
    }

  m_calcOptions.setCoordinateList(coordList);

  Coordinates *coordinates = coordList.last();

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

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

  return true;
}


void
FragmentationDlg::populateFragSpecList()
{
  for(int iter = 0; iter < mp_polChemDef->fragSpecList().size(); ++iter)
    {
      FragSpec *fragSpec = mp_polChemDef->fragSpecList().at(iter);
      Q_ASSERT(fragSpec);

      m_ui.fragmentationPatternListWidget->addItem(fragSpec->name());
    }

  return;
}


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

  mpa_proxyModel = new FragmentOligomerTableViewSortProxyModel(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);
}


void
FragmentationDlg::fragment()
{
  // Update the options from the parent window.
  m_calcOptions = mp_editorWnd->calcOptions();

  // And now override the selection data by using the
  // selection-specific function below.
  if(!populateSelectedOligomerData())
    return;

  // For each fragmentation specification selected in the list of
  // available fragmentation specifications we have to construct a
  // FragOptions instance and add it to the list of such frag
  // options. If no item is currently selected, just return.

  // What are the currently selected specifications ?
  QList<QListWidgetItem *> selectionList =
    m_ui.fragmentationPatternListWidget->selectedItems();

  if(selectionList.isEmpty())
    return;

  // We'll need to use the specifications in the polymer chemistry
  // definition... for each frag options object.
  QList<FragSpec *> refList = mp_polChemDef->fragSpecList();

  // We'll have to store all the fragOptions in a list:
  QList<FragOptions *> fragOptionList;

  // We'll need a fragmentation specification each time we iterate in
  // a new specification item below.
  FragSpec fragSpec(mp_polChemDef, "NOT_SET");

  // Finally parse all the different fragmentation specification items
  // currently selected in the list widget.

  for(int iter = 0; iter < selectionList.size(); ++iter)
    {
      QListWidgetItem *item = selectionList.at(iter);

      if(item->isSelected())
        {
          // Need to create a new fragOptions with the correct
          // fragmentation specification. First get its name from the
          // list widget. Then use that name to find the FragSpec
          // from the list of frag specs in the polymer chemistry
          // definition. Finally allocate a new frag options object
          // and set data into it.

          QString name = item->text();

          int res = FragSpec::isNameInList(name, refList, &fragSpec);

          if(res == -1)
            {
              return;
            }

          FragOptions *fragOptions = new FragOptions(fragSpec);

          // The user might ask that some formulas be applied while
          // making the fragmentation with the selection
          // fragmentation option. Lets feed the FragOptions
          // instance with the related formulas.

          if(m_ui.decompositionsMinusWaterCheckBox->checkState() == Qt::Checked)
            {
              fragOptions->addFormula(QString("-H2O"));
            }

          if(m_ui.decompositionsMinusAmmoniaCheckBox->checkState() ==
             Qt::Checked)
            {
              fragOptions->addFormula(QString("-NH3"));
            }

          if(!m_ui.decompositionsFormulaLineEdit->text().isEmpty())
            {
              msXpSlibmass::Formula formula(
                m_ui.decompositionsFormulaLineEdit->text());

              // At this point, we should check if the formula is valid.
              const QList<msXpSlibmass::Atom *> &atomRefList =
                mp_polChemDef->atomList();

              if(!formula.validate(atomRefList))
                {
                  QMessageBox::information(
                    0,
                    tr("massXpert - Fragmentation"),
                    tr("Failed to validate the decomposition formula."),
                    QMessageBox::Ok);

                  return;
                }

              fragOptions->addFormula(formula);
            }

          // We got the coordinates of the oligomer to fragment in the
          // mp_calcOptions from the sequence editor window.

          Coordinates *coordinates = m_calcOptions.coordinateList().last();

          fragOptions->setStartIndex(coordinates->start());
          fragOptions->setEndIndex(coordinates->end());


          // Set the ionization levels.
          int valueStart = m_ui.ionizeLevelStartSpinBox->value();
          int valueEnd   = m_ui.ionizeLevelEndSpinBox->value();

          if(valueStart > valueEnd)
            {
              fragOptions->setStartIonizeLevel(valueEnd);
              fragOptions->setEndIonizeLevel(valueStart);
            }
          else
            {
              fragOptions->setStartIonizeLevel(valueStart);
              fragOptions->setEndIonizeLevel(valueEnd);
            }


          //   qDebug() << "Start:" << fragOptions.startIonizeLevel();
          //   qDebug() << "End:" << fragOptions.endIonizeLevel();

          // Add the frag options to the list.
          fragOptionList.append(fragOptions);
        }
      else
        continue;
    }

  // At this point the list of frag options should contain the same
  // number of items as the number of items selected in the list
  // widget.

  Q_ASSERT(fragOptionList.size() == selectionList.size());

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

  // Now that we have all the fragmentation options in the list, we
  // can allocate the fragmenter! Note that this function might be
  // called many times, which means that we first have to free the
  // mpa_fragmenter allocated member if non-0.

  delete mpa_fragmenter;
  mpa_fragmenter = 0;

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

  mpa_fragmenter = new Fragmenter(
    mp_polymer, mp_polChemDef, fragOptionList, m_calcOptions, *mp_ionizeRule);

  mpa_fragmenter->setOligomerList(&oligomerList);

  if(!mpa_fragmenter->fragment())
    {
      delete mpa_fragmenter;

      QMessageBox::critical(this,
                            tr("massXpert - Polymer Cleavage"),
                            tr("Failed to perform fragmentation."),
                            QMessageBox::Ok);

      return;
    }

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

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

  // We are *transferring* the oligomers from oligomerList to the
  // list of oligomers that is in the model : we are not duplicating
  // the oligomers. When the transfer has been done, oligomerList
  // will be empty.
  int addedOligomers = mpa_oligomerTableViewModel->addOligomers(oligomerList);

  // 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 oligomerList without freeing the
  // instances it currently contain...
  oligomerList.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 fragmentation details so that we know how the oligos were
  // computed.
  updateFragmentationDetails(m_calcOptions);
}


void
FragmentationDlg::updateFragmentationDetails(const CalcOptions &calcOptions)
{
  if(calcOptions.monomerEntities() & MONOMER_CHEMENT_MODIF)
    m_ui.modificationsCheckBox->setCheckState(Qt::Checked);
  else
    m_ui.modificationsCheckBox->setCheckState(Qt::Unchecked);

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

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

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


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

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


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


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

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


void
FragmentationDlg::filterPattern()
{
  // First off, we have to get the pattern that is in the lineEdit.

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

  if(text.isEmpty())
    return;

  mpa_proxyModel->setPatternFilter(text);
  mpa_proxyModel->setFilterKeyColumn(0);
  mpa_proxyModel->applyNewFilter();

  mp_focusWidget = m_ui.filterPatternLineEdit;
}


void
FragmentationDlg::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
FragmentationDlg::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
FragmentationDlg::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;
}


// The results-exporting functions. ////////////////////////////////
// The results-exporting functions. ////////////////////////////////
// The results-exporting functions. ////////////////////////////////
void
FragmentationDlg::exportResults(int index)
{
  // Remember that we had set up the combobox with the following strings:
  // << tr("To Clipboard")
  // << tr("To File")
  // << tr("Select File");

  if(index == 0)
    {
      exportResultsClipboard();
    }
  else if(index == 1)
    {
      exportResultsFile();
    }
  else if(index == 2)
    {
      selectResultsFile();
    }
  else
    Q_ASSERT(0);
}


void
FragmentationDlg::prepareResultsTxtString()
{
  // 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;
  msXpS::MassType massType = msXpS::MassType::MASS_NONE;

  forXpertMiner = m_ui.forXpertMinerCheckBox->isChecked();

  if(!forXpertMiner)
    {
      *mpa_resultsString += QObject::tr(
        "# \n"
        "# ---------------------------\n"
        "# Fragmentation: \n"
        "# ---------------------------\n");
    }
  else
    {
      // Then, we should ask whether the masses to be exported are
      // the mono or avg masses.

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

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

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

  *mpa_resultsString += *text;

  delete text;
}


bool
FragmentationDlg::exportResultsClipboard()
{
  prepareResultsTxtString();

  QClipboard *clipboard = QApplication::clipboard();

  clipboard->setText(*mpa_resultsString, QClipboard::Clipboard);

  return true;
}


bool
FragmentationDlg::exportResultsFile()
{
  if(m_resultsFilePath.isEmpty())
    {
      if(!selectResultsFile())
        return false;
    }

  QFile file(m_resultsFilePath);

  if(!file.open(QIODevice::WriteOnly | QIODevice::Append))
    {
      QMessageBox::information(0,
                               tr("massXpert - Export Data"),
                               tr("Failed to open file in append mode."),
                               QMessageBox::Ok);
      return false;
    }

  QTextStream stream(&file);
  stream.setCodec("UTF-8");

  prepareResultsTxtString();

  stream << *mpa_resultsString;

  file.close();

  return true;
}


bool
FragmentationDlg::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 msXpSmassXpert
