/* 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>
#include <QSettings>
#include <QWidget>


/////////////////////// pappsomspp includes
#include <pappsomspp/core/processing/combiners/tracepluscombiner.h>
#include <pappsomspp/gui/plotwidget/basetraceplotwidget.h>


/////////////////////// libXpertMass includes


/////////////////////// libXpertMassGui includes
#include <MsXpS/libXpertMassGui/ColorSelector.hpp>
#include <MsXpS/libXpertMassGui/ActionManager.hpp>


/////////////////////// Local includes


/////////////////////// Local includes
#include "BasePlotCompositeWidget.hpp"
#include "MassSpecTracePlotCompositeWidget.hpp"
#include "DriftSpecTracePlotCompositeWidget.hpp"
#include "TicXicChromTracePlotCompositeWidget.hpp"
#include "MsFragmentationSpecDlg.hpp"
#include "MzIntegrationParamsDlg.hpp"
#include "ProcessingFlowViewerDlg.hpp"
#include "Application.hpp"
#include "ProgramWindow.hpp"
#include "ui_BasePlotCompositeWidget.h"

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),
    m_ui(new ::Ui::BasePlotCompositeWidget)
{
  // 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();

  // 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,
          [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() << "BEGIN creating the main menu";

  // if(mp_mainMenu != nullptr)
  //   delete mp_mainMenu;

  if(mp_mainMenu != nullptr)
    return;

  // #include <type_traits>
  // static_assert(std::is_base_of<QWidget, BasePlotCompositeWidget>::value,
  //               "BasePlotCompositeWidget should inherit from QWidget");

  // Create the menu.
  mp_mainMenu = new QMenu(static_cast<QWidget *>(this));

  // qDebug() << "Created the main menu:" << mp_mainMenu;

  // Create the menu push button.
  // const QIcon main_menu_icon =
  //   QIcon(":/images/svg/mobile-phone-like-menu-button.svg");
  // m_ui->mainMenuPushButton->setIcon(main_menu_icon);

  ProgramWindow *program_window_p =
    static_cast<Application *>(qApp)->getProgramWindow();
  QString action_label;
  libXpertMassGui::ActionId action_id;
  QKeySequence key_sequence;
  QAction *action_p = nullptr;

  key_sequence = QKeySequence("Ctrl+P, M");
  action_label = QString("Plot widget menu");
  action_id.initialize("Global", "Menus", "CompositeWidget", action_label);
  action_p = program_window_p->getActionManager()->installAction(action_id,
                                                                 key_sequence);
  this->addAction(action_p);
  m_ui->mainMenuPushButton->setToolTip(action_p->text());

  connect(action_p, &QAction::triggered, this, [&]() {
    // qDebug() << "Trigger.";

    // We need to know what has triggered. First off, we do not want that
    // the menu be opened for all the BasePlotCompositeWidget instances
    // having the focus in all the plot windows. And then we only
    // want to open the menu for the composite plot widget that has the focus.

    if(static_cast<QWidget *>(parent())->isActiveWindow() && isFocussed())
      m_ui->mainMenuPushButton->click();
  });

  // Older version
  // 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);

  key_sequence = QKeySequence("Ctrl+O, I");
  action_label = QString("Open m/z integration parameters dialog");
  action_id.initialize(
    "Global", "Integrations", "OpenMzIntegrationParams", action_label);
  action_p = program_window_p->getActionManager()->installAction(action_id,
                                                                 key_sequence);
  this->addAction(action_p);
  connect(action_p, &QAction::triggered, this, [&]() {
    // qDebug() << "Trigger.";

    // We need to know what has triggered. First off, we do not want that
    // the dialog window be opened for all the BasePlotCompositeWidget instances
    // having the focus in all the plot windows (TIC/XIC chrom. and mass
    // spectra, for example). And then we only want to open the dialog for the
    // composite plot widget that has the focus.

    if(static_cast<QWidget *>(parent())->isActiveWindow() && isFocussed())
      BasePlotCompositeWidget::showMzIntegrationParamsDlg();
  });
  mp_mainMenu->addAction(action_p);

  // Older version
  // 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("Display processing flow", dynamic_cast<QObject *>(this));
  show_processing_flow_action_p->setStatusTip(
    "Display the processing flow that describes the selected trace");
  show_processing_flow_action_p->setShortcut(QKeySequence("Ctrl+D, P"));

  connect(const_cast<const QAction *>(show_processing_flow_action_p),
          &QAction::triggered,
          this,
          &BasePlotCompositeWidget::showProcessingFlow);

  mp_mainMenu->addAction(show_processing_flow_action_p);

  QAction *reset_plot_ranges_history_p =
    new QAction("Reset plot ranges history", dynamic_cast<QObject *>(this));
  reset_plot_ranges_history_p->setStatusTip(
    "Empty the list of plot ranges in the history (reset the backspace "
    "feature)");
  reset_plot_ranges_history_p->setShortcut(QKeySequence("Ctrl+R, H"));

  connect(reset_plot_ranges_history_p,
          &QAction::triggered,
          this,
          &BasePlotCompositeWidget::resetPlotRangesHistory);

  mp_mainMenu->addAction(reset_plot_ranges_history_p);

  mp_mainMenu->addSeparator();

  mp_plotGraphicsMenu = new QMenu("Save plot graphics to clipboard");

  QAction *save_svg_plot_to_clipboard_p =
    new QAction("Save SVG plot graphics", this);
  save_svg_plot_to_clipboard_p->setStatusTip(tr("Save SVG plot graphics"));

  mp_plotGraphicsMenu->addAction(save_svg_plot_to_clipboard_p);

  connect(save_svg_plot_to_clipboard_p, &QAction::triggered, [this]() {
    saveSvgPlotToClipboard();
  });

  QAction *save_raster_plot_to_clipboard_p =
    new QAction("Save RASTER plot graphics", this);
  save_raster_plot_to_clipboard_p->setStatusTip(
    tr("Save RASTER plot graphics"));

  mp_plotGraphicsMenu->addAction(save_raster_plot_to_clipboard_p);

  connect(save_raster_plot_to_clipboard_p, &QAction::triggered, [this]() {
    saveRasterPlotToClipboard();
  });

  mp_mainMenu->addMenu(mp_plotGraphicsMenu);

  // qDebug() << "END creating the main menu:" << mp_mainMenu;
}

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

  return mp_parentWnd->getAnalysisFilePtr();
}

QSharedPointer<DiscoveriesPreferences>
BasePlotCompositeWidget::getDiscoveriesPreferences()
{
  return mp_parentWnd->getDiscoveriesPreferences();
}

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();
  m_isFocussed = has_focus;
}

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);
}

std::pair<int, int>
BasePlotCompositeWidget::computePlotSize(std::pair<int, int> requested,
                                         std::pair<int, int> actual) const
{
  int actual_width  = actual.first;
  int actual_height = actual.second;

  int requested_width  = requested.first;
  int requested_height = requested.second;

  int final_width;
  int final_height;

  double aspect_ratio = actual_width / actual_height;

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

  if(!requested_width && !requested_height)
    {
      // The user has requested no dimensions. Use the actual plot size.
      // qDebug() << "Both dimensions are naught.";
      final_width  = actual_width;
      final_height = actual_height;
    }
  else if(!requested_width)
    {
      // Compute this dimension using the aspect ratio.

      // qDebug() << "width is naught.";
      final_height = requested_height;
      final_width  = final_height * aspect_ratio;
    }
  else if(!requested_height)
    {
      // Compute this dimension using the aspect ratio.
      // qDebug() << "height is naught.";
      final_width  = requested_width;
      final_height = final_width / aspect_ratio;
    }
  else
    {
      // qDebug() << "Both dimensions are non-naught.";
      final_width  = requested_width;
      final_height = requested_height;
    }

  // qDebug() << "Saving PNG file with final dimensions:"
  //<< final_width << final_height;

  return std::pair<int, int>(final_width, final_height);
}

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

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

  // How about reading the full filel into a string and then setting that string
  // to the clipboard as an image.

  QBuffer out_buffer;
  out_buffer.open(QBuffer::WriteOnly);
  svg_generator.setOutputDevice(&out_buffer);

  qcpPainter.begin(&svg_generator);
  mp_plotWidget->toPainter(
    &qcpPainter, plot_widget_size.width(), plot_widget_size.height());
  qcpPainter.end();

  // qDebug() << "out_buffer size:" << out_buffer.size();

  QMimeData *mime_data_p = new QMimeData();
  mime_data_p->setData("image/svg+xml", out_buffer.buffer());
  QApplication::clipboard()->setMimeData(mime_data_p, QClipboard::Clipboard);

  return true;
}

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

  QBuffer out_buffer;
  out_buffer.open(QBuffer::WriteOnly);
  QSvgGenerator svg_generator;
  svg_generator.setOutputDevice(&out_buffer);

  QCPPainter qcpPainter;
  qcpPainter.begin(&svg_generator);
  mp_plotWidget->toPainter(
    &qcpPainter, plot_widget_size.width(), plot_widget_size.height());
  qcpPainter.end();

  QMimeData *mime_data_p = new QMimeData();
  mime_data_p->setData("image/svg+xml", out_buffer.buffer());
  QApplication::clipboard()->setMimeData(mime_data_p, QClipboard::Clipboard);

  return true;
}

bool
BasePlotCompositeWidget::savePlotToPngFile(
  const QString &file_name, int width, int height, int scale, int quality) const
{
  // qDebug() << "Saving PNG file with:" << width << height << scale;

  QSize plot_widget_size = mp_plotWidget->size();

  std::pair<int, int> plot_dimensions = computePlotSize(
    std::pair<int, int>(width, height),
    std::pair<int, int>(plot_widget_size.width(), plot_widget_size.height()));

  // qDebug() << "Saving PNG file with final dimensions:"
  //<< plot_dimensions.first << plot_dimensions.second;

  return mp_plotWidget->savePng(
    file_name, plot_dimensions.first, plot_dimensions.second, scale, quality);
}

bool
BasePlotCompositeWidget::savePlotToJpgFile(
  const QString &file_name, int width, int height, int scale, int quality) const
{
  // qDebug() << "Saving JPG file with:" << width << height << scale;

  QSize plot_widget_size = mp_plotWidget->size();

  std::pair<int, int> plot_dimensions = computePlotSize(
    std::pair<int, int>(width, height),
    std::pair<int, int>(plot_widget_size.width(), plot_widget_size.height()));

  // qDebug() << "Saving JPG file with final dimensions:"
  //<< plot_dimensions.first << plot_dimensions.second;

  return mp_plotWidget->saveJpg(
    file_name, plot_dimensions.first, plot_dimensions.second, scale, quality);
}

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

  QPixmap pixmap = mp_plotWidget->toPixmap(
    plot_widget_size.width(), plot_widget_size.height(), 1);

  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;
    }

  const ProcessingFlow *processing_flow_p =
    getCstProcessingFlowPtrForPlottable(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)
    {
      // qDebug() << "Allocating new ProcessingFlowViewerDlg.";

      mp_processingFlowViewerDlg = new ProcessingFlowViewerDlg(
        mp_parentWnd, "MineXpert3", plottable_p->pen().color());
    }
  else
    {
      // qDebug() << "Using existing ProcessingFlowViewerDlg window.";

      mp_processingFlowViewerDlg->updateTraceColor(plottable_p->pen().color());
    }

  mp_processingFlowViewerDlg->setProcessingFlowText(
    processing_flow_p->toString(0, " "));

  mp_processingFlowViewerDlg->activateWindow();
  mp_processingFlowViewerDlg->raise();
  mp_processingFlowViewerDlg->show();
  // qDebug() << "Now dialog window should be visible.";

  // qDebug() << "And the window is active:"
  //          << mp_processingFlowViewerDlg->isActiveWindow()
  //          << "and visible:" << mp_processingFlowViewerDlg->isVisible();
}

void
BasePlotCompositeWidget::resetPlotRangesHistory()
{

  // We need to make sure the plot widget has its plot ranges history list
  // emptied.

  mp_plotWidget->resetAxesRangeHistory();
}

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

void
BasePlotCompositeWidget::lastCursorHoveredPoint(const QPointF &pointf)
{
  // Display the position point in the status text. Depending on the derived
  // class (mass spectrum, ion mobility, retention time...) we display the value
  // with differing numbers of decimal places.

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

  QString status_line_edit_text;

  const MassSpecTracePlotCompositeWidget *mass_spec_compo_widget_p =
    dynamic_cast<MassSpecTracePlotCompositeWidget *>(this);
  if(mass_spec_compo_widget_p != nullptr)
    status_line_edit_text =
      QString("(%1,%2)")
        .arg(pointf.x(), 0, 'f', libXpertMassCore::MZ_DEC_PLACES)
        .arg(pointf.y(), 0, 'f', libXpertMassCore::INTENSITY_DEC_PLACES);

  const DriftSpecTracePlotCompositeWidget *drift_spec_compo_widget_p =
    dynamic_cast<DriftSpecTracePlotCompositeWidget *>(this);
  if(drift_spec_compo_widget_p != nullptr)
    status_line_edit_text =
      QString("(%1,%2)")
        .arg(pointf.x(), 0, 'f', libXpertMassCore::DEFAULT_DEC_PLACES)
        .arg(pointf.y(), 0, 'f', libXpertMassCore::INTENSITY_DEC_PLACES);

  const TicXicChromTracePlotCompositeWidget *ticxic_spec_compo_widget_p =
    dynamic_cast<TicXicChromTracePlotCompositeWidget *>(this);
  if(ticxic_spec_compo_widget_p != nullptr)
    status_line_edit_text =
      QString("(%1,%2)")
        .arg(pointf.x(), 0, 'f', libXpertMassCore::DEFAULT_DEC_PLACES)
        .arg(pointf.y(), 0, 'f', libXpertMassCore::INTENSITY_DEC_PLACES);

  m_ui->statusLineEdit->setText(status_line_edit_text);

  // The user might have configured the decimals to display depending on the
  // kind of x-axis value (retention time, m/z, ion mobility...).


  // The color might have been changed (when setting the intensity value text)
  // But here we want it in black.
  QString style = QString("color: black");
  m_ui->statusLineEdit->setStyleSheet(style);

  // 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)
{
  // A measurement was performed, inform the user.

  // There are several situations:
  //
  // 1. The selection polygon is 1D, we only care of the X axis segment
  // coordinates and delta.
  //
  // 2. The selection polygon is 2D, we should care of both the X axis segment
  // coordinates and of the Y axis segment coordinates, also show both deltas.

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

  // First describe the selection polygon.

  //**QString text = context.m_selectionPolygon.toShort4PointsString();
  QString text = context.msp_integrationScope->toString();

  // Now describe the x|y delta.

  // We always want the X delta.
  //**context.m_selectionPolygon.rangeX(start, end);
  //**double delta = abs(end - start);
  double width;
  if(!context.msp_integrationScope->getWidth(width))
    qFatal() << "Failed to get the width.";

  int decimals  = pappso::Utils::zeroDecimalsInValue(width) + 3;
  text         += QString(" -- xDelta: %1").arg(width, 0, 'f', decimals);

  if(width < 1)
    {
      // And also the inverse of xDelta because it provides a hint to the charge
      // is the xDelta is between to consecutive peaks of an isotopic cluster.

      text += QString(" -- 1/xDelta: %1").arg(1 / width, 0, 'f', decimals);
    }

  // We want the Y delta only if the polygon is 2D.
  //**if(context.m_selectionPolygon.is2D())
  double height;
  if(context.msp_integrationScope->getHeight(height))
    {
      // qDebug() << "Integration scope is Rect/Poly";
      //**context.m_selectionPolygon.rangeY(start, end);
      text += QString(" -- yDelta: %1").arg(height, 0, 'f', 2);
    }

  // The color might have been changed (when setting the intensity value text)
  // But here we want it in black.
  QString style = QString("color: black");
  m_ui->statusLineEdit->setStyleSheet(style);
  m_ui->statusLineEdit->setText(text);
}

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

  m_plotWidgetPressedKeyCode = context.m_pressedKeyCode;

  if(context.m_pressedKeyCode == Qt::Key_Space)
    {
      // qDebug() << "Going to call craftAnalysisStanza()";
      craftAnalysisStanza(context);
    }
}

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

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

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

  // if(context.m_mouseButtonsAtMouseRelease == Qt::NoButton)
  //   qDebug() << "no button with context:" << context.toString();
  //
  // if(context.m_mouseButtonsAtMouseRelease == Qt::LeftButton)
  //   qDebug() << "left button with context:" << context.toString();
  //
  // if(context.m_mouseButtonsAtMouseRelease == 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.m_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;

  if(!is_selected)
    {
      // If it is not selected, but there is only one plottable, there is no
      // ambiguity: show the sample name with color.

      QCPAbstractPlottable *the_plottable_p =
        plottableToBeUsedAsIntegrationSource();

      if(the_plottable_p == nullptr)
        {
          // There are more than one plottable.
          // Erase the sample name.
          m_ui->sampleNameLabel->setText("");
          return;
        }
    }

  // At this point we know we we can show the sample name with proper color.

  const ProcessingFlow *processing_flow_p =
    getCstProcessingFlowPtrForPlottable(plottable_p);
  MsRunDataSetCstSPtr ms_run_data_set_csp =
    processing_flow_p->getMsRunDataSetCstSPtr();


  QString sample_name = ms_run_data_set_csp->getMsRunId()->getSampleName();
  m_ui->sampleNameLabel->setText(sample_name);

  QColor color = plottable_p->pen().color();
  setSampleNameColor(color);
}

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;
}

const ProcessingFlow *
BasePlotCompositeWidget::getCstProcessingFlowPtrForPlottable(
  const QCPAbstractPlottable *plottable_p) const
{
  // qDebug() << "Entering getProcessingFlowForPlottable()";

  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.

  std::map<QCPAbstractPlottable *, ProcessingFlow *>::const_iterator it;

  for(it = m_plottableProcessingFlowMap.begin();
      it != m_plottableProcessingFlowMap.end();
      ++it)
    {
      if(it->first == plottable_p)
        {
          // qDebug() << "Found ProcessingFlow:" << it->second.toString();
          return it->second;
        }
    }

  qFatal()
    << "Cannot be that a graph have no processing flow associated to it.";

  return nullptr;
}

ProcessingFlow *
BasePlotCompositeWidget::getProcessingFlowPtrForPlottable(
  const QCPAbstractPlottable *plottable_p)
{
  // qDebug() << "Entering getProcessingFlowForPlottable()";

  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.

  std::map<QCPAbstractPlottable *, ProcessingFlow *>::const_iterator it;

  for(it = m_plottableProcessingFlowMap.begin();
      it != m_plottableProcessingFlowMap.end();
      ++it)
    {
      if(it->first == plottable_p)
        {
          // qDebug() << "Found ProcessingFlow:" << it->second.toString();
          return it->second;
        }
    }

  qFatal()
    << "Cannot be that a graph have no processing flow associated to it.";

  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.";
      // Needed for cppcheck to see that erase will be called if
      // iterator is not end().
      return;
    }

  m_plottableProcessingFlowMap.erase(result);
}

MsRunDataSetCstSPtr
BasePlotCompositeWidget::getMsRunDataSetCstSPtrForPlottable(
  const QCPAbstractPlottable *plottable_p) const
{
  const ProcessingFlow *processing_flow_cp =
    getCstProcessingFlowPtrForPlottable(plottable_p);

  return processing_flow_cp->getMsRunDataSetCstSPtr();
}

QCPAbstractPlottable *
BasePlotCompositeWidget::plottableToBeUsedAsIntegrationSource() const
{
  // qDebug();

  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 plottable_count = mp_plotWidget->plottableCount();

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

  return plottable_list;
}

bool
BasePlotCompositeWidget::isFocussed() const
{
  return m_isFocussed;
}

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_cp =
    getCstProcessingFlowPtrForPlottable(source_plottable_p);

  MsFragmentationSpec ms_fragmentation_spec;

  // Ask that only steps having a valid MsFragmentationSpec instance be
  // returned (true boolean below). Only of interest are step with the
  // destination matching MZ.

  std::vector<ProcessingStepCstSPtr> processing_steps =
    processing_flow_cp->stepsMatchingDestType(ProcessingType("MZ"), true);

  // qDebug() << "The number of processing specs that match processing
  // type "
  //"\"ANY_TO_MZ\" is:"
  //<< processing_specs.size();

  if(!processing_steps.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_cp->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 step instances with a valid ms
      // fragmentation spec. Arbitrarily select the last one.

      ProcessingStepCstSPtr processing_step_csp = processing_steps.back();
      ms_fragmentation_spec = processing_step_csp->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);
}

/* Heap-allocated*/
pappso::MzIntegrationParams *
BasePlotCompositeWidget::getSensibleMzIntegrationParams(
  const QCPAbstractPlottable *plottable_p)
{
  // qDebug();

  // Get the processing flow for the plottable because there might be
  // mz integration params in there.
  const ProcessingFlow *processing_flow_cp =
    getCstProcessingFlowPtrForPlottable(plottable_p);
  Q_ASSERT(processing_flow_cp != nullptr);

  return processing_flow_cp->getSensibleMzIntegrationParams();
}

