/* This code comes right from the msXpertSuite software project.
 *
 * msXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright(C) 2009,...,2018 Filippo Rusconi
 *
 * http://www.msxpertsuite.org
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 * END software license
 */


/////////////////////// StdLib includes
#include <vector>


/////////////////////// Qt includes
#include <QVector>


/////////////////////// Local includes
#include "baseplotwidget.h"
#include "../../pappsoexception.h"
#include "../../exception/exceptionnotpossible.h"


int basePlotContextMetaTypeId =
  qRegisterMetaType<pappso::BasePlotContext>("pappso::BasePlotContext");
int basePlotContextPtrMetaTypeId =
  qRegisterMetaType<pappso::BasePlotContext *>("pappso::BasePlotContext *");


namespace pappso
{

QString
BasePlotContext::toString() const
{
  QString text("Context:");

  text += QString("data kind: %1").arg(static_cast<int>(dataKind));

  text += QString(" isMouseDragging: %1 -- wasMouseDragging: %2")
            .arg(isMouseDragging ? "true" : "false")
            .arg(wasMouseDragging ? "true" : "false");

  text += QString(" -- startDragPoint : (%1, %2)")
            .arg(startDragPoint.x())
            .arg(startDragPoint.y());

  text += QString(" -- currentDragPoint : (%1, %2)")
            .arg(currentDragPoint.x())
            .arg(currentDragPoint.y());

  text += QString(" -- lastCursorHoveredPoint : (%1, %2)")
            .arg(lastCursorHoveredPoint.x())
            .arg(lastCursorHoveredPoint.y());

  text += QString(" -- xRange: (%1, %2)").arg(xRange.lower).arg(xRange.upper);

  text += QString(" -- yRange: (%1, %2)").arg(yRange.lower).arg(yRange.upper);

  text +=
    QString(" -- wasClickOnXAxis: %1").arg(wasClickOnXAxis ? "true" : "false");
  text +=
    QString(" -- wasClickOnYAxis: %1").arg(wasClickOnYAxis ? "true" : "false");
  text += QString(" -- isMeasuringDistance: %1")
            .arg(isMeasuringDistance ? "true" : "false");

  text += QString(" -- xRegionRangeStart: %1 -- xRegionRangeEnd: %2")
            .arg(xRegionRangeStart)
            .arg(xRegionRangeEnd);

  text += QString(" -- yRegionRangeStart: %1 -- yRegionRangeEnd: %2")
            .arg(yRegionRangeStart)
            .arg(yRegionRangeEnd);

  text += QString(" -- xDelta: %1 -- yDelta: %2").arg(xDelta).arg(yDelta);

  text += QString(" -- pressedKeyCode: %1").arg(pressedKeyCode);

  text += QString(" -- keyboardModifiers: %1").arg(keyboardModifiers);

  text += QString(" -- lastPressedMouseButton: %1").arg(lastPressedMouseButton);

  text +=
    QString(" -- lastReleasedMouseButton: %1").arg(lastReleasedMouseButton);

  text += QString(" -- pressedMouseButtons: %1").arg(pressedMouseButtons);

  text +=
    QString(" -- mouseButtonsAtMousePress: %1").arg(mouseButtonsAtMousePress);

  text += QString(" -- mouseButtonsAtMouseRelease: %1")
            .arg(mouseButtonsAtMouseRelease);

  return text;
}


BasePlotWidget::BasePlotWidget(QWidget *parent) : QCustomPlot(parent)
{
  // qDebug();

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

  // Default settings for the pen used to graph the data.
  m_pen.setStyle(Qt::SolidLine);
  m_pen.setBrush(Qt::black);
  m_pen.setWidth(1);

  if(!setupWidget())
    qFatal("Programming error.");

  show();
}


BasePlotWidget::BasePlotWidget(QWidget *parent,
                               const QString &x_axis_label,
                               const QString &y_axis_label)
  : QCustomPlot(parent), m_axisLabelX(x_axis_label), m_axisLabelY(y_axis_label)
{
  // qDebug();

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

  // Default settings for the pen used to graph the data.
  m_pen.setStyle(Qt::SolidLine);
  m_pen.setBrush(Qt::black);
  m_pen.setWidth(1);

  xAxis->setLabel(x_axis_label);
  yAxis->setLabel(y_axis_label);

  if(!setupWidget())
    qFatal("Programming error.");

  show();
}


//! Destruct \c this BasePlotWidget instance.
/*!

  The destruction involves clearing the history, deleting all the axis range
  history items for x and y axes.

*/
BasePlotWidget::~BasePlotWidget()
{
  // qDebug() << "In the destructor of plot widget:" << this;

  m_xAxisRangeHistory.clear();
  m_yAxisRangeHistory.clear();
}


bool
BasePlotWidget::setupWidget()
{
  // qDebug();

  // By default the widget comes with a graph. Remove it.

  if(graphCount())
    removeGraph(0);

  // This is required so that we get the keyboard events.
  setFocusPolicy(Qt::StrongFocus);
  setInteractions(QCP::iRangeZoom | QCP::iSelectPlottables | QCP::iMultiSelect);

  // Make a copy of the pen to just change its color and set that color to
  // the tracer line.
  QPen pen = m_pen;

  // Create the lines that will act as tracers for position and selection of
  // regions.
  //
  // We have the cross hair that serves as the cursor. That crosshair cursor is
  // made of a vertical line (green, because when click-dragging the mouse it
  // becomes the tracer that is being anchored at the region start. The second
  // line i horizontal and is always black.

  pen.setColor(QColor("black"));

  // The set of tracers (horizontal and vertical) that track the position of the
  // mouse cursor.
  mp_vPosTracerItem = new QCPItemLine(this);
  mp_vPosTracerItem->setPen(m_pen);
  mp_vPosTracerItem->start->setType(QCPItemPosition::ptPlotCoords);
  mp_vPosTracerItem->end->setType(QCPItemPosition::ptPlotCoords);
  mp_vPosTracerItem->start->setCoords(0, 0);
  mp_vPosTracerItem->end->setCoords(0, 0);

  mp_hPosTracerItem = new QCPItemLine(this);
  mp_hPosTracerItem->setPen(m_pen);
  mp_hPosTracerItem->start->setType(QCPItemPosition::ptPlotCoords);
  mp_hPosTracerItem->end->setType(QCPItemPosition::ptPlotCoords);
  mp_hPosTracerItem->start->setCoords(0, 0);
  mp_hPosTracerItem->end->setCoords(0, 0);

  // The set of tracers (horizontal only) that track the region
  // spanning/selection regions.
  //
  // The start vertical tracer is colored in greeen.
  pen.setColor(QColor("green"));

  mp_vStartTracerItem = new QCPItemLine(this);
  mp_vStartTracerItem->setPen(pen);
  mp_vStartTracerItem->start->setType(QCPItemPosition::ptPlotCoords);
  mp_vStartTracerItem->end->setType(QCPItemPosition::ptPlotCoords);
  mp_vStartTracerItem->start->setCoords(0, 0);
  mp_vStartTracerItem->end->setCoords(0, 0);

  // The end vertical tracer is colored in red.
  pen.setColor(QColor("red"));

  mp_vEndTracerItem = new QCPItemLine(this);
  mp_vEndTracerItem->setPen(pen);
  mp_vEndTracerItem->start->setType(QCPItemPosition::ptPlotCoords);
  mp_vEndTracerItem->end->setType(QCPItemPosition::ptPlotCoords);
  mp_vEndTracerItem->start->setCoords(0, 0);
  mp_vEndTracerItem->end->setCoords(0, 0);

  mp_zoomRectItem = new QCPItemRect(this);
  mp_zoomRectItem->setPen(m_pen);
  mp_zoomRectItem->topLeft->setType(QCPItemPosition::ptPlotCoords);
  mp_zoomRectItem->bottomRight->setType(QCPItemPosition::ptPlotCoords);
  mp_zoomRectItem->setVisible(false);

  mp_selectLineItem = new QCPItemLine(this);
  mp_selectLineItem->setPen(m_pen);
  mp_selectLineItem->start->setType(QCPItemPosition::ptPlotCoords);
  mp_selectLineItem->end->setType(QCPItemPosition::ptPlotCoords);
  mp_selectLineItem->setVisible(false);

  // When the user click-drags the mouse, the X distance between the drag start
  // point and the drag end point (current point) is the xDelta.
  mp_xDeltaTextItem = new QCPItemText(this);
  mp_xDeltaTextItem->setPositionAlignment(Qt::AlignBottom | Qt::AlignCenter);
  mp_xDeltaTextItem->position->setType(QCPItemPosition::ptPlotCoords);
  mp_xDeltaTextItem->setVisible(false);

  // We want to capture the signals emitted by the QCustomPlot base class.
  connect(
    this, &QCustomPlot::mouseMove, this, &BasePlotWidget::mouseMoveHandler);

  connect(
    this, &QCustomPlot::mousePress, this, &BasePlotWidget::mousePressHandler);

  connect(this,
          &QCustomPlot::mouseRelease,
          this,
          &BasePlotWidget::mouseReleaseHandler);

  connect(this,
          &QCustomPlot::axisDoubleClick,
          this,
          &BasePlotWidget::axisDoubleClickHandler);

  return true;
}


void
BasePlotWidget::setPen(const QPen &pen)
{
  m_pen = pen;
}


const QPen &
BasePlotWidget::getPen() const
{
  return m_pen;
}


void
BasePlotWidget::setPlottingColor(QCPAbstractPlottable *plottable_p,
                                 const QColor &new_color)
{
  if(plottable_p == nullptr)
    qFatal("Pointer cannot be nullptr.");

  // First this single-graph widget
  QPen pen;

  pen = plottable_p->pen();
  pen.setColor(new_color);
  plottable_p->setPen(pen);

  replot();
}


void
BasePlotWidget::setPlottingColor(int index, const QColor &new_color)
{
  if(!new_color.isValid())
    return;

  QCPGraph *graph_p = graph(index);

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

  return setPlottingColor(graph_p, new_color);
}


QColor
BasePlotWidget::getPlottingColor(QCPAbstractPlottable *plottable_p) const
{
  if(plottable_p == nullptr)
    qFatal("Programming error.");

  return plottable_p->pen().color();
}


QColor
BasePlotWidget::getPlottingColor(int index) const
{
  QCPGraph *graph_p = graph(index);

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

  return getPlottingColor(graph_p);
}


void
BasePlotWidget::setAxisLabelX(const QString &label)
{
  xAxis->setLabel(label);
}


void
BasePlotWidget::setAxisLabelY(const QString &label)
{
  yAxis->setLabel(label);
}


// AXES RANGE HISTORY-related functions
void
BasePlotWidget::resetAxesRangeHistory()
{
  m_xAxisRangeHistory.clear();
  m_yAxisRangeHistory.clear();

  m_xAxisRangeHistory.push_back(new QCPRange(xAxis->range()));
  m_yAxisRangeHistory.push_back(new QCPRange(yAxis->range()));

  // qDebug() << "size of history:" << m_xAxisRangeHistory.size()
  //<< "setting index to 0";

  // qDebug() << "resetting axes history to values:" << xAxis->range().lower
  //<< "--" << xAxis->range().upper << "and" << yAxis->range().lower
  //<< "--" << yAxis->range().upper;

  m_lastAxisRangeHistoryIndex = 0;
}


//! Create new axis range history items and append them to the history.
/*!

  The plot widget is queried to get the current x/y-axis ranges and the
  current ranges are appended to the history for x-axis and for y-axis.

*/
void
BasePlotWidget::updateAxesRangeHistory()
{
  m_xAxisRangeHistory.push_back(new QCPRange(xAxis->range()));
  m_yAxisRangeHistory.push_back(new QCPRange(yAxis->range()));

  m_lastAxisRangeHistoryIndex = m_xAxisRangeHistory.size() - 1;

  // qDebug() << "axes history size:" << m_xAxisRangeHistory.size()
  //<< "current index:" << m_lastAxisRangeHistoryIndex
  //<< xAxis->range().lower << "--" << xAxis->range().upper
  //<< "and"
  //<< yAxis->range().lower << "--" << yAxis->range().upper;
}


//! Go up one history element in the axis history.
/*!

  If possible, back up one history item in the axis histories and update the
  plot's x/y-axis ranges to match that history item.

*/
void
BasePlotWidget::restorePreviousAxesRangeHistory()
{
  // qDebug() << "axes history size:" << m_xAxisRangeHistory.size()
  //<< "current index:" << m_lastAxisRangeHistoryIndex;

  if(m_lastAxisRangeHistoryIndex == 0)
    {
      // qDebug() << "current index is 0 returning doing nothing";

      return;
    }

  // qDebug() << "setting index to:" << m_lastAxisRangeHistoryIndex - 1
  //<< "and restoring axes history to that index";

  restoreAxesRangeHistory(--m_lastAxisRangeHistoryIndex);
}


//! Get the axis histories at index \p index and update the plot ranges.
/*!

  \param index index at which to select the axis history item.

  \sa updateAxesRangeHistory().

*/
void
BasePlotWidget::restoreAxesRangeHistory(std::size_t index)
{
  // qDebug() << "axes history size:" << m_xAxisRangeHistory.size()
  //<< "current index:" << m_lastAxisRangeHistoryIndex
  //<< "asking to restore index:" << index;

  if(index >= m_xAxisRangeHistory.size())
    {
      // qDebug() << "index >= history size. Returning.";
      return;
    }

  xAxis->setRange(*(m_xAxisRangeHistory.at(index)));
  yAxis->setRange(*(m_yAxisRangeHistory.at(index)));

  hideAllPlotItems();

  mp_vPosTracerItem->setVisible(false);
  mp_hPosTracerItem->setVisible(false);

  mp_vStartTracerItem->setVisible(false);
  mp_vEndTracerItem->setVisible(false);


  // The start trace will keep beeing represented at the last position and last
  // size even if we call this function repetitively. So actually do not show,
  // it will reappare as soon as the mouse is moved.
  // if(m_shouldTracersBeVisible)
  //{
  // mp_vStartTracerItem->setVisible(true);
  //}

  replot();

  updateContextRanges();

  // qDebug() << "restored axes history to index:" << index
  //<< "with values:" << xAxis->range().lower << "--"
  //<< xAxis->range().upper << "and" << yAxis->range().lower << "--"
  //<< yAxis->range().upper;

  emit plotRangesChangedSignal(m_context);
}
// AXES RANGE HISTORY-related functions


/// KEYBOARD-related EVENTS
void
BasePlotWidget::keyPressEvent(QKeyEvent *event)
{
  // qDebug() << "ENTER";

  // We need this because some keys modify our behaviour.
  m_context.pressedKeyCode    = event->key();
  m_context.keyboardModifiers = QGuiApplication::queryKeyboardModifiers();

  if(event->key() == Qt::Key_Left || event->key() == Qt::Key_Right ||
     event->key() == Qt::Key_Up || event->key() == Qt::Key_Down)
    {
      return directionKeyPressEvent(event);
    }
  else if(event->key() == m_leftMousePseudoButtonKey ||
          event->key() == m_rightMousePseudoButtonKey)
    {
      return mousePseudoButtonKeyPressEvent(event);
    }

  // Do not do anything here, because this function is used by derived classes
  // that will emit the signal below. Otherwise there are going to be multiple
  // signals sent.
  // qDebug() << "Going to emit keyPressEventSignal(m_context);";
  // emit keyPressEventSignal(m_context);
}


//! Handle specific key codes and trigger respective actions.
void
BasePlotWidget::keyReleaseEvent(QKeyEvent *event)
{
  m_context.releasedKeyCode = event->key();

  // The keyboard key is being released, set the key code to 0.
  m_context.pressedKeyCode    = 0;
  m_context.keyboardModifiers = QGuiApplication::queryKeyboardModifiers();

  // Now test if the key that was released is one of the housekeeping keys.
  if(event->key() == Qt::Key_Backspace)
    {
      // The user wants to iterate back in the x/y axis range history.
      restorePreviousAxesRangeHistory();

      event->accept();
    }
  else if(event->key() == Qt::Key_Space)
    {
      return spaceKeyReleaseEvent(event);
    }
  else if(event->key() == Qt::Key_Delete)
    {
      // The user wants to delete a graph. What graph is to be determined
      // programmatically:

      // If there is a single graph, then that is the graph to be removed.
      // If there are more than one graph, then only the ones that are selected
      // are to be removed.

      // Note that the user of this widget might want to provide the user with
      // the ability to specify if all the children graph needs to be removed
      // also. This can be coded in key modifiers. So provide the context.

      int graph_count = plottableCount();

      if(!graph_count)
        {
          // qDebug() << "Not a single graph in the plot widget. Doing
          // nothing.";

          event->accept();
          return;
        }

      if(graph_count == 1)
        {
          // qDebug() << "A single graph is in the plot widget. Emitting a graph
          // " "destruction requested signal for it:"
          //<< graph();

          emit plottableDestructionRequestedSignal(this, graph(), m_context);
        }
      else
        {
          // At this point we know there are more than one graph in the plot
          // widget. We need to get the selected one (if any).
          QList<QCPGraph *> selected_graph_list;

          selected_graph_list = selectedGraphs();

          if(!selected_graph_list.size())
            {
              event->accept();
              return;
            }

          // qDebug() << "Number of selected graphs to be destrobyed:"
          //<< selected_graph_list.size();

          for(int iter = 0; iter < selected_graph_list.size(); ++iter)
            {
              // qDebug()
              //<< "Emitting a graph destruction requested signal for graph:"
              //<< selected_graph_list.at(iter);

              emit plottableDestructionRequestedSignal(
                this, selected_graph_list.at(iter), m_context);

              // We do not do this, because we want the slot called by the
              // signal above to handle that removal. Remember that it is not
              // possible to delete graphs manually.
              //
              // removeGraph(selected_graph_list.at(iter));
            }
          event->accept();
        }
    }
  // End of
  // else if(event->key() == Qt::Key_Delete)
  else if(event->key() == Qt::Key_T)
    {
      // The user wants to toggle the visibiity of the tracers.
      m_shouldTracersBeVisible = !m_shouldTracersBeVisible;

      if(!m_shouldTracersBeVisible)
        hideTracers();
      else
        showTracers();

      event->accept();
    }
  else if(event->key() == Qt::Key_Left || event->key() == Qt::Key_Right ||
          event->key() == Qt::Key_Up || event->key() == Qt::Key_Down)
    {
      return directionKeyReleaseEvent(event);
    }
  else if(event->key() == m_leftMousePseudoButtonKey ||
          event->key() == m_rightMousePseudoButtonKey)
    {
      return mousePseudoButtonKeyReleaseEvent(event);
    }

  // At this point emit the signal, since we did not treat it. Maybe the
  // consumer widget wants to know that the keyboard key was released.

  emit keyReleaseEventSignal(m_context);
}


void
BasePlotWidget::spaceKeyReleaseEvent([[maybe_unused]] QKeyEvent *event)
{
  // qDebug();
}


void
BasePlotWidget::directionKeyPressEvent(QKeyEvent *event)
{
  // qDebug() << "event key:" << event->key();

  // The user is trying to move the positional cursor/markers. There are
  // multiple way they can do that:
  //
  // 1.a. Hitting the arrow left/right keys alone will search for next pixel.
  // 1.b. Hitting the arrow left/right keys with Alt modifier will search for a
  // multiple of pixels that might be equivalent to one 20th of the pixel width
  // of the plot widget.
  // 1.c Hitting the left/right keys with Alt and Shift modifiers will search
  // for a multiple of pixels that might be the equivalent to half of the pixel
  // width.
  //
  // 2. Hitting the Control modifier will move the cursor to the next data point
  // of the graph.

  int pixel_increment = 0;

  if(m_context.keyboardModifiers == Qt::NoModifier)
    pixel_increment = 1;
  else if(m_context.keyboardModifiers == Qt::AltModifier)
    pixel_increment = 50;

  // The user is moving the positional markers. This is equivalent to a
  // non-dragging cursor movement to the next pixel. Note that the origin is
  // located at the top left, so key down increments and key up decrements.

  if(event->key() == Qt::Key_Left)
    horizontalMoveMouseCursorCountPixels(-pixel_increment);
  else if(event->key() == Qt::Key_Right)
    horizontalMoveMouseCursorCountPixels(pixel_increment);
  else if(event->key() == Qt::Key_Up)
    verticalMoveMouseCursorCountPixels(-pixel_increment);
  else if(event->key() == Qt::Key_Down)
    verticalMoveMouseCursorCountPixels(pixel_increment);

  event->accept();
}


void
BasePlotWidget::directionKeyReleaseEvent(QKeyEvent *event)
{
  // qDebug() << "event key:" << event->key();
  event->accept();
}


void
BasePlotWidget::mousePseudoButtonKeyPressEvent([
  [maybe_unused]] QKeyEvent *event)
{
  // qDebug();
}


void
BasePlotWidget::mousePseudoButtonKeyReleaseEvent(QKeyEvent *event)
{

  QPointF pixel_coordinates(
    xAxis->coordToPixel(m_context.lastCursorHoveredPoint.x()),
    yAxis->coordToPixel(m_context.lastCursorHoveredPoint.y()));

  Qt::MouseButton button    = Qt::NoButton;
  QEvent::Type q_event_type = QEvent::MouseButtonPress;

  if(event->key() == m_leftMousePseudoButtonKey)
    {
      // Toggles the left mouse button on/off

      button = Qt::LeftButton;

      m_context.isLeftPseudoButtonKeyPressed =
        !m_context.isLeftPseudoButtonKeyPressed;

      if(m_context.isLeftPseudoButtonKeyPressed)
        q_event_type = QEvent::MouseButtonPress;
      else
        q_event_type = QEvent::MouseButtonRelease;
    }
  else if(event->key() == m_rightMousePseudoButtonKey)
    {
      // Toggles the right mouse button.

      button = Qt::RightButton;

      m_context.isRightPseudoButtonKeyPressed =
        !m_context.isRightPseudoButtonKeyPressed;

      if(m_context.isRightPseudoButtonKeyPressed)
        q_event_type = QEvent::MouseButtonPress;
      else
        q_event_type = QEvent::MouseButtonRelease;
    }

  // qDebug() << "pressed/released pseudo button:" << button
  //<< "q_event_type:" << q_event_type;

  // Synthesize a QMouseEvent and use it.

  QMouseEvent *mouse_event_p =
    new QMouseEvent(q_event_type,
                    pixel_coordinates,
                    mapToGlobal(pixel_coordinates.toPoint()),
                    mapToGlobal(pixel_coordinates.toPoint()),
                    button,
                    button,
                    m_context.keyboardModifiers,
                    Qt::MouseEventSynthesizedByApplication);

  if(q_event_type == QEvent::MouseButtonPress)
    mousePressHandler(mouse_event_p);
  else
    mouseReleaseHandler(mouse_event_p);

  // event->accept();
}
/// KEYBOARD-related EVENTS


/// MOUSE-related EVENTS

void
BasePlotWidget::mousePressHandler(QMouseEvent *event)
{
  // When the user clicks this widget it has to take focus.
  setFocus();

  QPointF mousePoint = event->localPos();

  m_context.lastPressedMouseButton   = event->button();
  m_context.mouseButtonsAtMousePress = event->buttons();

  // The pressedMouseButtons must continually inform on the status of pressed
  // buttons so add the pressed button.
  m_context.pressedMouseButtons |= event->button();

  // qDebug().noquote() << m_context.toString();

  // In all the processing of the events, we need to know if the user is
  // clicking somewhere with the intent to change the plot ranges (reframing
  // or rescaling the plot).
  //
  // Reframing the plot means that the new x and y axes ranges are modified so
  // that they match the region that the user has encompassed by left clicking
  // the mouse and dragging it over the plot. That is we reframe the plot so
  // that it contains only the "selected" region.
  //
  // Rescaling the plot means the the new x|y axis range is modified such that
  // the lower axis range is constant and the upper axis range is moved either
  // left or right by the same amont as the x|y delta encompassed by the user
  // moving the mouse. The axis is thus either compressed (mouse movement is
  // leftwards) or un-compressed (mouse movement is rightwards).

  // There are two ways to perform axis range modifications:
  //
  // 1. By clicking on any of the axes
  // 2. By clicking on the plot region but using keyboard key modifiers, like
  // Alt and Ctrl.
  //
  // We need to know both cases separately which is why we need to perform a
  // number of tests below.

  // Let's check if the click is on the axes, either X or Y, because that
  // will allow us to take proper actions.

  if(isClickOntoXAxis(mousePoint))
    {
      // The X axis was clicked upon, we need to document that:
      // qDebug() << __FILE__ << __LINE__
      //<< "Layout element is axisRect and actually on an X axis part.";

      m_context.wasClickOnXAxis = true;

      // int currentInteractions = interactions();
      // currentInteractions |= QCP::iRangeDrag;
      // setInteractions((QCP::Interaction)currentInteractions);
      // axisRect()->setRangeDrag(xAxis->orientation());
    }
  else
    m_context.wasClickOnXAxis = false;

  if(isClickOntoYAxis(mousePoint))
    {
      // The Y axis was clicked upon, we need to document that:
      // qDebug() << __FILE__ << __LINE__
      //<< "Layout element is axisRect and actually on an Y axis part.";

      m_context.wasClickOnYAxis = true;

      // int currentInteractions = interactions();
      // currentInteractions |= QCP::iRangeDrag;
      // setInteractions((QCP::Interaction)currentInteractions);
      // axisRect()->setRangeDrag(yAxis->orientation());
    }
  else
    m_context.wasClickOnYAxis = false;

  // At this point, let's see if we need to remove the QCP::iRangeDrag bit:

  if(!m_context.wasClickOnXAxis && !m_context.wasClickOnYAxis)
    {
      // qDebug() << __FILE__ << __LINE__
      // << "Click outside of axes.";

      // int currentInteractions = interactions();
      // currentInteractions     = currentInteractions & ~QCP::iRangeDrag;
      // setInteractions((QCP::Interaction)currentInteractions);
    }

  m_context.startDragPoint.setX(xAxis->pixelToCoord(mousePoint.x()));
  m_context.startDragPoint.setY(yAxis->pixelToCoord(mousePoint.y()));

  // Now install the vertical start tracer at the last cursor hovered
  // position.
  if(m_shouldTracersBeVisible)
    mp_vStartTracerItem->setVisible(true);

  mp_vStartTracerItem->start->setCoords(m_context.lastCursorHoveredPoint.x(),
                                        yAxis->range().upper);
  mp_vStartTracerItem->end->setCoords(m_context.lastCursorHoveredPoint.x(),
                                      yAxis->range().lower);

  replot();
}


void
BasePlotWidget::mouseReleaseHandler(QMouseEvent *event)
{
  m_context.lastReleasedMouseButton = event->button();

  // The event->buttons() is the description of the buttons that are pressed at
  // the moment the handler is invoked, that is now. If left and right were
  // pressed, and left was released, event->buttons() would be right.
  m_context.mouseButtonsAtMouseRelease = event->buttons();

  // The pressedMouseButtons must continually inform on the status of pressed
  // buttons so remove the released button.
  m_context.pressedMouseButtons ^= event->button();

  // qDebug().noquote() << m_context.toString();

  // We'll need to know if modifiers were pressed a the moment the user
  // released the mouse button.
  m_context.keyboardModifiers = QGuiApplication::keyboardModifiers();

  if(!m_context.isMouseDragging)
    {
      // Let the user know that the mouse was *not* being dragged.
      m_context.wasMouseDragging = false;

      event->accept();

      return;
    }

  // Let the user know that the mouse was being dragged.
  m_context.wasMouseDragging = true;

  // We cannot hide all items in one go because we rely on their visibility
  // to know what kind of dragging operation we need to perform (line-only
  // X-based zoom or rectangle-based X- and Y-based zoom, for example). The
  // only thing we know is that we can make the text invisible.

  // We would not want to still see the selection line item.
  mp_selectLineItem->setVisible(false);

  // Same for the x delta text item
  mp_xDeltaTextItem->setVisible(false);

  // We do not show the end vertical region range marker.
  mp_vEndTracerItem->setVisible(false);

  // Horizontal position tracer.
  mp_hPosTracerItem->setVisible(true);
  mp_hPosTracerItem->start->setCoords(xAxis->range().lower,
                                      m_context.lastCursorHoveredPoint.y());
  mp_hPosTracerItem->end->setCoords(xAxis->range().upper,
                                    m_context.lastCursorHoveredPoint.y());

  // Vertical position tracer.
  mp_vPosTracerItem->setVisible(true);

  mp_vPosTracerItem->setVisible(true);
  mp_vPosTracerItem->start->setCoords(m_context.lastCursorHoveredPoint.x(),
                                      yAxis->range().upper);
  mp_vPosTracerItem->end->setCoords(m_context.lastCursorHoveredPoint.x(),
                                    yAxis->range().lower);

  // Force replot now because later that call might not be performed.
  replot();

  // If we were using the "quantum" display for the rescale of the axes
  // using the Ctrl-modified left button click drag in the axes, then reset
  // the count to 0.
  m_mouseMoveHandlerSkipCount = 0;

  // Compute the delta values, X and Y, that correspond to the movement that
  // was done by the user while pressing the mouse button, that is get the
  // geometry of the drag movement.

  calculateDragDeltasAndUnSortedRegionCorners();

  // Now that we have computed the useful ranges, we need to check what to do
  // depending on the button that was pressed.

  if(m_context.lastReleasedMouseButton == Qt::LeftButton)
    {
      return mouseReleaseHandlerLeftButton();
    }
  else if(m_context.lastReleasedMouseButton == Qt::RightButton)
    {
      return mouseReleaseHandlerRightButton();
    }


  // By definition we are stopping the drag operation by releasing the mouse
  // button. Whatever that mouse button was pressed before and if there was
  // one pressed before.  We cannot set that boolean value to false before
  // this place, because we call a number of routines above that need to know
  // that dragging was occurring. Like mouseReleaseHandledEvent(event) for
  // example.

  m_context.isMouseDragging = false;

  event->accept();

  return;
}


void
BasePlotWidget::mouseReleaseHandlerLeftButton()
{

  if(m_context.wasClickOnXAxis || m_context.wasClickOnYAxis)
    {

      // When the mouse move handler pans the plot, we cannot store each axes
      // range history element that would mean store a huge amount of such
      // elements, as many element as there are mouse move event handled by
      // the Qt event queue. But we can store an axis range history element
      // for the last situation of the mouse move: when the button is
      // released:

      updateAxesRangeHistory();

      emit plotRangesChangedSignal(m_context);

      replot();

      // Nothing else to do.
      return;
    }

  // Start handling the zoom rectangle so that we can skip any keyboard
  // modified, like Ctrl that would not mean anything anyway.
  if(mp_zoomRectItem->visible())
    {

      // If we were dragging with the left button pressed and could draw a
      // rectangle, then we were preparing a zoom operation. Let's bring
      // that operation to an end.

      mp_zoomRectItem->setVisible(false);

      axisZoom();

      return;
    }

  // If the user started by clicking in the plot region, dragged the mouse
  // cursor with the left button and pressed the Ctrl modifier, then that
  // means that they wanted to do a rescale over the x-axis in the form of a
  // reframing.

  if(m_context.keyboardModifiers & Qt::ControlModifier)
    {
      return axisReframe();
    }
}


void
BasePlotWidget::mouseReleaseHandlerRightButton()
{

  // The right button is used for the integrations. Not for axis range
  // operations. So all we have to do is remove the various graphics items and
  // send a signal with the context that contains all the data required by the
  // user to perform the integrations over the right plot regions.

  // Whatever we were doing we need to make the selection line invisible:

  if(mp_selectLineItem->visible())
    mp_selectLineItem->setVisible(false);

  if(mp_xDeltaTextItem->visible())
    mp_xDeltaTextItem->setVisible(false);

  // Also make the vertical end tracer invisible.
  mp_vEndTracerItem->setVisible(false);

  // Force replot now because later that call might not be performed.
  replot();

  // Note that we only request an integration if the x-axis delta is enough.

  double x_delta_pixel =
    fabs(xAxis->coordToPixel(m_context.currentDragPoint.x()) -
         xAxis->coordToPixel(m_context.startDragPoint.x()));

  if(x_delta_pixel > 3)
    emit integrationRequestedSignal(m_context);
  // else
  // qDebug() << "Not asking for integration.";
}


void
BasePlotWidget::mouseMoveHandler(QMouseEvent *event)
{

  // If we have no focus, then get it. See setFocus() to understand why asking
  // for focus is cosly and thus why we want to make this decision first.
  if(!hasFocus())
    setFocus();

  // The event->button() must be by Qt instructions considered to be 0.

  // Whatever happens, we want to store the plot coordinates of the current
  // mouse cursor position (will be useful later for countless needs).

  QPointF mousePoint = event->localPos();

  // qDebug() << "local mousePoint position in pixels:" << mousePoint;

  m_context.lastCursorHoveredPoint.setX(xAxis->pixelToCoord(mousePoint.x()));
  m_context.lastCursorHoveredPoint.setY(yAxis->pixelToCoord(mousePoint.y()));

  // qDebug() << "lastCursorHoveredPoint coord:"
  //<< m_context.lastCursorHoveredPoint;

  // Now, depending on the button(s) (if any) that are pressed or not, we have
  // a different processing.

  if(m_context.pressedMouseButtons & Qt::LeftButton ||
     m_context.pressedMouseButtons & Qt::RightButton)
    mouseMoveHandlerDraggingCursor();
  else
    mouseMoveHandlerNotDraggingCursor();

  event->accept();
}


void
BasePlotWidget::mouseMoveHandlerNotDraggingCursor()
{

  m_context.isMouseDragging = false;

  // We are not dragging the mouse (no button pressed), simply let this
  // widget's consumer know the position of the cursor and update the markers.

  emit lastCursorHoveredPointSignal(m_context.lastCursorHoveredPoint);

  // We are not dragging, so we do not show the region end tracer we only show
  // the anchoring start trace that might be of use if the user starts using
  // the arrow keys to move the cursor.
  mp_vEndTracerItem->setVisible(false);

  // Only bother with the tracers if the user wants them to be visible. Their
  // crossing point must be exactly at the last cursor-hovered point.

  if(m_shouldTracersBeVisible)
    {
      // We are not dragging, so only show the position markers (v and h);

      // Horizontal position tracer.
      mp_hPosTracerItem->setVisible(true);
      mp_hPosTracerItem->start->setCoords(xAxis->range().lower,
                                          m_context.lastCursorHoveredPoint.y());
      mp_hPosTracerItem->end->setCoords(xAxis->range().upper,
                                        m_context.lastCursorHoveredPoint.y());

      // Vertical position tracer.
      mp_vPosTracerItem->setVisible(true);

      mp_vPosTracerItem->setVisible(true);
      mp_vPosTracerItem->start->setCoords(m_context.lastCursorHoveredPoint.x(),
                                          yAxis->range().upper);
      mp_vPosTracerItem->end->setCoords(m_context.lastCursorHoveredPoint.x(),
                                        yAxis->range().lower);

      replot();
    }

  return;
}


void
BasePlotWidget::mouseMoveHandlerDraggingCursor()
{

  m_context.isMouseDragging = true;

  // Now store the mouse position data into the the current drag point
  // member datum, that will be used in countless occasions later.
  m_context.currentDragPoint  = m_context.lastCursorHoveredPoint;
  m_context.keyboardModifiers = QGuiApplication::queryKeyboardModifiers();

  // When we drag (either keyboard or mouse), we hide the position markers
  // (black) and we show the start and end vertical markers for the region.
  // Then, we draw the horizontal region range marker that delimits
  // horizontally the dragged-over region.

  mp_hPosTracerItem->setVisible(false);
  mp_vPosTracerItem->setVisible(false);

  // Only bother with the tracers if the user wants them to be visible.
  if(m_shouldTracersBeVisible)
    {

      // The vertical end tracer position must be refreshed.
      mp_vEndTracerItem->start->setCoords(m_context.currentDragPoint.x(),
                                          yAxis->range().upper);

      mp_vEndTracerItem->end->setCoords(m_context.currentDragPoint.x(),
                                        yAxis->range().lower);

      mp_vEndTracerItem->setVisible(true);
    }

  // Whatever the button, when we are dealing with the axes, we do not
  // want to show any of the tracers.

  if(m_context.wasClickOnXAxis || m_context.wasClickOnYAxis)
    {
      mp_hPosTracerItem->setVisible(false);
      mp_vPosTracerItem->setVisible(false);

      mp_vStartTracerItem->setVisible(false);
      mp_vEndTracerItem->setVisible(false);
    }

  // We will need the axes ranges matching the region being selected by the
  // user while left-mouse-dragging the cursor.

  calculateDragDeltasAndUnSortedRegionCorners();

  // Now deal with the BUTTON-SPECIFIC CODE.

  if(m_context.mouseButtonsAtMousePress & Qt::LeftButton)
    {
      return mouseMoveHandlerLeftButtonDraggingCursor();
    }
  else if(m_context.mouseButtonsAtMousePress & Qt::RightButton)
    {
      return mouseMoveHandlerRightButtonDraggingCursor();
    }
}


void
BasePlotWidget::mouseMoveHandlerLeftButtonDraggingCursor()
{
  // qDebug() << "the left button is dragging.";

  // Set the context.isMeasuringDistance to false, which later might be set to
  // true if effectively we are measuring a distance. This is required because
  // the derived widget classes might want to know if they have to perform
  // some action on the basis that context is measuring a distance, for
  // example the mass spectrum-specific widget might want to compute
  // deconvolutions.

  m_context.isMeasuringDistance = false;

  // Let's first check if the mouse drag operation originated on either
  // axis. In that case, the user is performing axis reframing or rescaling.

  if(m_context.wasClickOnXAxis || m_context.wasClickOnYAxis)
    {
      // qDebug() << __FILE__ << __LINE__ << "Click was on one of the axes.";

      if(m_context.keyboardModifiers & Qt::ControlModifier)
        {
          // The user is asking a rescale of the plot.

          // We know that we do not want the tracers when we perform axis
          // rescaling operations.

          mp_hPosTracerItem->setVisible(false);
          mp_vPosTracerItem->setVisible(false);

          mp_vStartTracerItem->setVisible(false);
          mp_vEndTracerItem->setVisible(false);

          // This operation is particularly intensive, thus we want to
          // reduce the number of calculations by skipping this calculation
          // a number of times. The user can ask for this feature by
          // clicking the 'Q' letter.

          if(m_context.pressedKeyCode == Qt::Key_Q)
            {
              if(m_mouseMoveHandlerSkipCount < m_mouseMoveHandlerSkipAmount)
                {
                  m_mouseMoveHandlerSkipCount++;
                  return;
                }
              else
                {
                  m_mouseMoveHandlerSkipCount = 0;
                }
            }

          // qDebug() << "Asking that the axes be rescaled.";

          axisRescale();
        }
      else
        {
          // The user was simply dragging the axis. Just pan, that is slide
          // the plot in the same direction as the mouse movement and with the
          // same amplitude.

          // qDebug() << "Asking that the axes be panned.";

          axisPan();
        }

      return;
    }

  // At this point we understand that the user was not performing any
  // panning/rescaling operation by clicking on any one of the axes.. Go on
  // with other possibilities.

  // Let's check if the user is actually drawing a rectangle (covering a
  // real area) or is drawing a line.

  // qDebug() << "The mouse dragging did not originate on an axis.";

  if(isProperSelectionRectangle())
    {
      // qDebug() << "Apparently the selection is a real rectangle.";

      // When we draw a rectangle the tracers are of no use.

      mp_hPosTracerItem->setVisible(false);
      mp_vPosTracerItem->setVisible(false);

      mp_vStartTracerItem->setVisible(false);
      mp_vEndTracerItem->setVisible(false);

      drawRectangleAndPrepareZoom();
    }
  else
    {
      // qDebug() << "Apparently we are measuring a delta.";

      // The pure position tracers should be hidden.
      mp_hPosTracerItem->setVisible(true);
      mp_vPosTracerItem->setVisible(true);

      // Then, make sure the region range vertical tracers are visible.
      mp_vStartTracerItem->setVisible(true);
      mp_vEndTracerItem->setVisible(true);

      drawXDeltaLineAndMeasure();
    }
}


void
BasePlotWidget::mouseMoveHandlerRightButtonDraggingCursor()
{
  // qDebug() << "the right button is dragging.";

  // Set the context.isMeasuringDistance to false, which later might be set to
  // true if effectively we are measuring a distance. This is required because
  // the derived widgets might want to know if they have to perform some
  // action on the basis that context is measuring a distance, for example the
  // mass spectrum-specific widget might want to compute deconvolutions.

  m_context.isMeasuringDistance = false;

  drawXDeltaLineForIntegration();
}


void
BasePlotWidget::axisDoubleClickHandler(
  QCPAxis *axis,
  [[maybe_unused]] QCPAxis::SelectablePart part,
  QMouseEvent *event)
{

  m_context.keyboardModifiers = QGuiApplication::queryKeyboardModifiers();

  if(m_context.keyboardModifiers & Qt::ControlModifier)
    {

      // If the Ctrl modifiers is active, then both axes are to be reset. Also
      // the histories are reset also.

      rescaleAxes();
      resetAxesRangeHistory();
    }
  else
    {
      // Only the axis passed as parameter is to be rescaled.
      // Reset the range of that axis to the max view possible.

      axis->rescale();

      updateAxesRangeHistory();

      event->accept();
    }

  // The double-click event does not cancel the mouse press event. That is, if
  // left-double-clicking, at the end of the operation the button still
  // "pressed". We need to remove manually the button from the pressed buttons
  // context member.

  m_context.pressedMouseButtons ^= event->button();

  updateContextRanges();

  emit plotRangesChangedSignal(m_context);

  replot();
}


bool
BasePlotWidget::isClickOntoXAxis(const QPointF &mousePoint)
{
  QCPLayoutElement *layoutElement = layoutElementAt(mousePoint);

  if(layoutElement &&
     layoutElement == dynamic_cast<QCPLayoutElement *>(axisRect()))
    {
      // The graph is *inside* the axisRect that is the outermost envelope of
      // the graph. Thus, if we want to know if the click was indeed on an
      // axis, we need to check what selectable part of the the axisRect we
      // were
      // clicking:
      QCPAxis::SelectablePart selectablePart;

      selectablePart = xAxis->getPartAt(mousePoint);

      if(selectablePart == QCPAxis::spAxisLabel ||
         selectablePart == QCPAxis::spAxis ||
         selectablePart == QCPAxis::spTickLabels)
        return true;
    }

  return false;
}


bool
BasePlotWidget::isClickOntoYAxis(const QPointF &mousePoint)
{
  QCPLayoutElement *layoutElement = layoutElementAt(mousePoint);

  if(layoutElement &&
     layoutElement == dynamic_cast<QCPLayoutElement *>(axisRect()))
    {
      // The graph is *inside* the axisRect that is the outermost envelope of
      // the graph. Thus, if we want to know if the click was indeed on an
      // axis, we need to check what selectable part of the the axisRect we
      // were
      // clicking:
      QCPAxis::SelectablePart selectablePart;

      selectablePart = yAxis->getPartAt(mousePoint);

      if(selectablePart == QCPAxis::spAxisLabel ||
         selectablePart == QCPAxis::spAxis ||
         selectablePart == QCPAxis::spTickLabels)
        return true;
    }

  return false;
}

/// MOUSE-related EVENTS


/// MOUSE MOVEMENTS mouse/keyboard-triggered

int
BasePlotWidget::dragDirection()
{
  // The user is dragging the mouse, probably to rescale the axes, but we need
  // to sort out in which direction the drag is happening.

  // This function should be called after calculateDragDeltas, so that
  // m_context has the proper x/y delta values that we'll compare.

  // Note that we cannot compare simply x or y deltas because the y axis might
  // have a different scale that the x axis. So we first need to convert the
  // positions to pixels.

  double x_delta_pixel =
    fabs(xAxis->coordToPixel(m_context.currentDragPoint.x()) -
         xAxis->coordToPixel(m_context.startDragPoint.x()));

  double y_delta_pixel =
    fabs(yAxis->coordToPixel(m_context.currentDragPoint.y()) -
         yAxis->coordToPixel(m_context.startDragPoint.y()));

  if(x_delta_pixel > y_delta_pixel)
    return Qt::Horizontal;

  return Qt::Vertical;
}


void
BasePlotWidget::moveMouseCursorGraphCoordToGlobal(QPointF graph_coordinates)
{
  // First convert the graph coordinates to pixel coordinates.

  QPointF pixels_coordinates(xAxis->coordToPixel(graph_coordinates.x()),
                             yAxis->coordToPixel(graph_coordinates.y()));

  moveMouseCursorPixelCoordToGlobal(pixels_coordinates.toPoint());
}


void
BasePlotWidget::moveMouseCursorPixelCoordToGlobal(QPointF pixel_coordinates)
{
  // qDebug() << "Calling set pos with new cursor position.";
  QCursor::setPos(mapToGlobal(pixel_coordinates.toPoint()));
}


void
BasePlotWidget::horizontalMoveMouseCursorCountPixels(int pixel_count)
{
  QPointF graph_coord = horizontalGetGraphCoordNewPointCountPixels(pixel_count);

  QPointF pixel_coord(xAxis->coordToPixel(graph_coord.x()),
                      yAxis->coordToPixel(graph_coord.y()));

  // Now we need ton convert the new coordinates to the global position system
  // and to move the cursor to that new position. That will create an event to
  // move the mouse cursor.

  moveMouseCursorPixelCoordToGlobal(pixel_coord.toPoint());
}


QPointF
BasePlotWidget::horizontalGetGraphCoordNewPointCountPixels(int pixel_count)
{
  QPointF pixel_coordinates(
    xAxis->coordToPixel(m_context.lastCursorHoveredPoint.x()) + pixel_count,
    yAxis->coordToPixel(m_context.lastCursorHoveredPoint.y()));

  // Now convert back to local coordinates.

  QPointF graph_coordinates(xAxis->pixelToCoord(pixel_coordinates.x()),
                            yAxis->pixelToCoord(pixel_coordinates.y()));

  return graph_coordinates;
}


void
BasePlotWidget::verticalMoveMouseCursorCountPixels(int pixel_count)
{

  QPointF graph_coord = verticalGetGraphCoordNewPointCountPixels(pixel_count);

  QPointF pixel_coord(xAxis->coordToPixel(graph_coord.x()),
                      yAxis->coordToPixel(graph_coord.y()));

  // Now we need ton convert the new coordinates to the global position system
  // and to move the cursor to that new position. That will create an event to
  // move the mouse cursor.

  moveMouseCursorPixelCoordToGlobal(pixel_coord.toPoint());
}


QPointF
BasePlotWidget::verticalGetGraphCoordNewPointCountPixels(int pixel_count)
{
  QPointF pixel_coordinates(
    xAxis->coordToPixel(m_context.lastCursorHoveredPoint.x()),
    yAxis->coordToPixel(m_context.lastCursorHoveredPoint.y()) + pixel_count);

  // Now convert back to local coordinates.

  QPointF graph_coordinates(xAxis->pixelToCoord(pixel_coordinates.x()),
                            yAxis->pixelToCoord(pixel_coordinates.y()));

  return graph_coordinates;
}

/// MOUSE MOVEMENTS mouse/keyboard-triggered


/// RANGE-related functions

QCPRange
BasePlotWidget::getRangeX(bool &found_range, int index) const
{
  QCPGraph *graph_p = graph(index);

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

  return graph_p->getKeyRange(found_range);
}


QCPRange
BasePlotWidget::getRangeY(bool &found_range, int index) const
{
  QCPGraph *graph_p = graph(index);

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

  return graph_p->getValueRange(found_range);
}


QCPRange
BasePlotWidget::getRange(PlotAxis axis,
                         RangeType range_type,
                         bool &found_range) const
{

  // Iterate in all the graphs in this widget and return a QCPRange that has
  // its lower member as the greatest lower value of all
  // its upper member as the smallest upper value of all

  if(!graphCount())
    {
      found_range = false;

      return QCPRange(0, 1);
    }

  if(graphCount() == 1)
    return graph()->getKeyRange(found_range);

  bool found_at_least_one_range = false;

  // Create an invalid range.
  QCPRange result_range(QCPRange::minRange + 1, QCPRange::maxRange + 1);

  for(int iter = 0; iter < graphCount(); ++iter)
    {
      QCPRange temp_range;

      bool found_range_for_iter = false;

      QCPGraph *graph_p = graph(iter);

      // Depending on the axis param, select the key or value range.

      if(axis == PlotAxis::x_axis)
        temp_range = graph_p->getKeyRange(found_range_for_iter);
      else if(axis == PlotAxis::y_axis)
        temp_range = graph_p->getValueRange(found_range_for_iter);
      else
        qFatal("Cannot reach this point. Programming error.");

      // Was a range found for the iterated graph ? If not skip this
      // iteration.

      if(!found_range_for_iter)
        continue;

      // While the innermost_range is invalid, we need to seed it with a good
      // one. So check this.

      if(!QCPRange::validRange(result_range))
        qFatal("The obtained range is invalid !");

      // At this point we know the obtained range is OK.
      result_range = temp_range;

      // We found at least one valid range!
      found_at_least_one_range = true;

      // At this point we have two valid ranges to compare. Depending on
      // range_type, we need to perform distinct comparisons.

      if(range_type == RangeType::innermost)
        {
          if(temp_range.lower > result_range.lower)
            result_range.lower = temp_range.lower;
          if(temp_range.upper < result_range.upper)
            result_range.upper = temp_range.upper;
        }
      else if(range_type == RangeType::outermost)
        {
          if(temp_range.lower < result_range.lower)
            result_range.lower = temp_range.lower;
          if(temp_range.upper > result_range.upper)
            result_range.upper = temp_range.upper;
        }
      else
        qFatal("Cannot reach this point. Programming error.");

      // Continue to next graph, if any.
    }
  // End of
  // for(int iter = 0; iter < graphCount(); ++iter)

  // Let the caller know if we found at least one range.
  found_range = found_at_least_one_range;

  return result_range;
}


QCPRange
BasePlotWidget::getInnermostRangeX(bool &found_range) const
{

  return getRange(PlotAxis::x_axis, RangeType::innermost, found_range);
}


QCPRange
BasePlotWidget::getOutermostRangeX(bool &found_range) const
{
  return getRange(PlotAxis::x_axis, RangeType::outermost, found_range);
}


QCPRange
BasePlotWidget::getInnermostRangeY(bool &found_range) const
{

  return getRange(PlotAxis::y_axis, RangeType::innermost, found_range);
}


QCPRange
BasePlotWidget::getOutermostRangeY(bool &found_range) const
{
  return getRange(PlotAxis::y_axis, RangeType::outermost, found_range);
}


/// RANGE-related functions


/// PLOTTING / REPLOTTING functions

void
BasePlotWidget::axisRescale()
{
  double xLower = xAxis->range().lower;
  double xUpper = xAxis->range().upper;

  // Get the current y lower/upper range.
  double yLower = yAxis->range().lower;
  double yUpper = yAxis->range().upper;

  // This function is called only when the user has clicked on the x/y axis or
  // when the user has dragged the left mouse button with the Ctrl key
  // modifier. The m_context.wasClickOnXAxis is then simulated in the mouse
  // move handler. So we need to test which axis was clicked-on.

  if(m_context.wasClickOnXAxis)
    {

      // We are changing the range of the X axis.

      // What is the x delta ?
      double xDelta =
        m_context.currentDragPoint.x() - m_context.startDragPoint.x();

      // If xDelta is < 0, the  we were dragging from right to left, we are
      // compressing the view on the x axis, by adding new data to the right
      // hand size of the graph. So we add xDelta to the upper bound of the
      // range. Otherwise we are uncompressing the view on the x axis and
      // remove the xDelta from the upper bound of the range. This is why we
      // have the
      // '-'
      // and not '+' below;

      // qDebug() << "Setting xaxis:" << xLower << "--" << xUpper - xDelta;

      xAxis->setRange(xLower, xUpper - xDelta);
    }
  // End of
  // if(m_context.wasClickOnXAxis)
  else // that is, if(m_context.wasClickOnYAxis)
    {
      // We are changing the range of the Y axis.

      // What is the y delta ?
      double yDelta =
        m_context.currentDragPoint.y() - m_context.startDragPoint.y();

      // See above for an explanation of the computation.

      yAxis->setRange(yLower, yUpper - yDelta);

      // Old version
      // if(yDelta < 0)
      //{
      //// The dragging operation was from top to bottom, we are enlarging
      //// the range (thus, we are unzooming the view, since the widget
      //// always has the same size).

      // yAxis->setRange(yLower, yUpper + fabs(yDelta));
      //}
      // else
      //{
      //// The dragging operation was from bottom to top, we are reducing
      //// the range (thus, we are zooming the view, since the widget
      //// always has the same size).

      // yAxis->setRange(yLower, yUpper - fabs(yDelta));
      //}
    }
  // End of
  // else // that is, if(m_context.wasClickOnYAxis)

  // Update the context with the current axes ranges

  updateContextRanges();

  emit plotRangesChangedSignal(m_context);

  replot();
}


void
BasePlotWidget::axisReframe()
{

  // double sorted_start_drag_point_x =
  // std::min(m_context.startDragPoint.x(), m_context.currentDragPoint.x());

  // xAxis->setRange(sorted_start_drag_point_x,
  // sorted_start_drag_point_x + fabs(m_context.xDelta));

  xAxis->setRange(
    QCPRange(m_context.xRegionRangeStart, m_context.xRegionRangeEnd));

  // Note that the y axis should be rescaled from current lower value to new
  // upper value matching the y-axis position of the cursor when the mouse
  // button was released.

  yAxis->setRange(
    xAxis->range().lower,
    std::max<double>(m_context.yRegionRangeStart, m_context.yRegionRangeEnd));

  // qDebug() << "xaxis:" << xAxis->range().lower << "-" <<
  // xAxis->range().upper
  //<< "yaxis:" << yAxis->range().lower << "-" << yAxis->range().upper;

  updateContextRanges();

  updateAxesRangeHistory();
  emit plotRangesChangedSignal(m_context);

  replot();
}


void
BasePlotWidget::axisZoom()
{

  // Use the m_context.xRegionRangeStart/End values, but we need to sort the
  // values before using them, because now we want to really have the lower x
  // value. Simply craft a QCPRange that will swap the values if lower is not
  // < than upper QCustomPlot calls this normalization).

  xAxis->setRange(
    QCPRange(m_context.xRegionRangeStart, m_context.xRegionRangeEnd));

  yAxis->setRange(
    QCPRange(m_context.yRegionRangeStart, m_context.yRegionRangeEnd));

  updateContextRanges();

  updateAxesRangeHistory();
  emit plotRangesChangedSignal(m_context);

  replot();
}

void
BasePlotWidget::axisPan()
{
  if(m_context.wasClickOnXAxis)
    {
      xAxis->setRange(m_context.xRange.lower - m_context.xDelta,
                      m_context.xRange.upper - m_context.xDelta);
    }

  if(m_context.wasClickOnYAxis)
    {
      yAxis->setRange(m_context.yRange.lower - m_context.yDelta,
                      m_context.yRange.upper - m_context.yDelta);
    }

  updateContextRanges();

  // We cannot store the new ranges in the history, because the pan operation
  // involved a huge quantity of micro-movements elicited upon each mouse move
  // cursor event so we would have a huge history.
  // updateAxesRangeHistory();

  // Now that the contex has the right range values, we can emit the
  // signal that will be used by this plot widget users, typically to
  // abide by the x/y range lock required by the user.

  emit plotRangesChangedSignal(m_context);

  replot();
}


void
BasePlotWidget::replotWithAxesRanges(QCPRange xAxisRange,
                                     QCPRange yAxisRange,
                                     PlotAxis axis)
{
  if(static_cast<int>(axis) & static_cast<int>(PlotAxis::x_axis))
    {
      xAxis->setRange(xAxisRange.lower, xAxisRange.upper);
    }

  if(static_cast<int>(axis) & static_cast<int>(PlotAxis::y_axis))
    {
      yAxis->setRange(yAxisRange.lower, yAxisRange.upper);
    }

  // We do not want to update the history, because there would be way too
  // much history items, since this function is called upon mouse moving
  // handling and not only during mouse release events.
  // updateAxesRangeHistory();

  replot();
}


void
BasePlotWidget::replotWithAxisRangeX(double lower, double upper)
{
  // qDebug();

  xAxis->setRange(lower, upper);

  replot();
}


void
BasePlotWidget::replotWithAxisRangeY(double lower, double upper)
{
  // qDebug();

  yAxis->setRange(lower, upper);

  replot();
}

/// PLOTTING / REPLOTTING functions


/// PLOT ITEMS : TRACER TEXT ITEMS...

//! Hide the selection line, the xDelta text and the zoom rectangle items.
void
BasePlotWidget::hideAllPlotItems()
{
  mp_selectLineItem->setVisible(false);

  mp_xDeltaTextItem->setVisible(false);

  mp_zoomRectItem->setVisible(false);

  // Force a replot to make sure the action is immediately visible by the
  // user, even without moving the mouse.
  replot();
}


//! Show the traces (vertical and horizontal).
void
BasePlotWidget::showTracers()
{
  m_shouldTracersBeVisible = true;

  mp_vPosTracerItem->setVisible(true);
  mp_hPosTracerItem->setVisible(true);

  mp_vStartTracerItem->setVisible(true);
  mp_vEndTracerItem->setVisible(true);

  // Force a replot to make sure the action is immediately visible by the
  // user, even without moving the mouse.
  replot();
}


//! Hide the traces (vertical and horizontal).
void
BasePlotWidget::hideTracers()
{
  m_shouldTracersBeVisible = false;
  mp_hPosTracerItem->setVisible(false);
  mp_vPosTracerItem->setVisible(false);

  mp_vStartTracerItem->setVisible(false);
  mp_vEndTracerItem->setVisible(false);

  // Force a replot to make sure the action is immediately visible by the
  // user, even without moving the mouse.
  replot();
}


void
BasePlotWidget::drawRectangleAndPrepareZoom()
{
  // The user has drawn the mouse left button on the graph, which means he is
  // willing to draw a zoom rectangle.

  mp_selectLineItem->setVisible(false);

  mp_xDeltaTextItem->setVisible(false);

  mp_zoomRectItem->topLeft->setCoords(m_context.startDragPoint.x(),
                                      m_context.startDragPoint.y());
  mp_zoomRectItem->bottomRight->setCoords(m_context.currentDragPoint.x(),
                                          m_context.currentDragPoint.y());

  mp_zoomRectItem->setVisible(true);

  // Now set the geometry of the rectangle to the context so that upon the
  // mouse button release, if the zoom rectanble is visible we know how to
  // reframe the plot axes. Note that we do not sort the values, if that is
  // required the user needs to use the sortAscendingRange() function.

  m_context.xRegionRangeStart = m_context.startDragPoint.x();
  m_context.xRegionRangeEnd   = m_context.currentDragPoint.x();

  m_context.yRegionRangeStart = m_context.startDragPoint.y();
  m_context.yRegionRangeEnd   = m_context.currentDragPoint.y();

  // Note that if we draw a zoom rectangle, then we are certainly not
  // measuring anything. So set the boolean value to false so that the user of
  // this widget or derived classes know that there is nothing to perform upon
  // (like deconvolution, for example).

  m_context.isMeasuringDistance = false;

  // Also remove the delta value from the pipeline by sending a simple
  // distance without measurement signal.

  emit xAxisMeasurementSignal(m_context, false);

  replot();
}


void
BasePlotWidget::prepareXDeltaLineAndMeasure()
{
  // The user has dragged the mouse left button on the graph in such a way
  // that the xDelta is big and the yDelta is almost nothing, that
  // means that he does not want to draw a rectangle but a line to
  // measure the delta between two points of the graph.

  mp_zoomRectItem->setVisible(false);

  // We also want to show the span as a text item.

  // m_context.xDelta is not an absolute value.
  double m_xDeltaHalf = fabs(m_context.xDelta / 2);

  // Use the m_context.xRegionRangeStart/End values, but we need to sort the
  // values before using them, because now we want to really have the lower x
  // value. Simply craft a QCPRange that will swap the values if lower is not
  // < than upper QCustomPlot calls this normalization).

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

  // qDebug() << "sorted range:" << sorted_range.lower << "-" <<
  // sorted_range.upper
  //<< "xrangedeltahalf:" << m_xDeltaHalf;

  mp_xDeltaTextItem->position->setCoords(sorted_range.lower + m_xDeltaHalf,
                                         m_context.currentDragPoint.y());

  mp_xDeltaTextItem->setText(QString("%1").arg(m_context.xDelta, 0, 'f', 3));

  mp_xDeltaTextItem->setFont(QFont(font().family(), 7));

  mp_xDeltaTextItem->setVisible(true);

  // We do not want to show the position markers because the only horiontal
  // line to be visible must be contained between the start and end vertiacal
  // tracer items.
  mp_hPosTracerItem->setVisible(false);
  mp_vPosTracerItem->setVisible(false);

  mp_selectLineItem->setVisible(true);

  mp_selectLineItem->start->setCoords(m_context.startDragPoint.x(),
                                      m_context.startDragPoint.y());
  // But we want the line to be horizontal, thus we keep the original y
  // value.
  mp_selectLineItem->end->setCoords(m_context.currentDragPoint.x(),
                                    m_context.startDragPoint.y());

  // Set the boolean to true so that derived widgets know that something is
  // being measured, and they can act accordingly, for example by computing
  // deconvolutions in a mass spectrum.
  m_context.isMeasuringDistance = true;

  return;
}


void
BasePlotWidget::drawXDeltaLineAndMeasure()
{
  prepareXDeltaLineAndMeasure();

  // Also, we do not want arrows, because we are not integrating anything
  // here.
  mp_selectLineItem->setHead(QCPLineEnding::esNone);
  mp_selectLineItem->setTail(QCPLineEnding::esNone);

  replot();

  // Let the caller know that we were measuring something.
  emit xAxisMeasurementSignal(m_context, true);
}


void
BasePlotWidget::drawXDeltaLineForIntegration()
{
  prepareXDeltaLineAndMeasure();

  // Since we draw the selection line with the xDelta text for integration, we
  // let the user guess it by the arrows at the ends of the line.
  mp_selectLineItem->setHead(QCPLineEnding::esSpikeArrow);
  mp_selectLineItem->setTail(QCPLineEnding::esSpikeArrow);

  replot();

  // Let the caller know that we were measuring something.
  emit xAxisMeasurementSignal(m_context, true);
}


void
BasePlotWidget::calculateDragDeltas()
{

  // We compute signed differentials. If the user does not want the sign,
  // fabs(double) is their friend.

  // Compute the xAxis differential:

  m_context.xDelta =
    m_context.currentDragPoint.x() - m_context.startDragPoint.x();

  // Same with the Y-axis range:

  m_context.yDelta =
    m_context.currentDragPoint.y() - m_context.startDragPoint.y();

  // qDebug() << "xDelta:" << m_context.xDelta
  //<< "and yDelta:" << m_context.yDelta;

  return;
}


void
BasePlotWidget::calculateDragDeltasAndUnSortedRegionCorners()
{

  calculateDragDeltas();

  // Note that we do not sort the x value nor the y values. If the user of the
  // ranges need them to be sorted, they can use the sortAscendingRange().

  m_context.xRegionRangeStart = m_context.startDragPoint.x();
  m_context.xRegionRangeEnd   = m_context.currentDragPoint.x();

  m_context.yRegionRangeStart = m_context.startDragPoint.y();
  m_context.yRegionRangeEnd   = m_context.currentDragPoint.y();

  return;
}


bool
BasePlotWidget::isProperSelectionRectangle()
{
  // First get the height of the plot.
  double plotHeight = yAxis->range().upper - yAxis->range().lower;

  double heightDiff =
    fabs(m_context.startDragPoint.y() - m_context.currentDragPoint.y());

  double heightDiffRatio = (heightDiff / plotHeight) * 100;

  if(heightDiffRatio > 10)
    return true;

  return false;
}


void
BasePlotWidget::setFocus()
{
  // qDebug() << "Setting focus to the QCustomPlot:" << this;

  QCustomPlot::setFocus();

  // qDebug() << "Emitting setFocusSignal().";

  emit setFocusSignal();
}


//! Redraw the background of the \p focusedPlotWidget plot widget.
void
BasePlotWidget::redrawPlotBackground(QWidget *focusedPlotWidget)
{
  if(focusedPlotWidget == nullptr)
    throw ExceptionNotPossible(
      "baseplotwidget.cpp @ redrawPlotBackground(QWidget *focusedPlotWidget -- "
      "ERROR focusedPlotWidget cannot be nullptr.");

  if(dynamic_cast<QWidget *>(this) != focusedPlotWidget)
    {
      // The focused widget is not *this widget. We should make sure that
      // we were not the one that had the focus, because in this case we
      // need to redraw an unfocused background.

      axisRect()->setBackground(m_unfocusedBrush);
    }
  else
    {
      axisRect()->setBackground(m_focusedBrush);
    }

  replot();
}


void
BasePlotWidget::updateContextRanges()
{
  m_context.xRange = QCPRange(xAxis->range().lower, xAxis->range().upper);
  m_context.yRange = QCPRange(yAxis->range().lower, yAxis->range().upper);
}


const BasePlotContext &
BasePlotWidget::getContext() const
{
  return m_context;
}


} // namespace pappso
