/* BEGIN software license
 *
 * MsXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright(C) 2009,...,2018 Filippo Rusconi
 *
 * http://www.msxpertsuite.org
 *
 * This file is part of the MsXpertSuite project.
 *
 * The MsXpertSuite project is the successor of the massXpert project. This
 * project now includes various independent modules:
 *
 * - massXpert, model polymer chemistries and simulate mass spectrometric data;
 * - mineXpert, a powerful TIC chromatogram/mass spectrum viewer/miner;
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 * END software license
 */


/////////////////////// Qt includes
#include <QtGlobal>


/////////////////////// Local includes
#include "Ionizable.hpp"
#include "PolChemDef.hpp"


namespace MsXpS
{

namespace libXpertMass
{

/*!
\class MsXpS::libXpertMass::Ionizable
\inmodule libXpertMass
\ingroup PolChemDefBuildingdBlocks
\inheaderfile Ionizable.hpp

\brief The Ionizable class provides abstractions to work with entities that
have masses (base class \l Ponderable) and that can be ionized.

An Ionizable is a \l Ponderable that might undergo ionization, using either its
own member \l IonizeRule or an IonizeRule passed as argument to one of its
functions. An Ionizable \e is also a \l PolChemDefEntity, as it has to know at
each instant what polymer chemistry definition it is based upon. The ionization
status of the Ionizable can be checked at each moment and a call to an ionizing
function will first trigger deionization if the Ionizable is already ionized.
The main members of the Ionizable class are the following:

\list
\li a IonizeRule;
\li a boolean member stating if the entity has actually been
ionized;
\endlist

Upon creation of an Ionizable (without use of the copy constructor), all the
data required for the full qualification of the new instance should be passed to
the constructor. Default parameters ensure that the Ionizable is set to a
consistent status (that is its IonizeRule member is \e invalid and that its
m_isIonized flag is false).

The caller is responsible for seeding correct and consistent values into the
constructor for proper operation of the class instances.

For the ionization to be actually performed, and the masses to be effectively
updated to account for that ionization, the Ionizable instance must be
ionize()d.

It is possible to deionize() an Ionizable instance as it is possible to reionize
the instance with another IonizeRule, which will replace the member IonizeRule
if the reionization succeeds. The deionize() call effects the state of the
Ionizable instance only if the m_isIonized boolean is true.
*/


/*!
\variable MsXpS::libXpertMass::Ionizable::m_ionizeRule

\brief The ionization rule that defines the way to ionize this Ionizable.

\sa IonizeRule
*/

/*!
\variable MsXpS::libXpertMass::Ionizable::m_isIonized

\brief Tells if this Ionizable has undergone an ionization.
*/


/*!
\brief  Constructs an Ionizable.

The Ionizable instance will be an ionized entity if \a is_ionized is true. The
\a ponderable's mono and avg masses must thus be masses which take
(is_ionized is true) or do not take (is_ionized is false) into account the data
located in \a ionize_rule. If \a is_ionized is true, the \a ionize_rule is
validated and the level of the ionization must not be 0. If one of these two
tests fails, this is an error and the program aborts.

  \a pol_chem_def_csp Polymer chemistry definition (cannot be nullptr);

  \a name Name of this Ionizable (defaults to "NOT_SET");

  \a ponderable \l Ponderable (mono and avg masses);

  \a ionize_rule \l IonizeRule (defaults to IonizeRule(), that is,
  an invalid IonizeRule).

  \a is_ionized Tells if the Ionizable to be constructed should be considered
as an ionized chemical entity.
*/
Ionizable::Ionizable(PolChemDefCstSPtr pol_chem_def_csp,
                     const QString &name,
                     const Ponderable &ponderable,
                     const IonizeRule &ionize_rule,
                     bool is_ionized)
  : PolChemDefEntity(pol_chem_def_csp, name),
    Ponderable(ponderable),
    m_ionizeRule(ionize_rule),
    m_isIonized(is_ionized)
{
  if(m_isIonized)
    {
      // If the Ionizable is ionized, then that means that its
      // IonizeRule should validate and also that the ionization
      // level should not be 0.
      if(!m_ionizeRule.isValid() || m_ionizeRule.level() == 0)
        {
          qFatal("Failure to construct Ionizable");
        }
    }
}


/*!
\brief  Constructs an Ionizable as a copy of \a other.

No assumption should be made as to the status of the created Ionizable. The
caller should characterize the ionization status of the Ionizable with
isIonized().
 */
Ionizable::Ionizable(const Ionizable &other)
  : PolChemDefEntity(other),
    Ponderable(other),
    m_ionizeRule(other.m_ionizeRule),
    m_isIonized(other.m_isIonized)
{
}


/*!
\brief  Destructs this Ionizable.
*/
Ionizable::~Ionizable()
{
}


/*!
\brief  Assigns \a other to this Ionizable.

Return A reference to this Ionizable instance.
*/
Ionizable &
Ionizable::operator=(const Ionizable &other)
{
  if(&other == this)
    return *this;

  PolChemDefEntity::operator=(other);
  Ponderable::operator=(other);

  m_ionizeRule = other.m_ionizeRule;
  m_isIonized  = other.m_isIonized;

  return *this;
}


/*!
\brief  Returns a constant reference to this IonizeRule.
*/
const IonizeRule &
Ionizable::ionizeRule() const
{
  return m_ionizeRule;
}


/*!
\brief  Returns a pointer to this IonizeRule.
*/
IonizeRule *
Ionizable::ionizeRule()
{
  return &m_ionizeRule;
}


/*!
\brief  Returns true if this Ionizable is ionized, false otherwise.

 The ionization status is returned on the basis of the m_isIonized member
boolean value.
*/
bool
Ionizable::isIonized() const
{
  return m_isIonized;
}

/*!
\brief  Returns the ionization charge of this Ionizable.

 The charge is returned as a positive number (0 allowed) or as -1 if the
IonizeRule is not valid or if m_isIonized is false.

\note The charge is returned as the compound product of the IonizeRule's
m_charge and m_level members.
*/
int
Ionizable::charge() const
{
  // If the member ionizeRule is not valid, then return -1, that is
  // inform the caller that something is not correct in the
  // "workflow".

  if(!m_ionizeRule.isValid())
    return -1;

  // Now, the ionizeRule is correct, but the ionizable does not
  // advertise that it has been ionized. In this case, its charge is
  // 0.

  if(!m_isIonized)
    return 0;

  // Finally, the ionizable is effectively
  // ionized, that is, its ionizeRule is valid and it advertises that
  // it is ionized, in which case we can return its charge.

  return (m_ionizeRule.charge() * m_ionizeRule.level());
}


/*!
\brief  Sets the charge of this Ionizable to \a charge.

The charge of an ionizable is the compound product of m_charge and m_level in
its \l IonizeRule member. The value passed as \a charge is \e that compound
product. Thus, the member IonizeRule is updated to reflect the new \c charge
using the following code:

\code
  int level = charge / m_ionizeRule.charge();
  m_ionizeRule.setLevel(level);
\endcode

Only the level value of IonizeRule is changed to reflect the change of the \a
charge because the chemistry of the ionization rule itself must not be changed.

The following logic is applied:

\list 1
\li
If the member ionizeRule is not valid, this function returns -1 because it
does not make sense to try to change the charge of an Ionizable if its member
IonizeRule is not valid.
\li This Ionizable is first deionized if it is ionized. If the deionization
fails -1 is returned.
\li This Ionizable is ionized with the level value in its member IonizeRule. If
that ionization fails, -1 is returned.
\endlist

At this point all the process went fine and 1 is returned.
*/
int
Ionizable::setCharge(int charge)
{
  if(!m_ionizeRule.isValid())
    return -1;

  // If *this Ionizable is ionized, first deionize it with its own
  // m_ionizeRule, so that we get back to M, as we would say in
  // front of a mass spectrometer.

  if(m_isIonized)
    {
      if(!deionize())
        return -1;
      else
        // Make clear that at this point we are not ionized.
        m_isIonized = false;
    }

  // At this point, if charge is 0, then we are done, and we can
  // return a success.
  if(!charge)
    return 1;

  // At this point we can compute what IonizeRule's level should
  // be
  //(taking into account its charge, that is its unitary charge) so
  // that the total charge is identical to the parameter.

  int level = charge / m_ionizeRule.charge();

  m_ionizeRule.setLevel(level);

  if(ionize() == -1)
    return -1;

  // Return 1 because we know we changed something:
  return 1;
}


/*!
\brief  Ionizes this Ionizable.

Ionization is performed on this Ionizable using the member IonizeRule.

The following logic is applied:

\list 1
\li If the member ionizeRule is not valid, this function returns -1 because it
does not make sense to try to change the charge of an Ionizable if its member
IonizeRule is not valid.
\li If this Ionizable is ionized, return 0 (already ionized, then nothing to
do).
\li The mass status of this Ionizable (\l Ponderable base class) is stored
for later comparison.
\li The ionization process is carried out and the new mass status is compared
to the older one. If the new status differs from the previous one, than 1 is
returned, otherwise -1 is returned.
\endlist
*/
int
Ionizable::ionize()
{
  if(!m_ionizeRule.isValid())
    return -1;

  if(m_isIonized)
    return 0;

  // At this point perform the ionization proper.

  Formula formula(m_ionizeRule.formula());

  qDebug() << "The formula right before ionizing:" << formula.toString();

  Ponderable temp(*this);

  qDebug() << __FILE__ << __LINE__ << "Before ionization:\n"
           << "Ionizable's charge / level:" << m_ionizeRule.charge() << "/"
           << m_ionizeRule.level() << " -- "
           << "Ionizable's whole charge:"
           << m_ionizeRule.charge() * m_ionizeRule.level() << " -- "
           << "Mono:" << temp.mono() << "Avg:" << temp.avg() << "\n\n";

  double localMono = 0;
  double localAvg  = 0;

  IsotopicDataCstSPtr isotopic_data_csp =
    mcsp_polChemDef->getIsotopicDataCstSPtr();

  // Note the times(localIonizeRule.level()) param to call below.
  if(!formula.accountMasses(
       isotopic_data_csp, &localMono, &localAvg, m_ionizeRule.level()))
    return -1;

  qDebug() << qSetRealNumberPrecision(6)
           << "Right after accounting formula masses:" << localAvg << "-"
           << localMono;

  // OK, the accounting of the masses worked ok, we can update the
  // values in the mono and avg params(note that we know that the
  // charge and level values of m_ionizeRule cannot be <= 0 because
  // otherwise the m_ionizRule would not have validated.

  int ionCharge = m_ionizeRule.charge() * m_ionizeRule.level();

  qDebug() << "The ion charge:" << ionCharge;

  qDebug() << qSetRealNumberPrecision(6) << "m_mono:" << m_mono
           << "m_avg:" << m_avg;

  m_mono += localMono;
  m_mono = m_mono / ionCharge;

  m_avg += localAvg;
  m_avg = m_avg / ionCharge;

  // Of course, we now are ionized.
  m_isIonized = true;

  qDebug() << __FILE__ << __LINE__ << "After ionization:\n"
           << "Ionizable's charge / level:" << m_ionizeRule.charge() << "/"
           << m_ionizeRule.level() << " -- "
           << "Ionizable's whole charge:"
           << m_ionizeRule.charge() * m_ionizeRule.level() << " -- "
           << "Mono:" << m_mono << "Avg:" << m_avg << "\n\n";

  // If something changed in the masses, then return 1, otherwise
  // return 0.
  if(temp != static_cast<Ponderable>(*this))
    {
      return 1;
    }

  return 0;
}

/*!
\brief  Ionizes this Ionizable using \a ionize_rule.

The following logic is applied:

\list 1
\li
If \a ionize_rule is not valid, this function returns -1 because it
does not make sense to try to change the charge of an Ionizable if that
ionization rule is not valid.
\li If this Ionizable is ionized, first \l{deionize()} it. If this step fails,
returns -1.
\li The ionization process is carried out. If the process is successful, 1 is
returned, otherwise -1 is returned.
\endlist

\sa ionize()
*/
int
Ionizable::ionize(const IonizeRule &ionize_rule)
{
  if(!ionize_rule.isValid())
    return -1;

  // If *this Ionizable is ionized, first deionize it with its own
  // m_ionizeRule, so that we get back to M, as we would say in
  // front of a mass spectrometer.

  Ponderable temp(*this);

  if(m_isIonized)
    {
      qDebug() << "this before deionization:" << toString();

      if(!deionize())
        return -1;
      else
        // Make clear that at this point we are not ionized.
        m_isIonized = false;

      qDebug() << "this after deionization:" << toString();
    }
  else
    {
      qDebug() << "this is not ionized:" << toString();
    }

  // At this point perform the ionization proper using 'ionizeRule'.

  Formula formula(ionize_rule.formula());

  qDebug() << "The ionization formula is:" << formula.toString();

  double localMono = 0;
  double localAvg  = 0;

  IsotopicDataCstSPtr isotopic_data_csp =
    mcsp_polChemDef->getIsotopicDataCstSPtr();

  // Note the times(ionizeRule.level()) param to call below.
  if(!formula.accountMasses(
       isotopic_data_csp, &localMono, &localAvg, ionize_rule.level()))
    return -1;

  qDebug() << qSetRealNumberPrecision(6)
           << "Right after accounting the ionization masses:" << localMono
           << "-" << localAvg;

  // OK, the accounting of the masses worked ok, we can update the
  // values in the mono and avg params(note that we know that the
  // charge and level values of ionizeRule cannot be <= 0 because
  // otherwise the ionizRule would not have validated.

  qDebug()
    << qSetRealNumberPrecision(6)
    << "The ionizable masses before addition of the masses due to ionization:"
    << m_mono << "-" << m_avg;

  m_mono += localMono;
  m_avg += localAvg;

  qDebug() << qSetRealNumberPrecision(6)
           << "Right after having added the ionization masses:" << m_mono << "-"
           << m_avg;

  // If the ionization rule is for actually ionizing, then perform
  // the division.
  if(ionize_rule.level())
    {
      m_mono = m_mono / (ionize_rule.charge() * ionize_rule.level());
      m_avg  = m_avg / (ionize_rule.charge() * ionize_rule.level());

      qDebug() << qSetRealNumberPrecision(6)
               << "And now true m/z values:" << m_mono << "-" << m_avg;

      m_isIonized = true;
    }
  else
    {
      m_isIonized = false;
    }

  // Update the internal IonizeRule.
  m_ionizeRule = ionize_rule;

  //   qDebug() << __FILE__ << __LINE__
  // 	    << "After ionization: Mono:" << *mono << "Avg:" << avg;

  // If something changed in the masses, then return 1, otherwise
  // return 0.
  if(temp != *this)
    return 1;

  return 0;
}


/*!
\brief  Ionizes the \a ionizable using \a ionize_rule.

The following logic is applied:

\list 1
\li If \a ionize_rule is not valid, this function returns -1 because it
does not make sense to try to change the charge of an Ionizable if that
ionization rule is not valid.
\li If \a ionizable is ionized, first \l{deionize()} it. If this step fails,
returns -1.
\li The ionization process is carried out. If the process is successful, 1 is
returned, otherwise -1 is returned.
\endlist

\sa ionize()
*/
int
Ionizable::ionize(Ionizable *ionizable, const IonizeRule &ionize_rule)
{
  if(!ionize_rule.isValid())
    return -1;

  // If *this Ionizable is ionized, first deionize it with its own
  // m_ionizeRule, so that we get back to M, as we would say in
  // front of a mass spectrometer.

  if(ionizable->m_isIonized)
    {
      if(!ionizable->deionize())
        return -1;
      else
        // Make clear that at this point we are not ionized.
        ionizable->m_isIonized = false;
    }

  // At this point perform the ionization proper using 'ionizeRule'.

  Formula formula(ionizable->ionizeRule()->formula());

  Ponderable temp(*ionizable);

  double localMono = 0;
  double localAvg  = 0;

  IsotopicDataCstSPtr isotopic_data_csp =
    ionizable->getPolChemDefCstSPtr()->getIsotopicDataCstSPtr();

  // Note the times(ionizeRule.level()) param to call below.
  if(!formula.accountMasses(
       isotopic_data_csp, &localMono, &localAvg, ionize_rule.level()))
    return -1;

  // OK, the accounting of the masses worked ok, we can update the
  // values in the mono and avg params(note that we know that the
  // charge and level values of ionizeRule cannot be <= 0 because
  // otherwise the ionizRule would not have validated.

  ionizable->m_mono += localMono;
  ionizable->m_avg += localAvg;

  // If the ionization rule is for actually ionizing, then perform
  // the division.
  if(ionize_rule.level())
    {
      ionizable->m_mono =
        ionizable->m_mono / (ionize_rule.charge() * ionize_rule.level());

      ionizable->m_avg =
        ionizable->m_avg / (ionize_rule.charge() * ionize_rule.level());

      ionizable->m_isIonized = true;
    }
  else
    {
      ionizable->m_isIonized = false;
    }

  // Update the internal IonizeRule.
  ionizable->m_ionizeRule = ionize_rule;

  //   qDebug() << __FILE__ << __LINE__
  // 	    << "After ionization: Mono:" << *mono << "Avg:" << avg;

  // If something changed in the masses, then return 1, otherwise
  // return 0.
  if(temp != *ionizable)
    return 1;

  return 0;
}


/*!
\brief  Deionizes this Ionizable.

This Ionizable is deionized using its member m_ionizeRule.
The following logic is applied:

\list 1
\li If this Ionizable is not ionized, this function returns 0.
\li If the member ionizeRule is not valid, this function returns -1 because it
does not make sense to try to change the ionization of an Ionizable if its
member IonizeRule is not valid.
\li
\li The deionization process is carried out. If the process is successful, 1 is
returned, otherwise -1 is returned.
\endlist
*/
int
Ionizable::deionize()
{
  if(!m_isIonized)
    {
      // The Ionizable is not ionized, nothing to do, return true.
      return 0;
    }

  Ponderable temp(*this);

  qDebug() << "this before deionizing:" << toString();


  // At this point we know the Ionizable is ionized, thus it is an
  // error that m_ionizeRule is not valid.

  if(!m_ionizeRule.isValid())
    return -1;

  // Now, reverse the usual M+zH/z(proteins) stuff.

  double localMono = m_mono * abs(m_ionizeRule.charge() * m_ionizeRule.level());

  qDebug() << "set to a variable the m_mono * ion charge:" << localMono;

  double localAvg = m_avg * abs(m_ionizeRule.charge() * m_ionizeRule.level());

  qDebug() << "set to a variable the m_avg * ion charge:" << localAvg;

  Formula formula(m_ionizeRule.formula());

  qDebug() << "The ionizerule formula:" << formula.toString();

  IsotopicDataCstSPtr isotopic_data_csp =
    mcsp_polChemDef->getIsotopicDataCstSPtr();

  // Note the negated 'times'(- m_ionizeRule.level())param to call
  // below so that we revert the chemical action that led level
  // times to the ionization of the analyte. Note that we do not
  // have any compound(level * charge) because we are dealing with
  // matter here, not charges, thus only 'level' is to be taken into
  // consideration.

  if(!formula.accountMasses(
       isotopic_data_csp, &localMono, &localAvg, -m_ionizeRule.level()))
    return -1;

  qDebug() << "After having accounted the ionization formula masses into the "
              "previously computed mono and avg masses:"
           << localMono << "-" << localAvg;

  // At this point we can update the member masses;

  m_mono = localMono;
  m_avg  = localAvg;

  m_isIonized = false;

  // If something changed in the masses, then return 1, otherwise
  // return 0.
  if(temp != *this)
    return 1;

  return 0;
}

/*!
\brief Returns the molecular mass (either monoisotopic or average, depending
on \a mass_type)

A copy of this Ionizable is first made, then it is deionized and its mass is
returned.
*/
double
Ionizable::molecularMass(MassType mass_type)
{
  Ionizable temp(*this);

  if(!temp.deionize())
    return -1;

  return temp.mass(mass_type);
}

/*!
\brief Validates this Ionizable.

The member IonizeRule needs to validate successfully.

Returns true if the validation is successfull, false otherwise.
*/
bool
Ionizable::validate()
{
  int tests = 0;

  tests += PolChemDefEntity::validate();

  // If this Ionizable is ionized, then it is an error if the
  // m_ionizeRule is not valid !
  tests += (m_isIonized && m_ionizeRule.isValid());

  if(tests < 2)
    return false;

  return true;
}


/*!
\brief Returns true if this Ionizable is identical to \a other, false otherwise.
*/
bool
Ionizable::operator==(const Ionizable &other) const
{
  int tests = 0;

  tests += (m_mono == other.m_mono);
  tests += (m_avg == other.m_avg);
  tests += m_ionizeRule == other.m_ionizeRule;
  tests += m_isIonized == other.m_isIonized;

  if(tests < 4)
    return false;

  return true;
}


/*!
\brief Returns true if this Ionizable is different than \a other, false
otherwise.
*/
bool
Ionizable::operator!=(const Ionizable &other) const
{
  int tests = 0;

  tests += (m_mono != other.m_mono);
  tests += (m_avg != other.m_avg);
  tests += m_ionizeRule != other.m_ionizeRule;
  tests += m_isIonized != other.m_isIonized;

  if(tests > 0)
    return true;

  return false;
}


/*!
\brief  Calculates the masses of this Ionizable.

If this Ionizable is ionized, the masses of the \l Ponderable base class are
let unchanged. If this Ionizable is not ionized and the IonizeRule is valid,
perform the ionization, which calculates the masses.

This function returns false if the member IonizeRule is not valid or if the
ionization failed, true otherwise.

\sa ionize()
*/
bool
Ionizable::calculateMasses()
{
  if(!Ponderable::calculateMasses())
    return false;

  if(m_isIonized)
    {
      // Because the Ionizable is ionized, we have nothing more to
      // do.

      return true;
    }
  else
    {
      // The Ionizable is not ionized. If the IonizeRule is valid,
      // then we just ionize it.
      if(m_ionizeRule.isValid())
        {
          if(ionize() == -1)
            return false;
          else
            return true;
        }
    }

  // We should not be here.
  return false;
}

/*!
\brief Returns a string describing this Ionizable.
*/
QString
Ionizable::toString()
{
  QString text;

  text += m_ionizeRule.toString();
  text += "\n";
  text += Ponderable::monoString(6);
  text += "-";
  text += Ponderable::avgString(6);
  text += "\n";
  text += (m_isIonized ? "ionized" : "not ionized");
  text += "\n";

  return text;
}

} // namespace libXpertMass

} // namespace MsXpS