pappso::FilterNameInterfaceCstSPtr
BasePlotCompositeWidget::filter(const QString filter_name,
                                const QCPAbstractPlottable *plottable_p) const
{

  // Get the processing flow for the plottable.
  const ProcessingFlow *processing_flow_cp =
    getCstProcessingFlowPtrForPlottable(plottable_p);

  // qDebug() << "The source plottable:" << plottable_p;
  // qDebug() << "The processing flow:" << &processing_flow;

  return processing_flow_cp->filter(filter_name);
}

void
BasePlotCompositeWidget::showMzIntegrationParamsDlg()
{
  // 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(
        dynamic_cast<QWidget *>(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. The integration
  // parameters contain data that we must keep (mzSmallestMz, for example) and
  // data that actually configure the bin size calculation (bin size model).
  // The bin size model, we want to check if the user has stored default
  // values for it. If so we use them. If not we keep what we got.

  const ProcessingFlow *processing_flow_cp =
    getCstProcessingFlowPtrForPlottable(source_plottable_p);
  Q_ASSERT(processing_flow_cp != nullptr);

  // Heap-allocated
  pappso::MzIntegrationParams *mz_integration_params_p =
    processing_flow_cp->getSensibleMzIntegrationParams();
  Q_ASSERT(mz_integration_params_p);

  // qDebug().noquote() << "Got MzIntegrationParams:\n"
  //                    << mz_integration_params_p->toString(0, "  ");

  if(mp_mzIntegrationParamsDlg == nullptr)
    {
      // qDebug() << "Now opening MzIntegrationParamsDlg.";

      mp_mzIntegrationParamsDlg =
        new MzIntegrationParamsDlg(dynamic_cast<QWidget *>(this),
                                   *mz_integration_params_p,
                                   source_plottable_p->pen().color());

      delete mz_integration_params_p;

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

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

      mp_mzIntegrationParamsDlg->show();
    }
}

void
BasePlotCompositeWidget::mzIntegrationParamsChanged(
  pappso::MzIntegrationParams &mz_integration_params)
{
  // 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(
        dynamic_cast<QWidget *>(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 QColor &color) const
{
  m_lastTicIntensity = tic_intensity;
  QString style;

  if(!color.isValid())
    style = QString("color: black");
  else
    style = QString("color: %1").arg(color.name());

  m_ui->statusLineEdit->setStyleSheet(style);
  m_ui->statusLineEdit->setText(
    "TIC intensity: " +
    QString("%1").arg(
      tic_intensity, 0, 'f', libXpertMassCore::INTENSITY_DEC_PLACES));
}

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

QString
BasePlotCompositeWidget::craftAnalysisStanza(
  const pappso::BasePlotContext &context)
{
  // qDebug().noquote() << "With context:" << context.toString();

  // qDebug().noquote() << "Drag direction(s):" <<
  // context.dragDirectionsToString();

  // If no analysis preferences were defined, just return an empty string.
  const QSharedPointer<DiscoveriesPreferences> discoveries_preferences_qsp =
    mp_parentWnd->getDiscoveriesPreferences();

  if(discoveries_preferences_qsp == nullptr)
    {
      QMessageBox::information(
        this,
        "Crafting an analysis stanza for a graph",
        "Please define analysis preferences and try again.");

      m_analysisStanza = "";
      return QString();
    }

  QCPAbstractPlottable *plottable_p = plottableToBeUsedAsIntegrationSource();

  if(plottable_p == nullptr)
    {
      qInfo()
        << "Returned plottable is nullptr (there might be more than one).";

      // 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,
            "Crafting an analysis stanza for a graph",
            "Please select a single trace and try again.");


          m_analysisStanza = "";
          return QString();
        }
    }

  // qDebug() << "Plottable:" << plottable_p;

  const ProcessingFlow *processing_flow_p =
    getCstProcessingFlowPtrForPlottable(plottable_p);

  // qDebug() << "Processing flow:" << processing_flow.toString();

  MsRunDataSetCstSPtr ms_run_data_set_csp =
    processing_flow_p->getMsRunDataSetCstSPtr();

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

  QFileInfo fileInfo(file_name);
  if(fileInfo.exists())
    file_name = fileInfo.fileName();
  else
    file_name = "Untitled";

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

  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.";

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

  pappso::Enums::DataKind data_kind = context.m_dataKind;

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

  DataFormatStringSpecif *specif_p = nullptr;

  if(data_kind == pappso::Enums::DataKind::mz)
    specif_p = discoveries_preferences_qsp->m_dataFormatStringSpecifHash.value(
      FormatType::MASS_SPEC);
  else if(data_kind == pappso::Enums::DataKind::rt)
    specif_p = discoveries_preferences_qsp->m_dataFormatStringSpecifHash.value(
      FormatType::TIC_CHROM);
  else if(data_kind == pappso::Enums::DataKind::dt)
    specif_p = discoveries_preferences_qsp->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;

  qDebug().noquote() << "The BasePlotContext: \n" << context.toString();

  qDebug().noquote() << "formatString: \n" << formatString;


  // 1 = key format-specifying letter
  // 2 = optional precision without the dot

  // Not all the format specifications are for number, specify which are
  static const QSet<QChar> floatKeys = {
    'X', 'Y', 'C', 'x', 'y', 'c', 'I', 'b', 'e'};
  static const QSet<QChar> integerKeys = {'z'};
  static const QSet<QChar> stringKeys  = {'f', 's'};

  // Automate the creation of the string representing all the format keys;
  // That string will be used below to craft the regular expression
  // that we will use to match the format keys in the format string
  // provided by the user.
  QString all_format_keys_as_string;

  foreach(QChar format_char, floatKeys)
    {
      all_format_keys_as_string += format_char;
    }

  foreach(QChar format_char, integerKeys)
    {
      all_format_keys_as_string += format_char;
    }

  foreach(QChar format_char, stringKeys)
    {
      all_format_keys_as_string += format_char;
    }

  QString regular_expression_string =
    QString("%([%1])(\\.(\\d+))?").arg(all_format_keys_as_string);

  static const QRegularExpression re(regular_expression_string);

  // static const QRegularExpression re("%([fsXYCxyczIbe])(\\.(\\d+))?");


  // In this base class, we will only handle non-specialty data set
  // data, like file name and sample, and non-specialty trace plot
  // data, like (x,y) data for a given point, or (deltaX, deltaY)
  // for a range mouse dragging movement, for example.

  // For specialty data, the derived classes will handle the specialty format
  // strings.

  // For now, the format string elements that we can handle are the following.
  // Each double format specification may specify the number of decimals
  // in the form %x.6, for example, which means that the double value
  // for format specif %x will be printed out with 6 decimals.

  // "%f : mass spectrometry data file name\n";
  // "%s : sample name\n";
  //
  // "%X.3 : value on the X axis (no unit)\n";
  // "%Y : value on the Y axis at cursor point(no unit)\n";
  // "%Z : value on the Y axis on the curve (no unit)\n";
  //
  // "%x.6 : delta value on the X axis (when appropriate, no unit)\n";
  // "%y : delta value on the Y axis (when appropriate, no unit)\n";
  // "%z : delta value on the Y axis on the curve (no unit)\n";
  //
  // "%b.3 : X axis range begin value (where applicable)\n";
  // "%e.3 : X axis range end value (where applicable)\n";
  //
  // "%I : TIC intensity after TIC integration over a graph range\n";

  QHash<QChar, QVariant> replacements;

  replacements['f'] = file_name;
  replacements['s'] = sample_name;


  // This is where the thing becomes tricky: depending on what action the user
  // has performed with their mouse, we need to craft a stanza that is
  // most meaningful.

  // If the user has not dragged the mouse without even cliking it,
  // or has only clicked, all we know is the last lastCursorHoveredPoint
  // otherwise it is the start drag point.

  // Recall an important concept:

  // context.m_isMouseDragging means that the user has clicked the space
  // during a mouse dragging operation (no mouse button release yet)

  // context.m_wasMouseDragging means that the user has terminated
  // the mouse dragging operation by releasing the mouse button.

  // This means that m_wasMouseDragging can be true and m_isMouseDragging false.
  // However, because of the way the context is captured, ,for the purpose
  // of the current stanza creation, both context situations are valid,
  // so we need to || or && them in the logical conditions below

  /////////////////// THE X and Y and Z values //////////////////////
  double x_value = 0;

  if(!context.m_isMouseDragging && !context.m_wasMouseDragging)
    {
      // The X value is the last hovered point x value
      x_value = context.m_lastCursorHoveredPoint.x();
    }
  else
    {
      // The X value is now the drag start point x value
      x_value = context.m_startDragPoint.x();
    }

  // We can now craft the other values, based on that x_value.

  replacements['X'] = x_value;
  ///////////////// Coordinates at the cursor point /////////////////
  replacements['Y'] = context.m_startDragPoint.y();
  ///////////////// Coordinates at the curve point /////////////////
  replacements['C'] =
    static_cast<pappso::BaseTracePlotWidget *>(mp_plotWidget)
      ->getYatX(x_value, dynamic_cast<QCPGraph *>(plottable_p));

  qDebug() << "Set the C key to " << replacements['C'];

  /////////////////// THE x and y and y values //////////////////////
  ///////////////// Drag delta values of coordinates /////////////////

  // These values only apply if there has been a mouse dragging event.
  if(context.m_isMouseDragging || context.m_wasMouseDragging)
    {
      double x_delta_value = context.m_xDelta;
      double y_delta_value = context.m_yDelta;

      replacements['x'] = x_delta_value;
      ///////////////// Coordinates at the cursor point /////////////////
      replacements['y'] = y_delta_value;
      ///////////////// Coordinates at the curve point /////////////////
      double y_start_curve =
        static_cast<pappso::BaseTracePlotWidget *>(mp_plotWidget)
          ->getYatX(context.m_startDragPoint.x(),
                    dynamic_cast<QCPGraph *>(plottable_p));
      double y_stop_curve =
        static_cast<pappso::BaseTracePlotWidget *>(mp_plotWidget)
          ->getYatX(context.m_currentDragPoint.x(),
                    dynamic_cast<QCPGraph *>(plottable_p));
      replacements['c'] = fabs(y_stop_curve - y_start_curve);

      qDebug() << "Set the c key to " << replacements['c'];
    }
  // else
  //   {
  //     replacements['x'] = "n/a";
  //     replacements['y'] = "n/a";
  //   }

  ///////////////// THE range b and e /////////////////////

  // These values only apply if there has been a mouse dragging event.
  if(context.m_isMouseDragging || context.m_wasMouseDragging)
    {
      replacements['b'] = context.m_xRegionRangeStart;
      replacements['e'] = context.m_xRegionRangeEnd;
    }
  // else
  //   {
  //     replacements['b'] = "n/a";
  //     replacements['e'] = "n/a";
  //   }

  //////////////// The computed intensity value /////////////////

  // This value is only available when the user has explicitely used
  // the Intensity integration. In that case, the m_intensity value
  // is valid.

  if(!qIsNaN(m_lastTicIntensity))
    {
      replacements['I'] =
        QString("%1").arg(static_cast<int>(m_lastTicIntensity));
    }
  else
    {
      replacements['I'] = "n/a";
    }

  QString stanza;
  stanza.reserve(formatString.size());

  int lastIndex = 0;
  auto it       = re.globalMatch(formatString);


  while(it.hasNext())
    {
      QRegularExpressionMatch m = it.next();

      int start = m.capturedStart();
      int end   = m.capturedEnd();

      // Copy text up to this match
      stanza += formatString.mid(lastIndex, start - lastIndex);

      const QChar key = m.captured(1)[0];

      qDebug() << "Captured key: " << key;

      const QString precision_spec_string = m.captured(3); // just the digits
      bool ok                             = false;
      int precision_spec                  = precision_spec_string.toInt(&ok);

      // By default float values have two decimals.
      if(!ok)
        precision_spec = 2;

      if(!replacements.contains(key))
        {
          QString untouched = m.captured(0);

          qDebug() << "Adding new untouched string element:" << untouched;

          stanza    += untouched;
          lastIndex  = end;
          continue;
        }

      const QVariant &v = replacements[key];

      if(floatKeys.contains(key))
        {
          qDebug() << "key" << key << "was found to be a float.";

          // ---- FLOAT TOKENS ----
          double d = 0.0;

          if(v.canConvert<double>())
            d = v.toDouble(); // integer or float → OK
          else
            {
              stanza    += "NaD";
              lastIndex  = end;
              continue;
            }

          stanza += QString::number(d, 'f', precision_spec);
        }
      else if(integerKeys.contains(key))
        {
          qDebug() << "key" << key << "was found to be an integer.";

          // ---- INTEGER TOKENS ----
          qlonglong i = 0;

          // INTEGER tokens should accept:
          // int, uint, qint64, quint64        (ideal case)
          // double or float                    (we can convert by
          // truncation) string with digits                 (allowed at your
          // discretion)

          if(v.canConvert<qlonglong>())
            i = v.toLongLong();
          else
            {
              stanza    += "NaI";
              lastIndex  = end;
              continue;
            }

          stanza += QString::number(i);
        }
      else if(stringKeys.contains(key))
        {
          qDebug() << "key" << key << "was found to be a string.";

          // ---- STRING TOKENS ----
          if(v.canConvert<QString>())
            stanza += v.toString();
          else
            stanza += "<NaS>"; // Not-a-String
        }

      lastIndex = end;
    }

  // trailing literal data
  stanza += formatString.mid(lastIndex);

#if 0
  /// INITIAL
  while(it.hasNext())
  {
  QRegularExpressionMatch m = it.next();

  int start = m.capturedStart();
  int end   = m.capturedEnd();

  // Copy text up to this match
  stanza += formatString.mid(lastIndex, start - lastIndex);

  const QChar key = m.captured(1)[0];

  qDebug() << "Captured key: " << key;

  const QString precision_spec = m.captured(3); // just the digits

  if(!replacements.contains(key))
  {
  QString untouched = m.captured(0);

  qDebug() << "Adding new untouched string element:" << untouched;

  stanza += untouched;
}
else
{
const QVariant &v = replacements[key];

if(numericKeys.contains(key))
{
qDebug() << "IS a numeric value.";

double d = v.toDouble();

if(!precision_spec.isEmpty())
{
qDebug() << "The precision spec is: " << precision_spec;

bool ok = false;
int p   = precision_spec.toInt(&ok);
if(ok)
{
QString formatted = QString::number(d, 'f', p);
qDebug()
<< "Formatted WITH precision spec:" << formatted;
stanza += formatted;
}
else
{
qDebug() << "Failed setting precision spec.";
QString formatted = QString::number(d);
qDebug()
<< "Formatted with NO precision spec:" << formatted;
stanza += formatted;
}
}
else
{
qDebug() << "Is NOT a numeric value.";
QString formatted = QString::number(d);
qDebug()
<< "Formatted with NO precision spec:" << formatted;
stanza += formatted;
}
}
else
{
qDebug() << "The variant" << v
<< "did not convert to double.";
QString formatted = v.toString();
qDebug() << "Formatted as a simple string";
// string-like contents (e.g., %f → filename)
// or integer values that need to decimals.
stanza += formatted;
}
}

lastIndex = end;
}

// Copy trailing part
QString trailing_string = formatString.mid(lastIndex);
qDebug() << "Adding the trailing string:" << trailing_string;
stanza += trailing_string;
#endif

  // Finally, set the stanza to the member datum and return it.
  // We want to keep it as a member datum, because derived classes
  // can build upon it with specialized data elements.
  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;
}

