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

// For the proton mass
#include "../../types.h"

#include "massspectraceplotwidget.h"
#include "../../pappsoexception.h"
#include "massspectraceplotcontext.h"


int massSpecTracePlotContextMetaTypeId =
  qRegisterMetaType<pappso::MassSpecTracePlotContext>(
    "pappso::MassSpecTracePlotContext");

int massSpecTracePlotContextPtrMetaTypeId =
  qRegisterMetaType<pappso::MassSpecTracePlotContext *>(
    "pappso::MassSpecTracePlotContext *");


namespace pappso
{

MassSpecTracePlotWidget::MassSpecTracePlotWidget(QWidget *parent)
  : BaseTracePlotWidget(parent)
{
  // Set the base context to be of kind DataKind::mz;

  BasePlotWidget::m_context.m_dataKind = DataKind::mz;

  // And then initialize ours with the base class' one.
  m_context.m_baseContext = BasePlotWidget::m_context;

  // qDebug() << "Data kind:" <<
  // static_cast<int>(m_context.m_baseContext.m_dataKind);
}

MassSpecTracePlotWidget::MassSpecTracePlotWidget(QWidget *parent,
                                                 const QString &x_axis_label,
                                                 const QString &y_axis_label)
  : BaseTracePlotWidget(parent, x_axis_label, y_axis_label)
{
  // Set the base context to be of kind DataKind::mz;

  BasePlotWidget::m_context.m_dataKind = DataKind::mz;

  // And then initialize ours with the base class' one.
  m_context.m_baseContext = BasePlotWidget::m_context;

  // qDebug() << "Data kind:" <<
  // static_cast<int>(m_context.m_baseContext.m_dataKind);
}


MassSpecTracePlotWidget::~MassSpecTracePlotWidget()
{
}


//! Set the \c m_pressedKeyCode to the key code in \p event.
void
MassSpecTracePlotWidget::keyPressEvent(QKeyEvent *event)
{
  // qDebug() << "ENTER";
  BasePlotWidget::keyPressEvent(event);

  // Before working on the various data belonging to the base context, we need
  // to get it from the base class and refresh our local context with it.
  refreshBaseContext();

  // qDebug() << "Going to emit keyPressEventSignal(m_context);";

  emit keyPressEventSignal(m_context);
  emit testKeyPressEventSignal(m_context);
  emit newKeyPressEventSignal(
    pappso::DataPoint(m_context.m_baseContext.m_currentDragPoint.x(),
                      m_context.m_baseContext.m_currentDragPoint.y()));
}


//! Handle specific key codes and trigger respective actions.
void
MassSpecTracePlotWidget::keyReleaseEvent(QKeyEvent *event)
{
  BasePlotWidget::keyReleaseEvent(event);

  // Before working on the various data belonging to the base context, we need
  // to get it from the base class and refresh our local context with it.
  refreshBaseContext();
}


//! Handle mouse movements, in particular record all the last visited points.
/*!

  This function is reponsible for storing at each time the last visited point
  in the graph. Here, point is intended as any x/y coordinate in the plot
  widget viewport, not a graph point.

  The stored values are then the basis for a large set of calculations
  throughout all the plot widget.

  \param pointer to QMouseEvent from which to retrieve the coordinates of the
  visited viewport points.
  */
void
MassSpecTracePlotWidget::mouseMoveHandler(QMouseEvent *event)
{
  BasePlotWidget::mouseMoveHandler(event);

  // Before working on the various data belonging to the base context, we need
  // to get it from the base class and refresh our local context with it.
  refreshBaseContext();
}


void
MassSpecTracePlotWidget::mouseMoveHandlerNotDraggingCursor()
{
  BasePlotWidget::mouseMoveHandlerNotDraggingCursor();

  // Before working on the various data belonging to the base context, we need
  // to get it from the base class and refresh our local context with it.
  refreshBaseContext();
}


void
MassSpecTracePlotWidget::mouseMoveHandlerDraggingCursor()
{
  BasePlotWidget::mouseMoveHandlerDraggingCursor();

  // Before working on the various data belonging to the base context, we need
  // to get it from the base class and refresh our local context with it.
  refreshBaseContext();

  if(m_context.m_baseContext.m_mouseButtonsAtMousePress & Qt::LeftButton)
    {
      if(!m_context.m_baseContext.m_isMeasuringDistance)
        return;

      // qDebug() << "lastMovingMouseButtons:"
      //<< m_context.m_baseContext.m_lastMovingMouseButtons;

      deconvolute();
      computeResolvingPower();
    }
}


//! Record the clicks of the mouse.
void
MassSpecTracePlotWidget::mousePressHandler(QMouseEvent *event)
{
  BasePlotWidget::mousePressHandler(event);

  // Before working on the various data belonging to the base context, we need
  // to get it from the base class and refresh our local context with it.
  refreshBaseContext();
}


//! React to the release of the mouse buttons.
void
MassSpecTracePlotWidget::mouseReleaseHandler(QMouseEvent *event)
{
  BasePlotWidget::mouseReleaseHandler(event);

  // Before working on the various data belonging to the base context, we need
  // to get it from the base class and refresh our local context with it.
  refreshBaseContext();
}


const MassSpecTracePlotContext &
MassSpecTracePlotWidget::refreshBaseContext() const
{
  m_context.m_baseContext = BasePlotWidget::m_context;

  return m_context;
}


void
MassSpecTracePlotWidget::setChargeMinimalFractionalPart(
  double charge_fractional_part)
{
  m_chargeMinimalFractionalPart = charge_fractional_part;
}


double
MassSpecTracePlotWidget::getChargeMinimalFractionalPart() const
{
  return m_chargeMinimalFractionalPart;
}


void
MassSpecTracePlotWidget::setChargeStateEnvelopePeakSpan(int interval)
{
  m_chargeStateEnvelopePeakSpan = interval;
}


int
MassSpecTracePlotWidget::getChargeStateEnvelopePeakSpan() const
{
  return m_chargeStateEnvelopePeakSpan;
}


//! Deconvolute the mass peaks into charge and molecular mass.
bool
MassSpecTracePlotWidget::deconvolute()
{

  // There are two situations: when the user is deconvoluting on the
  // basis of the distance between two consecutive peaks of a same
  // isotopic cluster or when the user deconvolutes on the basis of two
  // different charged-stated peaks that belong to the same envelope.

  // We can tell the difference because in the first case the xDelta
  // should be less than 1. In the other case, of course the difference
  // is much greater than 1.

  // In order to do the deconvolutions, we need to know what is the tolerance
  // on the fractional part of the deconvoluted charge value. This value is set
  // in the parent window's double spin box.

  if(fabs(m_context.m_baseContext.m_xDelta) >= 0 &&
     fabs(m_context.m_baseContext.m_xDelta) <= 1.1)
    {
      // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()"
      //<< "m_xDelta:" << m_context.m_baseContext.m_xDelta
      //<< "trying isotope-based deconvolution.";

      return deconvoluteIsotopicCluster();
    }

  // If not deconvoluting on the basis of the isotopic cluster, then:

  return deconvoluteChargedState(m_chargeStateEnvelopePeakSpan);
}


//! Deconvolute the mass peaks into charge and molecular mass.
/*!

  This is one of two methods to deconvolute mass data into a charge value and
  a Mr value. The method implemented in this function is based on the charge
  state envelope offered by the mass spectrum (most often for polymers of a
  reasonable size).

  \param span value representing the number of peaks of the charge state
  envelope that are spanned by the user selection. Defaults to 1, that is, the
  span encompasses two \e consecutive mass peaks of a given charge state
  envelope.

  Set m_lastMz, m_lastZ and m_lastMass.

  \return true if the deconvolution could be performed, false otherwise.
  */
bool
MassSpecTracePlotWidget::deconvoluteChargedState(int span)
{
  // We assume that we are dealing with two successive (if span is 1) mass
  // peaks belonging to a given charge state family.

  // We call span the number of intervals in a given charge state envelope
  // that separate the initial peak (lowerMz) from the last peak (upperMz).
  // That parameter defaults to 1, that is the two peaks are immediately
  // consecutive, that is, there is only one interval.

  // We use the m_contex.basecontext.m_xRegionRange structure that is unsorted.
  // That is, lower is the start drag point.x and upper is the current drag
  // point.x. If dragging occurs from left to right, start.x < cur.x.
  // We use the unsorted values, because we need to know in which direction
  // the user has drug the mouse, because we want to provide the Mr value
  // for the peak currently under the mouse cursor, that is under
  // currentDragPoint, that is the value in
  // m_context.m_baseContext.m_xRegionRange.upper.

  double startMz = m_context.m_baseContext.m_xRegionRangeStart;
  double curMz   = m_context.m_baseContext.m_xRegionRangeEnd;

  // qDebug() << "startMz:" << startMz << "curMz:" << curMz;

  if(startMz == curMz)
    {
      m_context.m_lastZ            = -1;
      m_context.m_lastMz           = std::numeric_limits<double>::min();
      m_context.m_lastTicIntensity = std::numeric_limits<double>::min();
      m_context.m_lastMr           = std::numeric_limits<double>::min();

      return false;
    }

  // We need to be aware that the status bar of the window that contains
  // this plot widget shows the cursor position realtime, and that cursor
  // position is the m_currentDragPoint.x value, that is, curMz. Thus, we need
  // to make the calculations with the charge being the one of the polymer under
  // the cursor position. This is tricky because it changes when the user
  // switches drag senses: from left to right and right to left.
  // The way z is calculated always makes it the charge of the highest mz
  // value. So knowing this, depending on the drag direction we'll have to take
  // curMz and apply to it either z charge (left to right drag) or (z+span)
  // charge (right to left).

  // Make sure lower is actually lower, even if drag is from right to left.
  // This is only to have a single charge calculation.
  double lowerMz;
  double upperMz;

  if(startMz < curMz)
    {
      lowerMz = startMz;
      upperMz = curMz;
    }
  else
    {
      lowerMz = curMz;
      upperMz = startMz;
    }

  double chargeTemp = ((lowerMz * span) - span) / (upperMz - lowerMz);

  // Make a judicious roundup.

  double chargeIntPart;
  double chargeFracPart = modf(chargeTemp, &chargeIntPart);

  // When calculating the charge of the ion, very rarely does it provide a
  // perfect integer value. Most often (if deconvolution is for bona fide
  // peaks belonging to the same charge state envelope) that value is with
  // either a large fractional part or a very small fractional part. What we
  // test here, it that fractional part. If it is greater than
  // m_chargeMinimalFractionalPart, then we simply round up to the next integer
  // value (that is, chargeIntPart = 27 and chargeFracPart 0.995, then we
  // set charge to 28). If it is lesser or equal to (1 -
  // m_chargeMinimalFractionalPart /* that is >= 0.01 */, then we let
  // chargeIntPart unmodified (that is, chargeIntPart = 29 and
  // chargeFracPart 0.01, then we set charge to 29). If chargeFracPart is in
  // between (1 - m_chargeMinimalFractionalPart) and
  // m_chargeMinimalFractionalPart, then we consider that the peaks do not
  // belong to the same charge state envelope.

  // qDebug() << __FILE__ << __LINE__ << __FUNCTION__
  //<< "Charge:" << chargeIntPart
  //<< "Charge fractional part: " << chargeFracPart;


  if(chargeFracPart >=
       (1 - m_chargeMinimalFractionalPart /* that is >= 0.01 */) &&
     chargeFracPart <= m_chargeMinimalFractionalPart /* that is <= 0.99 */)
    {
      m_context.m_lastZ            = -1;
      m_context.m_lastMz           = std::numeric_limits<double>::min();
      m_context.m_lastTicIntensity = std::numeric_limits<double>::min();
      m_context.m_lastMr           = std::numeric_limits<double>::min();

      // qDebug() << __FILE__ << __LINE__
      //<< "Not a charge state family peak,"
      //<< "returning from deconvoluteChargeState";

      return false;
    }

  if(chargeFracPart > m_chargeMinimalFractionalPart)
    m_context.m_lastZ = chargeIntPart + 1;
  else
    m_context.m_lastZ = chargeIntPart;

  // Now, to actually compute the molecular mass based on the charge and on
  // the currently displayed m/z value, we need to have some thinking:

  if(startMz < curMz)
    {
      // The drag was from left to right, that is curMz is greater than
      // startMz. Fine, the z value is effectively the charge of the ion at
      // curMz. Easy, no charge value modification here.
    }
  else
    {
      // The drag was from right to left, that is curMz is less than startMz.
      // So we want to show the charge of the curMz, that is, z + span.
      m_context.m_lastZ = m_context.m_lastZ + span;
    }

  m_context.m_lastMz = curMz;
  m_context.m_lastMr =
    (m_context.m_lastMz * m_context.m_lastZ) - (m_context.m_lastZ * MPROTON);

  // qDebug() << __FILE__ << __LINE__
  //<< "startMz:" << QString("%1").arg(startMz, 0, 'f', 6)
  //<< "m_lastMz (curMz):"
  //<< QString("%1").arg(m_context.m_lastMz, 0, 'f', 6)
  //<< "m_lastMass:" << QString("%1").arg(m_context.m_lastMr, 0, 'f', 6)
  //<< "m_lastZ:" << QString("%1").arg(m_context.m_lastZ);

  // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()"
  //<< "returning true";

  // The m_context was refreshed with the base class context in the calling
  // chain.
  emit massDeconvolutionSignal(m_context);

  return true;
}


//! Deconvolute the mass peaks into charge and molecular mass.
/*!

  This is one of two methods to deconvolute mass data into a charge value and
  a Mr value. The method implemented in this function is based on the distance
  that separates two immediately consecutive peaks of an isotopic cluster.
  This method can be used as long as the instrument produced data with a
  resolution sufficient to separate reasonably well the different peaks of an
  isotopic cluster.

  Set m_lastMz, m_lastZ and m_lastMass.

  \return true if the deconvolution could be performed, false otherwise.
  */
bool
MassSpecTracePlotWidget::deconvoluteIsotopicCluster()
{

  if(m_context.m_baseContext.m_xRegionRangeStart ==
     m_context.m_baseContext.m_xRegionRangeEnd)
    {
      // qDebug() << __FILE__ << __LINE__
      //<< "Same xRegionRange.upper and xRegionRange.lower:"
      //<< "returning from deconvoluteIsotopicCluster";

      return false;
    }

  double chargeTemp = 1 / fabs(m_context.m_baseContext.m_xDelta);

  // Make a judicious roundup.
  double chargeIntPart;
  double chargeFracPart = modf(chargeTemp, &chargeIntPart);

  // qDebug() << "m_xDelta:" << m_context.m_baseContext.m_xDelta
  //<< "chargeTemp:" << QString("%1").arg(chargeTemp, 0, 'f', 6)
  //<< "chargeIntPart:" << chargeIntPart
  //<< "chargeFracPart:" << QString("%1").arg(chargeFracPart, 0, 'f', 6)
  //<< "m_chargeMinimalFractionalPart:" << m_chargeMinimalFractionalPart;

  if(chargeFracPart >= (1 - m_chargeMinimalFractionalPart) &&
     chargeFracPart <= m_chargeMinimalFractionalPart)
    {
      m_context.m_lastZ            = -1;
      m_context.m_lastMz           = std::numeric_limits<double>::min();
      m_context.m_lastTicIntensity = std::numeric_limits<double>::min();
      m_context.m_lastMr           = std::numeric_limits<double>::min();

      // qDebug() << "Not in a isotopic cluster peak:"
      //<< "returning from deconvoluteIsotopicCluster";

      return false;
    }

  if(chargeFracPart > m_chargeMinimalFractionalPart)
    {
      m_context.m_lastZ = chargeIntPart + 1;

      // qDebug() << "chargeFracPart > m_chargeMinimalFractionalPart -> m_lastZ
      // = "
      //<< m_context.m_lastZ;
    }
  else
    {
      m_context.m_lastZ = chargeIntPart;

      // qDebug()
      //<< "chargeFracPart <=  m_chargeMinimalFractionalPart -> m_lastZ = "
      //<< m_context.m_lastZ;
    }

  // Now that we have the charge in the form of an int, we can compute the
  // Mr of the lightest isotopic cluster peak (the one that has the lowest x
  // value). That value is stored in m_xRangeLower.

  // We need to sort the xRegionRange before being certain that lower is indeed
  // the left value of the drag span.

  m_context.m_lastMz = std::min<double>(m_context.m_baseContext.m_xRegionRangeStart,
                                      m_context.m_baseContext.m_xRegionRangeEnd);

  m_context.m_lastMr =
    (m_context.m_lastMz * m_context.m_lastZ) - (m_context.m_lastZ * MPROTON);

  // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()"
  //<< "returning true";

  // The m_context was refreshed with the base class context in the calling
  // chain.
  emit massDeconvolutionSignal(m_context);

  return true;
}


bool
MassSpecTracePlotWidget::computeResolvingPower()
{

  // m_xRangeLower and m_xRangeUpper and m_xDelta (in fabs() form) have been set
  // during mouve movement handling. Note that the range values *are
  // sorted*.

  if(!m_context.m_baseContext.m_xDelta || m_context.m_baseContext.m_xDelta > 1)
    {
      m_context.m_lastResolvingPower = std::numeric_limits<double>::min();

      return false;
    }

  // Resolving power is m/z / Delta(m/z), for singly-charged species.

  m_context.m_lastResolvingPower =
    (std::min<double>(m_context.m_baseContext.m_xRegionRangeStart,
                      m_context.m_baseContext.m_xRegionRangeEnd) +
     (m_context.m_baseContext.m_xDelta / 2)) /
    m_context.m_baseContext.m_xDelta;

  // The m_context was refreshed with the base class context in the calling
  // chain.
  emit resolvingPowerComputationSignal(m_context);

  return true;
}


} // namespace pappso
