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


/////////////////////// StdLib includes


/////////////////////// Qt includes
#include <QMessageBox>
#include <QSvgGenerator>
#include <QSpinBox>

/////////////////////// QCustomPlot


/////////////////////// pappsomspp includes
#include <pappsomspp/processing/combiners/tracepluscombiner.h>


/////////////////////// Local includes
#include "BasePlotCompositeWidget.hpp"
#include "ColorSelector.hpp"
#include "MsFragmentationSpecDlg.hpp"
#include "MzIntegrationParamsDlg.hpp"
#include "ProcessingFlowViewerDlg.hpp"
#include "ProgramWindow.hpp"

namespace msxps
{
namespace minexpert
{


BasePlotCompositeWidget::BasePlotCompositeWidget(QWidget *parent,
                                                 const QString &x_axis_label,
                                                 const QString &y_axis_label)
  : mp_parentWnd(static_cast<BasePlotWnd *>(parent)),
    m_axisLabelX(x_axis_label),
    m_axisLabelY(y_axis_label)
{
  // By essence, the parent will be the plot window (derived from
  // BaseTracePlotWnd).

  if(parent == nullptr)
    qFatal("parent cannot be nullptr");

  // This call is no more possible because this function has gone pure virtual.
  // setupWidget();

  // This is a fundamental element: that name is used to iterate automatically
  // in all the composite widgets of the plot window.

  dynamic_cast<QObject *>(this)->setObjectName("plotCompositeWidget");

  // We need to setup the widget with the QCustomPlot widget in it. Also we need
  // to setup the ui so that the widgets are known when we use them below.
  m_ui.setupUi(dynamic_cast<QWidget *>(this));

  // Connect the integrate to Mz pushbutton so that we can show the MS level
  // slider.

  // connect(m_ui.msLevelSpinBox,
  // static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
  connect(m_ui.msLevelSpinBox,
          QOverload<int>::of(&QSpinBox::valueChanged),
          this,
          &BasePlotCompositeWidget::msLevelValueChanged);

  // Construct the context menu that will be associated to the main menu push
  // button.

  createMainMenu();

  // We need to connect the mainMenuPushButton to the clicked slot because that
  // is how we will  create live the contextual menu.
  connect(m_ui.mainMenuPushButton,
          &QPushButton::clicked,
          this,
          &BasePlotCompositeWidget::mainMenuPushButtonClicked);

  // The push pin is always present and should be toggable using a shortcut.
  m_ui.pushPinPushButton->setShortcut(QKeySequence("Ctrl+T, P"));
  m_ui.pushPinPushButton->setToolTip("Toggle push pin status (Ctrl+T,P)");

  m_ui.keepTraceCreateNewPushButton->setToolTip(
    "Overlay new trace onto the other(s) (Ctrl+T, O)");
  m_ui.keepTraceCreateNewPushButton->setShortcut(QKeySequence("Ctrl+T, O"));

  m_ui.eraseTraceCreateNewPushButton->setToolTip(
    "Replace currently selected trace(s) with new one (Ctrl+T, R)");
  m_ui.eraseTraceCreateNewPushButton->setShortcut(QKeySequence("Ctrl+T, R"));

  // The red cross pixmap plot widget closing button. Clicking this button
  // triggers the signal that is trapped by the containing window that then
  // handle the destruction of the widget.

  m_ui.closeCompositePlotWidgetPushButton->setShortcut(
    QKeySequence("Ctrl+P, C"));
  m_ui.closeCompositePlotWidgetPushButton->setToolTip(
    "Close the composite plot widget (Ctrl+P,C)");
  connect(
    m_ui.closeCompositePlotWidgetPushButton, &QPushButton::clicked, [this]() {
      mp_parentWnd->plotCompositeWidgetDestructionRequested(this);
    });

  // Put the focus on the whole plot composite wigdet.
  setFocus();

  // The derived classes will have to call setupWidget() here.
}


BasePlotCompositeWidget::~BasePlotCompositeWidget()
{
  // qDebug();
}


BasePlotWnd *
BasePlotCompositeWidget::getParentWnd()
{
  return mp_parentWnd;
}


BasePlotWnd *
BasePlotCompositeWidget::getParent()
{
  return mp_parentWnd;
}


void
BasePlotCompositeWidget::createMainMenu()
{
  // qDebug() << "ENTER";

  if(mp_mainMenu != nullptr)
    delete mp_mainMenu;

  // Create the menu.
  mp_mainMenu = new QMenu(this);

  // Create the menu push button.
  const QIcon main_menu_icon =
    QIcon(":/images/mobile-phone-like-menu-button.svg");
  m_ui.mainMenuPushButton->setIcon(main_menu_icon);
  m_ui.mainMenuPushButton->setToolTip("Plot widget menu (Ctrl+P, M)");
  m_ui.mainMenuPushButton->setShortcut(QKeySequence("Ctrl+P, M"));

  // Finally set the push button to be the menu...
  m_ui.mainMenuPushButton->setMenu(mp_mainMenu);

  QAction *show_mz_integration_params_dlg_action_p = new QAction(
    "Open m/z &integration params dialog", dynamic_cast<QObject *>(this));
  show_mz_integration_params_dlg_action_p->setStatusTip(
    "Open m/z integration params dialog");
  show_mz_integration_params_dlg_action_p->setShortcut(
    QKeySequence("Ctrl+O, I"));

  connect(show_mz_integration_params_dlg_action_p,
          &QAction::triggered,
          this,
          &BasePlotCompositeWidget::showMzIntegrationParamsDlg);

  mp_mainMenu->addAction(show_mz_integration_params_dlg_action_p);

  QAction *show_ms_fragmentation_spec_dlg_action_p =
    new QAction("Open MS &fragmentation dialog", dynamic_cast<QObject *>(this));
  show_ms_fragmentation_spec_dlg_action_p->setStatusTip(
    "Open MS fragmentation dialog");
  show_ms_fragmentation_spec_dlg_action_p->setShortcut(
    QKeySequence("Ctrl+O, F"));

  connect(show_ms_fragmentation_spec_dlg_action_p,
          &QAction::triggered,
          this,
          &BasePlotCompositeWidget::showMsFragmentationSpecDlg);

  mp_mainMenu->addAction(show_ms_fragmentation_spec_dlg_action_p);

  mp_mainMenu->addSeparator();

  QAction *show_processing_flow_action_p =
    new QAction("Show &processing flow", dynamic_cast<QObject *>(this));
  show_processing_flow_action_p->setStatusTip(
    "Show the processing flow that describes the selected trace");
  show_processing_flow_action_p->setShortcut(QKeySequence("Ctrl+S, P"));

  connect(show_processing_flow_action_p,
          &QAction::triggered,
          this,
          &BasePlotCompositeWidget::showProcessingFlow);

  mp_mainMenu->addAction(show_processing_flow_action_p);

  mp_mainMenu->addSeparator();

  QAction *save_plot_to_clipboard_p =
    new QAction("Save plot &graphics to clipboard", this);
  save_plot_to_clipboard_p->setStatusTip(tr("Save plot to clipboard"));
  save_plot_to_clipboard_p->setShortcut(QKeySequence("Ctrl+G, C"));

  connect(save_plot_to_clipboard_p, &QAction::triggered, [this]() {
    savePlotGraphicsToClipboard();
  });

  mp_mainMenu->addAction(save_plot_to_clipboard_p);

  // qDebug() << "EXIT";
}


QFile *
BasePlotCompositeWidget::getAnalysisFilePtr()
{
  // The analysis file belongs to the prgram window, we need to ask the parent
  // window.

  return mp_parentWnd->getAnalysisFilePtr();
}


AnalysisPreferences *
BasePlotCompositeWidget::getAnalysisPreferences()
{
  return mp_parentWnd->getAnalysisPreferences();
}


void
BasePlotCompositeWidget::mainMenuPushButtonClicked()
{
  // qDebug() << "ENTER";

  //// Create the contextual menu that will show when the button is clicked (see
  //// below).

  createMainMenu();
  m_ui.mainMenuPushButton->showMenu();

  // qDebug() << "EXIT";
}


void
BasePlotCompositeWidget::msFragmentationSpecReady(
  const MsFragmentationSpec &ms_fragmentation_spec)
{
  m_msFragmentationSpec = ms_fragmentation_spec;
}


void
BasePlotCompositeWidget::redrawBackground(bool has_focus)
{
  if(has_focus)
    mp_plotWidget->axisRect()->setBackground(m_focusedBrush);
  else
    mp_plotWidget->axisRect()->setBackground(m_unfocusedBrush);

  mp_plotWidget->replot();
}


bool
BasePlotCompositeWidget::savePlotToPdfFile(const QString &file_name,
                                           bool no_cosmetic_pen,
                                           int width,
                                           int height,
                                           const QString &pdf_creator,
                                           const QString &title) const
{
  return mp_plotWidget->savePdf(file_name,
                                width,
                                height,
                                no_cosmetic_pen ? QCP::epNoCosmetic
                                                : QCP::epAllowCosmetic,
                                pdf_creator,
                                title);
}


bool
BasePlotCompositeWidget::savePlotToSvgFile(const QString &file_name)
{
  QSize plot_widget_size = mp_plotWidget->size();

  QSvgGenerator svggenerator;
  svggenerator.setFileName(file_name);
  QCPPainter qcpPainter;
  qcpPainter.begin(&svggenerator);
  mp_plotWidget->toPainter(
    &qcpPainter, plot_widget_size.width(), plot_widget_size.height());
  qcpPainter.end();

  return true;
}


bool
BasePlotCompositeWidget::savePlotToPngFile(
  const QString &file_name, int width, int height, int scale, int quality) const
{
  return mp_plotWidget->savePng(file_name, width, height, scale, quality);
}


bool
BasePlotCompositeWidget::savePlotToJpgFile(
  const QString &file_name, int width, int height, int scale, int quality) const
{
  return mp_plotWidget->saveJpg(file_name, width, height, scale, quality);
}


bool
BasePlotCompositeWidget::savePlotGraphicsToClipboard()
{
  QSize plot_widget_size = mp_plotWidget->size();

  QSvgGenerator svggenerator;
  svggenerator.setFileName("/tmp/svg-rendering.svg");
  QCPPainter qcpPainter;
  qcpPainter.begin(&svggenerator);
  mp_plotWidget->toPainter(&qcpPainter, 400);
  qcpPainter.end();


  return savePlotGraphicsToClipboard(
    plot_widget_size.width(), plot_widget_size.height(), 1);
}


bool
BasePlotCompositeWidget::savePlotGraphicsToClipboard(int width,
                                                     int height,
                                                     int scale)
{
  QPixmap pixmap = mp_plotWidget->toPixmap(width, height, scale);

  QClipboard *clipboard_p = QApplication::clipboard();
  clipboard_p->setPixmap(pixmap);

  return true;
}


void
BasePlotCompositeWidget::showProcessingFlow()
{
  // qDebug();

  // Check first how many plottables there are in the widget.

  QCPAbstractPlottable *plottable_p = plottableToBeUsedAsIntegrationSource();

  if(plottable_p == nullptr)
    {
      // We should inform the user if there are more than one plottable.

      int plottable_count = mp_plotWidget->plottableCount();

      if(plottable_count > 1)
        QMessageBox::information(this,
                                 "Show processing flow for trace",
                                 "Please select a single trace and try again.");

      return;
    }

  ProcessingFlow processing_flow = getProcessingFlowForPlottable(plottable_p);

  // qDebug() << "The processing flow to display:" <<
  // processing_flow.toString();

  // If this window was never created, then create it now.
  if(mp_processingFlowViewerDlg == nullptr)
    mp_processingFlowViewerDlg =
      new ProcessingFlowViewerDlg(mp_parentWnd, "mineXpert");

  mp_processingFlowViewerDlg->setProcessingFlowText(
    processing_flow.toString(0, " "));

  mp_processingFlowViewerDlg->activateWindow();
  mp_processingFlowViewerDlg->raise();
  mp_processingFlowViewerDlg->show();
}


QString
BasePlotCompositeWidget::getAnalysisStanza()
{
  return m_analysisStanza;
}


void
BasePlotCompositeWidget::lastCursorHoveredPoint(const QPointF &pointf)
{

  // qDebug() << "pointf:" << pointf;

  // Display the position point in the status text.

  m_ui.statusLineEdit->setText(
    QString("(%1,%2)").arg(pointf.x(), 0, 'f', 3).arg(pointf.y(), 0, 'f', 3));
  // qDebug() << "point: (" << pointf.x() << "," << pointf.y() << ")";
}


void
BasePlotCompositeWidget::plotRangesChanged(
  const pappso::BasePlotContext &context)

{
  // This composite plot widget receives signals from the
  // pappso::BasePlotWidget. This signal tells that the range(s) has(ve)
  // changed in the plot widget, that is the x-axis and/or the y-axis range(s)
  // has(ve) changed. Since we want to provide the ability to lock the X/Y
  // ranges for all the plot widgets in a given parent window (that is useful
  // for comparing plots), we need to be able to tell the parent window that
  // the plot ranges of a given widget have changed. So we relay here the
  // signal.

  emit plotRangesChangedSignal(context);
}


void
BasePlotCompositeWidget::xAxisMeasurement(
  const pappso::BasePlotContext &context, bool with_delta)
{
  // A measurement was performed, inform the user.

  // Note that we want to provide the [x -- x] x axis range in sorted order,
  // but the data in the context are not sorted. So craft a QCPRange object
  // that will automagically sort the values. Then use it to craft the
  // message.

  QCPRange sorted_range(context.xRegionRangeStart, context.xRegionRangeEnd);

  QString message = QString("[%1--%2]")
                      .arg(sorted_range.lower, 0, 'f', 3)
                      .arg(sorted_range.upper, 0, 'f', 3);

  if(with_delta)
    message.append(QString(" - delta: %1").arg(context.xDelta, 0, 'f', 3));

  // qDebug() << message;

  m_ui.statusLineEdit->setText(message);
}


void
BasePlotCompositeWidget::plotWidgetKeyPressEvent(
  const pappso::BasePlotContext &context)
{
  // qDebug() << "The context" << context.toString();

  m_plotWidgetPressedKeyCode = context.pressedKeyCode;

  if(context.pressedKeyCode == Qt::Key_Space)
    {
      // qDebug();
      craftAnalysisStanza(context);
    }
}


void
BasePlotCompositeWidget::plotWidgetKeyReleaseEvent(
  const pappso::BasePlotContext &context)
{
  m_plotWidgetReleasedKeyCode = context.releasedKeyCode;
  // qDebug() << "Released key:" << m_plotWidgetReleasedKeyCode;
}


void
BasePlotCompositeWidget::plotWidgetMouseReleaseEvent(
  [[maybe_unused]] const pappso::BasePlotContext &context)
{
  // qDebug();

  // if(context.lastPressedMouseButtonsUponRelease == Qt::NoButton)
  // qDebug() << "no button";

  // if(context.lastPressedMouseButtonsUponRelease == Qt::LeftButton)
  // qDebug() << "left button";

  // if(context.lastPressedMouseButtonsUponRelease == Qt::RightButton)
  //{
  // qDebug() << "right button with context:" << context.toString();
  //}

  // Now check what keyboard key was being pressed while the mouse
  // operation was going on.

  // int pressed_key_code = context.pressedKeyCode;
  // qDebug() << "pressed_key_code:" << pressed_key_code;
}


void
BasePlotCompositeWidget::plottableSelectionChanged(
  [[maybe_unused]] QCPAbstractPlottable *plottable_p,
  [[maybe_unused]] bool is_selected)
{
  // qDebug() << "graph selection has changed: is it now selected?" <<
  // is_selected;
}


void
BasePlotCompositeWidget::pinDown(bool push_pinned)
{
  m_ui.pushPinPushButton->setChecked(push_pinned);
}


bool
BasePlotCompositeWidget::isPinnedDown() const
{
  if(m_ui.pushPinPushButton->isChecked())
    return true;

  return false;
}


const pappso::BasePlotWidget *
BasePlotCompositeWidget::getPlotWidget() const
{
  return mp_plotWidget;
}


ProcessingFlow
BasePlotCompositeWidget::getProcessingFlowForPlottable(
  const QCPAbstractPlottable *plottable_p) const
{
  if(plottable_p == nullptr)
    qFatal("Cannot be that the pointer is nullptr.");

  // Each graph has its own ProcessingFlow, and the association is in the
  // plottableProcessingFlowMap.

  ProcessingFlow processing_flow;

  auto result = std::find_if(
    m_plottableProcessingFlowMap.begin(),
    m_plottableProcessingFlowMap.end(),
    [plottable_p](
      const std::pair<QCPAbstractPlottable *, ProcessingFlow> &item) {
      return item.first == plottable_p;
    });

  if(result != m_plottableProcessingFlowMap.end())
    {
      // qDebug().noquote() << "Found the processing flow for graph:"
      //<< result->second.toString();

      return result->second;
    }
  else
    qFatal("Cannot be that graph has no processing flow associated to it.");

  return ProcessingFlow();
}


ProcessingFlow *
BasePlotCompositeWidget::getProcessingFlowPtrForPlottable(
  const QCPAbstractPlottable *plottable_p)
{
  if(plottable_p == nullptr)
    qFatal("Cannot be that the pointer is nullptr.");

  // Each graph has its own ProcessingFlow, and the association is in the
  // plottableProcessingFlowMap.

  auto result = std::find_if(
    m_plottableProcessingFlowMap.begin(),
    m_plottableProcessingFlowMap.end(),
    [plottable_p](
      const std::pair<QCPAbstractPlottable *, ProcessingFlow> &item) {
      return item.first == plottable_p;
    });

  if(result != m_plottableProcessingFlowMap.end())
    return const_cast<ProcessingFlow *>(&(result->second));

  return nullptr;
}


void
BasePlotCompositeWidget::removePlottableProcessingFlowMapItem(
  const QCPAbstractPlottable *plottable_p)
{

  auto result = std::find_if(
    m_plottableProcessingFlowMap.begin(),
    m_plottableProcessingFlowMap.end(),
    [plottable_p](
      const std::pair<QCPAbstractPlottable *, ProcessingFlow> &item) {
      return item.first == plottable_p;
    });

  if(result == m_plottableProcessingFlowMap.end())
    qFatal("Cannot be that map item is not found.");

  m_plottableProcessingFlowMap.erase(result);
}


MsRunDataSetCstSPtr
BasePlotCompositeWidget::getMsRunDataSetCstSPtrForPlottable(
  const QCPAbstractPlottable *plottable_p) const
{
  ProcessingFlow processing_flow = getProcessingFlowForPlottable(plottable_p);

  return processing_flow.getMsRunDataSetCstSPtr();
}


QCPAbstractPlottable *
BasePlotCompositeWidget::plottableToBeUsedAsIntegrationSource() const
{

  int plottable_count = mp_plotWidget->plottableCount();

  if(!plottable_count)
    return nullptr;

  if(plottable_count == 1)
    {
      // qDebug() << "Only a plottable is plotted, this is going to be the
      // source";

      return mp_plotWidget->plottable();
    }

  // At this point we know there are more than one plottable in the plot
  // widget. We need to get the selected one (if any).
  QList<QCPAbstractPlottable *> selected_plottable_list;

  selected_plottable_list = mp_plotWidget->selectedPlottables();

  if(!selected_plottable_list.size() || selected_plottable_list.size() > 1)
    return nullptr;

  // At this point we know there was *one* selected plottable. This is going
  // to be the source.
  return selected_plottable_list.front();
}


QCPAbstractPlottable *
BasePlotCompositeWidget::firstPlottable() const
{
  int plottable_count = mp_plotWidget->plottableCount();

  if(!plottable_count)
    return nullptr;

  return mp_plotWidget->plottable(0);
}


QList<QCPAbstractPlottable *>
BasePlotCompositeWidget::plottablesToBeUsedAsIntegrationDestination() const
{
  // If there is a single graph, selected or not, that is going to be the
  // destination. If there are more than one graph, only the ones selected
  // will be the destination.

  QList<QCPAbstractPlottable *> plottable_list;
  int graph_count = mp_plotWidget->graphCount();

  if(!graph_count)
    {
      // Not a single graph in the widget.
    }
  else if(graph_count == 1)
    {
      // Only one graph in the widget.
      plottable_list.append(mp_plotWidget->graph());
    }
  else
    {
      // More than one graph in the widget.
      plottable_list = mp_plotWidget->selectedPlottables();
    }

  return plottable_list;
}


void
BasePlotCompositeWidget::showMsFragmentationSpecDlg()
{
  // qDebug();

  // The idea is that we show the fragmentation spec dialog initialized
  // with the most recent step's fragmentation specification.

  // First we get the source plottable that might be of interest to the user.

  QCPAbstractPlottable *source_plottable_p =
    plottableToBeUsedAsIntegrationSource();

  if(source_plottable_p == nullptr)
    {
      QMessageBox::information(
        this,
        "Please select *one* trace",
        "In order to set the m/z integration parameters, the source "
        "graph needs to be selected.",
        QMessageBox::StandardButton::Ok,
        QMessageBox::StandardButton::NoButton);

      return;
    }

  // Next get the processing flow for that specific graph.

  const ProcessingFlow &processing_flow =
    getProcessingFlowForPlottable(source_plottable_p);

  const ProcessingStep *processing_step_p = processing_flow.mostRecentStep();

  MsFragmentationSpec ms_fragmentation_spec;

  // Ask that only specs having a valid MsFragmentationSpec instance be
  // returned (true boolean below)

  std::vector<ProcessingSpec *> processing_specs =
    processing_step_p->allProcessingSpecsMatching(ProcessingType("ANY_TO_MZ"),
                                                  true);
  // qDebug() << "The number of processing specs that match processing
  // type "
  //"\"ANY_TO_MZ\" is:"
  //<< processing_specs.size();

  if(!processing_specs.size())
    {
      // Not a single spec had a valid ms fragmentation spec. Last resort:
      // get the default one from the flow.

      ms_fragmentation_spec = processing_flow.getDefaultMsFragmentationSpec();

      // qDebug().noquote() << "The default ms frag spec:"
      //<< ms_fragmentation_spec.toString();

      // It might well be invalid, but that is not a problem, that would
      // mean that we open an empty dialog later.
    }
  else
    {
      // There was/were indeed spec instances with a valid ms
      // fragmentation spec. Arbitrarily select the last one.

      ProcessingSpec *processing_spec_p = processing_specs.back();
      ms_fragmentation_spec = processing_spec_p->getMsFragmentationSpec();
    }

  // We can finally use it to  configure the dialog window.

  if(mp_msFragmentationSpecifDlg == nullptr)
    {
      // qDebug().noquote() << "Opening new dialog with ms frag spec:"
      //<< ms_fragmentation_spec.toString();

      mp_msFragmentationSpecifDlg = new MsFragmentationSpecDlg(
        this, ms_fragmentation_spec, source_plottable_p->pen().color());

      connect(
        mp_msFragmentationSpecifDlg,
        &MsFragmentationSpecDlg::msFragmentationSpecDlgShouldBeDestroyedSignal,
        [this]() {
          // qDebug();
          delete QObject::sender();
          mp_msFragmentationSpecifDlg = nullptr;
        });

      connect(mp_msFragmentationSpecifDlg,
              &MsFragmentationSpecDlg::msFragmentationSpecChangedSignal,
              this,
              &BasePlotCompositeWidget::msFragmentationSpecChanged);

      mp_msFragmentationSpecifDlg->show();
    }
}


void
BasePlotCompositeWidget::msFragmentationSpecChanged(
  MsFragmentationSpec ms_fragmentation_spec)
{
  // qDebug();

  // First we get the source plottable that might be of interest to the user.

  QCPAbstractPlottable *source_plottable_p =
    plottableToBeUsedAsIntegrationSource();

  if(source_plottable_p == nullptr)
    {
      QMessageBox::information(
        this,
        "Please select *one* trace",
        "In order to set the MS fragmentation specifications, the source "
        "graph needs to be selected.",
        QMessageBox::StandardButton::Ok,
        QMessageBox::StandardButton::NoButton);

      return;
    }

  // qDebug() << "The validated ms fragmentation specification:"
  //<< ms_fragmentation_spec.toString();

  // Next get the processing flow for that specific graph.

  ProcessingFlow *processing_flow_p =
    getProcessingFlowPtrForPlottable(source_plottable_p);

  // Set the default ms frag spec.
  processing_flow_p->setDefaultMsFragmentationSpec(ms_fragmentation_spec);

  qDebug() << "Now setting the MS level to the spin box.";

  // Change the MS level value in the spinbox.
  // But we need to disconnect the spin box' value changed signal, because
  // otherwise, the msLevelValueChanged() is called and it will update the ms
  // frag spec's ms level again.

  disconnect(m_ui.msLevelSpinBox,
          QOverload<int>::of(&QSpinBox::valueChanged),
          this,
          &BasePlotCompositeWidget::msLevelValueChanged);

  m_ui.msLevelSpinBox->setValue(ms_fragmentation_spec.getMsLevel());

  // And connect back
  connect(m_ui.msLevelSpinBox,
          QOverload<int>::of(&QSpinBox::valueChanged),
          this,
          &BasePlotCompositeWidget::msLevelValueChanged);


}


void
BasePlotCompositeWidget::msLevelValueChanged(int value)
{
  // The user has selected a new MS level value, we need to ensure that this
  // value is record in the MsFragmentationSpec.

  qDebug() << "Value: " << value;

  QCPAbstractPlottable *source_plottable_p =
    plottableToBeUsedAsIntegrationSource();

  if(source_plottable_p == nullptr)
    {
      QMessageBox::information(
        this,
        "Please select *one* trace",
        "In order to set the MS level for the next integration, the source "
        "graph needs to be selected.",
        QMessageBox::StandardButton::Ok,
        QMessageBox::StandardButton::NoButton);

      return;
    }

  // Next get the processing flow for that specific graph.

  ProcessingFlow *processing_flow_p =
    getProcessingFlowPtrForPlottable(source_plottable_p);

  // Now get the MsFragmentationSpec out of that ProcessingFlow.

  MsFragmentationSpec ms_fragmentation_spec =
    processing_flow_p->getDefaultMsFragmentationSpec();

  // Update the value of the MS level.

  ms_fragmentation_spec.setMsLevel(value);

  // And finally set the default ms frag spec.
  processing_flow_p->setDefaultMsFragmentationSpec(ms_fragmentation_spec);
}


pappso::MzIntegrationParams
BasePlotCompositeWidget::getMzIntegrationParams(
  const QCPAbstractPlottable *plottable_p)
{
  // We need to be able to find mz integration params and there is a process to
  // search for them.

  // Get the processing flow for the plottable.
  const ProcessingFlow &processing_flow =
    getProcessingFlowForPlottable(plottable_p);

  // Now extract mz integration parameters from where they sit. That might
  // be multiple places.

  pappso::MzIntegrationParams mz_integration_params;

  // First check if the processing flow has default mz integration params.

  mz_integration_params = processing_flow.getDefaultMzIntegrationParams();

  if(!mz_integration_params.isValid())
    {
      // Check if there is at least one step in the flow that has mz integration
      // params set.

      const pappso::MzIntegrationParams *most_recent_mz_integration_params_p =
        processing_flow.mostRecentMzIntegrationParams();

      if(most_recent_mz_integration_params_p == nullptr)
        {
          // Last resort: get the parameters out of the ms run data set, the
          // params that are calculated on the basis of the statistical analysis
          // of the data.

          mz_integration_params = processing_flow.getMsRunDataSetCstSPtr()
                                    ->craftInitialMzIntegrationParams();

          if(!mz_integration_params.isValid())
            {
              qFatal(
                "Could not find any valid integration parameters for "
                "graph.");
            }
        }
      else
        {
          // qDebug().noquote() << "Got the most recent mz integration params:"
          //<< most_recent_mz_integration_params_p->toString();

          mz_integration_params = *most_recent_mz_integration_params_p;
        }
    }
  else
    {
      // qDebug().noquote() << "Got the default mz integration params:"
      //<< mz_integration_params.toString();
    }

  if(!mz_integration_params.isValid())
    qFatal("Cannot be possible not to have valid mz integration parameters.");

  return mz_integration_params;
}


void
BasePlotCompositeWidget::showMzIntegrationParamsDlg()
{
  // qDebug();

  // First we get the source plottable that might be of interest to the user.

  QCPAbstractPlottable *source_plottable_p =
    plottableToBeUsedAsIntegrationSource();

  if(source_plottable_p == nullptr)
    {
      QMessageBox::information(
        this,
        "Please select *one* trace",
        "In order to set the m/z integration parameters, the source "
        "graph needs to be selected.",
        QMessageBox::StandardButton::Ok,
        QMessageBox::StandardButton::NoButton);

      return;
    }

  // Get the mz integration params for the plottable.
  pappso::MzIntegrationParams mz_integration_params =
    getMzIntegrationParams(source_plottable_p);

  // qDebug().noquote() << "Found mz integration params:"
  //<< mz_integration_params.toString();

  if(mp_mzIntegrationParamsDlg == nullptr)
    {
      // qDebug();

      mp_mzIntegrationParamsDlg = new MzIntegrationParamsDlg(
        this, mz_integration_params, source_plottable_p->pen().color());

      connect(
        mp_mzIntegrationParamsDlg,
        &MzIntegrationParamsDlg::mzIntegrationParamsDlgShouldBeDestroyedSignal,
        [this]() {
          // qDebug();
          delete QObject::sender();
          mp_mzIntegrationParamsDlg = nullptr;
        });

      // connect(mp_mzIntegrationParamsDlg,
      //&MzIntegrationParamsDlg::mzIntegrationParamsChangedSignal,
      //[this](MzIntegrationParams mz_integration_params) {
      // qDebug() << "The mz integration params have been applied.";
      // m_mzIntegrationParams = mz_integration_params;
      // qDebug() << "params:" << m_mzIntegrationParams.toString();
      //});

      connect(mp_mzIntegrationParamsDlg,
              &MzIntegrationParamsDlg::mzIntegrationParamsChangedSignal,
              this,
              &BasePlotCompositeWidget::mzIntegrationParamsChanged);

      mp_mzIntegrationParamsDlg->show();
    }
}


void
BasePlotCompositeWidget::mzIntegrationParamsChanged(
  pappso::MzIntegrationParams mz_integration_params)
{
  // qDebug();

  // First we get the source plottable that might be of interest to the user.

  QCPAbstractPlottable *source_plottable_p =
    plottableToBeUsedAsIntegrationSource();

  if(source_plottable_p == nullptr)
    {
      QMessageBox::information(
        this,
        "Please select *one* trace",
        "In order to set the m/z integration parameters, the source "
        "graph needs to be selected.",
        QMessageBox::StandardButton::Ok,
        QMessageBox::StandardButton::NoButton);

      return;
    }

  // qDebug().noquote() << "The validated mz integration parameters:"
  //<< mz_integration_params.toString();

  // Next get the processing flow for that specific graph.

  ProcessingFlow *processing_flow_p =
    getProcessingFlowPtrForPlottable(source_plottable_p);

  // And finally set the default mz integration params.
  processing_flow_p->setDefaultMzIntegrationParams(mz_integration_params);
}


void
BasePlotCompositeWidget::newTicIntensityCalculated(double tic_intensity) const
{
  m_lastTicIntensity = tic_intensity;

  m_ui.statusLineEdit->setText("TIC intensity: " +
                               QString("%1").arg(tic_intensity));
}


void
BasePlotCompositeWidget::resetTicIntensity()
{
  m_lastTicIntensity = 0;
}


QString
BasePlotCompositeWidget::craftAnalysisStanza(
  const pappso::BasePlotContext &context)
{
  // qDebug();

  QCPAbstractPlottable *plottable_p = plottableToBeUsedAsIntegrationSource();

  if(plottable_p == nullptr)
    {
      // We should inform the user if there are more than one plottable.

      int plottable_count = mp_plotWidget->plottableCount();

      if(plottable_count > 1)
        {
          // qDebug();
          QMessageBox::information(
            this,
            "Crafting an analysis stanza for a graph",
            "Please select a single trace and try again.");
        }

      m_analysisStanza = "";
      return QString();
    }

  ProcessingFlow processing_flow = getProcessingFlowForPlottable(plottable_p);

  MsRunDataSetCstSPtr ms_run_data_set_csp =
    processing_flow.getMsRunDataSetCstSPtr();

  QString sample_name = ms_run_data_set_csp->getMsRunId()->getSampleName();
  QString file_name   = ms_run_data_set_csp->getMsRunId()->getFileName();

  // qDebug() << "sample_name:" << sample_name << "and file_name:" << file_name;

  BasePlotWnd *parent_window = dynamic_cast<BasePlotWnd *>(mp_parentWnd);
  if(parent_window == nullptr)
    qFatal("Pointer returned by the function cannot be nullptr.");

  const AnalysisPreferences *analPrefs = mp_parentWnd->getAnalysisPreferences();
  if(analPrefs == nullptr)
    {
      m_analysisStanza = "";
      return QString();
    }

  // We need to know what kind of data we are handling so that we can select the
  // proper format.

  pappso::DataKind data_kind = context.dataKind;

  // qDebug() << "data_kind:" << static_cast<int>(data_kind);

  DataFormatStringSpecif *specif_p = nullptr;

  if(data_kind == pappso::DataKind::mz)
    specif_p =
      analPrefs->m_dataFormatStringSpecifHash.value(FormatType::MASS_SPEC);
  else if(data_kind == pappso::DataKind::rt)
    specif_p =
      analPrefs->m_dataFormatStringSpecifHash.value(FormatType::TIC_CHROM);
  else if(data_kind == pappso::DataKind::dt)
    specif_p =
      analPrefs->m_dataFormatStringSpecifHash.value(FormatType::DRIFT_SPEC);
  else
    qFatal("Cannot be that the data kind is unset.");

  if(specif_p == nullptr)
    qFatal("Programming error.");

  // The way we work here is that we get a format string that specifies how
  // the user wants to have the data formatted in the stanza. That format
  // string is located in a DataFormatStringSpecif object as the m_format
  // member. There is also a m_formatType member that indicates what is the
  // format that we request (mass spec, tic chrom or drift spec). That
  // format type member is an int that also is the key of the hash that is
  // located in the analPrefs: QHash<int, DataFormatStringSpecif *>
  // m_dataFormatStringSpecifHash.

  QString formatString = specif_p->m_format;

  QString stanza;

  QChar prevChar = ' ';

  for(int iter = 0; iter < formatString.size(); ++iter)
    {
      QChar curChar = formatString.at(iter);

      // qDebug() << "Current char:" << curChar;

      if(curChar == '\\')
        {
          if(prevChar == '\\')
            {
              stanza += '\\';
              prevChar = ' ';
              continue;
            }
          else
            {
              prevChar = '\\';
              continue;
            }
        }

      if(curChar == '%')
        {
          if(prevChar == '\\')
            {
              stanza += '%';
              prevChar = ' ';
              continue;
            }
          else
            {
              prevChar = '%';
              continue;
            }
        }

      if(curChar == 'n')
        {
          if(prevChar == '\\')
            {
              stanza += QString("\n");

              // Because a newline only works if it is followed by something,
              // and if the user wants a termination newline, then we need to
              // duplicate that new line char if we are at the end of the
              // string.

              if(iter == formatString.size() - 1)
                {

                  // This is the last character of the line, then, duplicate the
                  // newline so that it actually creates a new line in the text.

                  stanza += QString("\n");
                }

              prevChar = ' ';
              continue;
            }
        }

      if(prevChar == '%')
        {
          // The current character might have a specific signification.
          if(curChar == 'f')
            {
              QFileInfo fileInfo(file_name);
              if(fileInfo.exists())
                stanza += fileInfo.fileName();
              else
                stanza += "Untitled";
              prevChar = ' ';
              continue;
            }
          if(curChar == 's')
            {
              stanza += sample_name;
              prevChar = ' ';
              continue;
            }
          // Do not handle the X nor the Y because it depends on the kind of
          // plot widget. Let that handling to the derived class.
          if(curChar == 'x')
            {
              stanza += QString("%1").arg(context.xDelta, 0, 'g', 6);
              prevChar = ' ';
              continue;
            }
          if(curChar == 'y')
            {
              stanza += QString("%1").arg(context.yDelta, 0, 'g', 6);

              prevChar = ' ';
              continue;
            }
          // Do not handle z, M, we can't, we have not the proper context.
          if(curChar == 'I')
            {
              stanza += QString("%1").arg(m_lastTicIntensity, 0, 'g', 3);

              prevChar = ' ';
              continue;
            }
          if(curChar == 'b')
            {
              stanza += QString("%1").arg(context.xRegionRangeStart, 0, 'f', 3);

              prevChar = ' ';
              continue;
            }
          if(curChar == 'e')
            {
              stanza += QString("%1").arg(context.xRegionRangeEnd, 0, 'f', 3);

              prevChar = ' ';
              continue;
            }

          // At this point the '%' is not followed by any special character
          // above, so we add it as we found it, because in derived classes
          // that %<char> might be meaningful, like %z for the charge of an ion
          // in a mass spectrum.
          else
            {
              stanza += '%';
              stanza += curChar;
              prevChar = ' ';

              continue;
            }
        }
      // End of
      // if(prevChar == '%')

      // The character prior this current one was not '%' so we just append
      // the current character.
      stanza += curChar;
    }
  // End of
  // for (int iter = 0; iter < pattern.size(); ++iter)

  // Finally, set the stanza to the member datum and return it.
  m_analysisStanza = stanza;

  // qDebug() << "Returning m_analysisStanza:" << m_analysisStanza;

  // Note that each time a stanza has been crafted, we need to reset the tic
  // intensity value to 0 so that we do not craft two stanzas with the same
  // value if no single intensity calculation was performed between them.

  resetTicIntensity();

  return m_analysisStanza;
}


void
BasePlotCompositeWidget::setPlottingColor(QCPAbstractPlottable *plottable_p,
                                          const QColor &color)
{
  if(!color.isValid())
    qFatal("The color is invalid!");

  return mp_plotWidget->setPlottingColor(plottable_p, color);
}


} // namespace minexpert

} // namespace msxps