QString
BasePlotCompositeWidget::craftAnalysisStanzaOld(
  const pappso::BasePlotContext &context)
{
  // qDebug().noquote() << "With context:" << context.toString();

  // If no analysis preferences were defined, just return an empty string.
  const QSharedPointer<DiscoveriesPreferences> discoveries_preferences_qsp =
    mp_parentWnd->getDiscoveriesPreferences();

  if(discoveries_preferences_qsp == nullptr)
    {
      QMessageBox::information(
        this,
        "Crafting an analysis stanza for a graph",
        "Please define analysis preferences and try again.");

      m_analysisStanza = "";
      return QString();
    }

  QCPAbstractPlottable *plottable_p = plottableToBeUsedAsIntegrationSource();

  if(plottable_p == nullptr)
    {
      qInfo()
        << "Returned plottable is nullptr (there might be more than one).";

      // 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,
            "Crafting an analysis stanza for a graph",
            "Please select a single trace and try again.");


          m_analysisStanza = "";
          return QString();
        }
    }

  // qDebug() << "Plottable:" << plottable_p;

  const ProcessingFlow *processing_flow_p =
    getCstProcessingFlowPtrForPlottable(plottable_p);

  // qDebug() << "Processing flow:" << processing_flow.toString();

  MsRunDataSetCstSPtr ms_run_data_set_csp =
    processing_flow_p->getMsRunDataSetCstSPtr();

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

  QFileInfo fileInfo(file_name);
  if(fileInfo.exists())
    file_name = fileInfo.fileName();
  else
    file_name = "Untitled";

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

  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.";

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

  pappso::Enums::DataKind data_kind = context.m_dataKind;

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

  DataFormatStringSpecif *specif_p = nullptr;

  if(data_kind == pappso::Enums::DataKind::mz)
    specif_p = discoveries_preferences_qsp->m_dataFormatStringSpecifHash.value(
      FormatType::MASS_SPEC);
  else if(data_kind == pappso::Enums::DataKind::rt)
    specif_p = discoveries_preferences_qsp->m_dataFormatStringSpecifHash.value(
      FormatType::TIC_CHROM);
  else if(data_kind == pappso::Enums::DataKind::dt)
    specif_p = discoveries_preferences_qsp->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;

  qDebug().noquote() << "The BasePlotContext: \n" << context.toString();

  qDebug().noquote() << "formatString: \n" << formatString;

  static const QRegularExpression re("%([fsXYZxyIbe])");


  // In this base class, we will only handle non-specialty data set
  // data, like file name and sample, and non-specialty trace plot
  // data, like (x,y) data for a given point, or (deltaX, deltaY)
  // for a range mouse dragging movement, for example.

  // For specialty data, the derived classes will handle the specialty format
  // strings.

  // For now, the format string elements that we can handle are:

  // "%f : mass spectrometry data file name\n";
  // "%s : sample name\n";
  //
  // "%X : value on the X axis (no unit)\n";
  // "%Y : value on the Y axis at cursor point(no unit)\n";
  // "%Z : value on the Y axis on the curve (no unit)\n";
  //
  // "%x : delta value on the X axis (when appropriate, no unit)\n";
  // "%y : delta value on the Y axis (when appropriate, no unit)\n";
  // "%z : delta value on the Y axis on the curve (no unit)\n";
  //
  // "%b : X axis range begin value (where applicable)\n";
  // "%e : X axis range end value (where applicable)\n";
  //
  // "%I : TIC intensity after TIC integration over a graph range\n";

  QHash<QChar, QString> replacements;

  replacements['f'] = file_name;
  replacements['s'] = sample_name;

  // This is where the thing becomes tricky: depending on what action the user
  // has performed with their mouse, we need to craft a stanza that is
  // most meaningful.

  // If the user has not dragged the mouse without even cliking it,
  // or has only clicked, all we know is the last lastCursorHoveredPoint
  // otherwise it is the start drag point.

  // Recall an important concept:

  // context.m_isMouseDragging means that the user has clicked the space
  // during a mouse dragging operation (no mouse button release yet)

  // context.m_wasMouseDragging means that the user has terminated
  // the mouse dragging operation by releasing the mouse button.

  // This means that m_wasMouseDragging can be true and m_isMouseDragging false.
  // However, because of the way the context is captured, ,for the purpose
  // of the current stanza creation, both context situations are valid,
  // so we need to || or && them in the logical conditions below

  ///////////////// THE X and Y and Z values /////////////////////
  double x_value = 0;

  if(!context.m_isMouseDragging && !context.m_wasMouseDragging)
    // The X value is the last hovered point x value
    x_value = context.m_lastCursorHoveredPoint.x();
  else
    // The X value is now the drag start point x value
    x_value = context.m_startDragPoint.x();

  // We can now craft the other values, based on that x_value.

  replacements['X'] = QString("%1").arg(x_value, 0, 'f', 6);
  replacements['Y'] =
    QString("%1").arg(context.m_startDragPoint.y(), 0, 'f', 6);
  replacements['Z'] = QString("%1").arg(
    static_cast<pappso::BaseTracePlotWidget *>(mp_plotWidget)
      ->getYatX(x_value, dynamic_cast<QCPGraph *>(plottable_p)),
    0,
    'g',
    6);

  ///////////////// THE x and y /////////////////////

  // These values only apply if there has been a mouse dragging event.
  if(context.m_isMouseDragging || context.m_wasMouseDragging)
    {
      double x_delta_value = context.m_xDelta;
      double y_delta_value = context.m_yDelta;

      replacements['x'] = QString("%1").arg(x_delta_value, 0, 'f', 6);
      replacements['y'] = QString("%1").arg(y_delta_value, 0, 'f', 6);
      replacements['z'] = QString("%1").arg(
        static_cast<pappso::BaseTracePlotWidget *>(mp_plotWidget)
          ->getYatX(x_delta_value, dynamic_cast<QCPGraph *>(plottable_p)),
        0,
        'g',
        6);
    }
  else
    {
      replacements['x'] = "n/a";
      replacements['y'] = "n/a";
    }

  ///////////////// THE range b and e /////////////////////

  // These values only apply if there has been a mouse dragging event.
  if(context.m_isMouseDragging || context.m_wasMouseDragging)
    {
      replacements['b'] =
        QString("%1").arg(context.m_xRegionRangeStart, 0, 'f', 4);
      replacements['e'] =
        QString("%1").arg(context.m_xRegionRangeEnd, 0, 'f', 4);
    }
  else
    {
      replacements['b'] = "n/a";
      replacements['e'] = "n/a";
    }

  //////////////// The computed intensity value /////////////////

  // This value is only available when the user has explicitely used
  // the Intensity integration. In that case, the m_intensity value
  // is valid.

  if(!qIsNaN(m_lastTicIntensity))
    {
      replacements['I'] =
        QString("%1").arg(static_cast<int>(m_lastTicIntensity));
    }
  else
    {
      replacements['I'] = "n/a";
    }

  QString stanza;
  stanza.reserve(formatString.size());

  int lastIndex = 0;
  auto it       = re.globalMatch(formatString);

  while(it.hasNext())
    {
      QRegularExpressionMatch m = it.next();

      int start = m.capturedStart();
      int end   = m.capturedEnd();

      // Copy text up to this match
      stanza += formatString.mid(lastIndex, start - lastIndex);

      const QChar key = m.captured(1)[0];

      qDebug() << "Captured key: " << key;

      if(replacements.contains(key))
        {
          QString round_replacement = replacements[key];
          qDebug() << "Adding new replacement string element:"
                   << round_replacement;
          stanza += replacements[key];
        }
      else
        {
          // Unknown token -> leave as-is
          QString untouched = m.captured(0);
          qDebug() << "Adding new untouched string element:" << untouched;
          stanza += untouched;
        }

      lastIndex = end;
    }

  // Copy trailing part
  QString trailing_string = formatString.mid(lastIndex);
  qDebug() << "Adding the trailing string:" << trailing_string;
  stanza += trailing_string;

  // Finally, set the stanza to the member datum and return it.
  // We want to keep it as a member datum, because derived classes
  // can build upon it with specialized data elements.
  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);


  // Now set the name of the sample to the trace label using the right
  // color.

  QPalette palette;
  palette.setColor(QPalette::WindowText, color);
  m_ui->sampleNameLabel->setPalette(palette);
}

void
BasePlotCompositeWidget::setSampleNameColor(const QColor &color)
{
  if(!color.isValid())
    qFatal() << "The color is invalid!";

  QPalette palette;
  palette.setColor(QPalette::WindowText, color);
  m_ui->sampleNameLabel->setPalette(palette);
}


} // namespace MineXpert

} // namespace MsXpS
