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

/////////////////////// Local includes
#include <minexpert/nongui/History.hpp>


namespace msXpSmineXpert
{


//! Construct an emtpy History instance.
/*!

*/
History::History()
{
}

//! Construct a History instance as the copy of another.
/*!

  The copy is deep, with all the HistoryItem instances referenced in \c
  m_historyItemList being copied to newly allocated ones.

  \param other reference to another History instance to be used for the copy.

  \sa History()

*/
History::History(const History &other)
{
  for(int iter = 0; iter < other.m_historyItemList.size(); ++iter)
    m_historyItemList.append(
      new HistoryItem(*(other.m_historyItemList.at(iter))));

  m_mzIntegrationParams = other.m_mzIntegrationParams;
}


//! Assignment operator.
/*!

  Copies all the member data of the other History item into \c this instance.
  The copying is deep, with all the items referenced in \c m_historyItemList
  being coied to newly allocated ones.

  \param other reference to the other History instance to be used for the copy.

  \return a reference to \c this History instance.

*/
History &
History::operator=(const History &other)
{
  if(&other == this)
    return *this;

  m_mzIntegrationParams.reset();
  m_mzIntegrationParams = other.m_mzIntegrationParams;

  freeList();
  for(int iter = 0; iter < other.m_historyItemList.size(); ++iter)
    m_historyItemList.append(
      new HistoryItem(*(other.m_historyItemList.at(iter))));

  return *this;
}


//! Destruct this History instance.
/*!

  All the items in the member \c m_historyItemList are deleted.

  \sa freeList()

*/
History::~History()
{
  freeList();
}


//! Tell if the member list of HistoryItem instances is empty.
bool
History::isEmpty()
{
  return m_historyItemList.isEmpty();
}


void
History::setMzIntegrationParams(MzIntegrationParams params)
{
  m_mzIntegrationParams = params;
}


MzIntegrationParams
History::mzIntegrationParams() const
{
  return m_mzIntegrationParams;
}


//! Deletes all the items in the History item list.
/*!

*/
void
History::freeList()
{
  while(m_historyItemList.size())
    delete m_historyItemList.takeFirst();

  m_historyItemList.clear();
}


//! Tell if History contains a HistoryItem.
/*!

  Iterates in \c m_historyItemList and for each HistoryItem checks if it is
  identical to \p other. Note that the comparison is deep, that is, the values
  inside the member data are tested.

  \param other HistoryItem instance to be searched for in this History item.

  \return true if a HistoryItem instance was found identical to \p other.

  \sa HistoryItem::operator==()

*/
int
History::containsHistoryItem(const HistoryItem &item)
{
  for(int iter = 0; iter < m_historyItemList.size(); ++iter)
    {
      HistoryItem *curItem = m_historyItemList.at(iter);

      if(*curItem == item)
        return iter;
    }

  return -1;
}


//! Get the first HistoryItem that has a specific IntegrationType.
/*!

  Iterate in m_historyItemList in search for a HistoryItem having the same
  IntegrationType as \p intType. As soon as one is found its IntegrationRange
  pointer is set to \p intRange.

  \param intType IntegrationType value to search for.

  \param intRange slot to set the pointer to the IntegrationRange instance
  pointer of the HistoryItem having the right IntegrationType.

  \return a pointer to the HistoryItem instance containing an IntegrationType
  \p intType.

  \sa lastHistoryItem()

*/
HistoryItem *
History::firstHistoryItem(int intType, IntegrationRange **intRange)
{
  // We want to return the first item that has integrationType.
  // We iterate in normal order because we want to return the first added
  // item.

  for(int iter = 0; iter < m_historyItemList.size(); ++iter)
    {
      HistoryItem *item = m_historyItemList.at(iter);

      if(item->hasIntegrationType(intType))
        {
          if(intRange != Q_NULLPTR)
            *intRange = item->integrationRange(intType);

          return item;
        }
    }

  return Q_NULLPTR;
}


//! Get the last HistoryItem that has a specific IntegrationType.
/*!

  Reverse-iterate in m_historyItemList in search for a HistoryItem having the
  same IntegrationType as \p intType. As soon as one is found its
  IntegrationRange pointer is set to \p intRange.

  \param intType IntegrationType value to search for.

  \param intRange slot to set the pointer to the IntegrationRange instance
  pointer of the HistoryItem having the right IntegrationType.

  \return a pointer to the HistoryItem instance containing an IntegrationType
  \p intType.

  \sa firstHistoryItem().

*/
HistoryItem *
History::lastHistoryItem(int intType, IntegrationRange **intRange)
{
  // We want to return the last item that has integration type intType.
  // We iterate in reverse order because we want to return the last added
  // item.

  for(int iter = m_historyItemList.size() - 1; iter >= 0; --iter)
    {
      HistoryItem *item = m_historyItemList.at(iter);

      if(item->hasIntegrationType(intType))
        {
          if(intRange != Q_NULLPTR)
            *intRange = item->integrationRange(intType);

          return item;
        }
    }

  return Q_NULLPTR;
}


//! Return the most recent HistoryItem instance in \c this History.
/*!

  Iterates in \c m_historyItemList and checks for the most recent HistoryItem
  element of the list.

  \return The most recent HistoryItem instance in \c this History.

*/
const HistoryItem *
History::newestHistoryItem() const
{
  if(m_historyItemList.isEmpty())
    {
      return Q_NULLPTR;
    }

  // Seed the holder time from the first history item and set the index to
  // that first history item. Most probably, that will be the oldest-dated
  // item because history items are appended to the list as the time goes
  // by.

  QDateTime newestDateTime = m_historyItemList.first()->m_dateTime;
  int newestDateTimeIndex  = 0;

  // qDebug() << __FILE__ << __LINE__ << __FUNCTION__
  //<< "First history item:" << m_historyItemList.first()->asText();

  for(int iter = 1; iter < m_historyItemList.size(); ++iter)
    {
      // qDebug() << __FILE__ << __LINE__ << __FUNCTION__
      //<< "iterating in second history item.";

      HistoryItem *historyItem = m_historyItemList.at(iter);

      QDateTime iterDateTime = historyItem->m_dateTime;

      if(iterDateTime > newestDateTime)
        {
          // qDebug() << __FILE__ << __LINE__ << __FUNCTION__
          //<< "This history item is more recent:"
          //<< historyItem->asText();

          newestDateTime      = iterDateTime;
          newestDateTimeIndex = iter;
        }
    }

  return m_historyItemList.at(newestDateTimeIndex);
}


//! Copy a History instance to \c this History.
/*!

  The \p history History instance is copied deeply to \c this History.
  HistoryItem elements are appended to \c this History. Upon copying, if
  HistoryItem elements of \p history are found in \c this History, they are
  not duplicated if \p noDuplicates is true.

  \param history reference to History to be copied into \c this History.

  \param noDuplicates boolean value telling if HistoryItem element in \p
  history found in \c this History may be duplicated or not.

  \sa appendHistoryItem()

*/
void
History::copyHistory(const History &history, bool noDuplicates)
{

  // qDebug() << __FILE__ << __LINE__
  //<< "the history to be appended:" << history.asText();

  for(int iter = 0; iter < history.m_historyItemList.size(); ++iter)
    {
      HistoryItem *item = history.m_historyItemList.at(iter);

      if(containsHistoryItem(*item) >= 0 && noDuplicates)
        {
          qDebug() << __FILE__ << __LINE__
                   << "Not appending history item as it would be duplicated.";
        }
      else
        {
          HistoryItem *newItem = new HistoryItem(*item);
          m_historyItemList.append(newItem);
        }
    }

  // Finally, the m/z integration parameters
  m_mzIntegrationParams = history.m_mzIntegrationParams;
}


//! Append a HistoryItem instance to \c this History.
/*!

  The \p item HistoryItem instance pointer is appended to \c m_historyItemList.
  Ownership of the HistoryItem instance is transferred to \c this History.

  \param item pointer to the HistoryItem instance to take ownership of.

  \return The index of the appended item.

  \sa duplicateHistoryItem().

*/
int
History::appendHistoryItem(HistoryItem *item)
{
  if(item == Q_NULLPTR)
    qFatal("Fatal error at %s@%d. Program aborted.", __FILE__, __LINE__);

  m_historyItemList.append(item);

  return m_historyItemList.size() - 1;
}


//! Copy a HistoryItem into \c this History.
/*!

  The \p item HistoryItem is copied to a newly allocated item that is then
  appended to \c this History. Note that there is no check to verify if \c
  this History contains a HistoryItem instance identical to \p item.

  \param item reference to the HistoryItem to be copied into \c this History.

  \return the index of the newly appended item.

  \sa appendHistoryItem()

*/
int
History::copyHistoryItem(const HistoryItem &item)
{
  HistoryItem *newItem = new HistoryItem(item);
  m_historyItemList.append(newItem);

  return m_historyItemList.size() - 1;
}


//! Search a RT range in the HistoryItem elements of \c this History.
/*!

  Iterate in m_historyItemList and for each HistoryItem check if they have an
  IntegrationRange of type RT_*.  While iterating, there might be more than
  one HistoryItem having IntegrationRange of type RT_*. Each time one is
  found, its IntegrationRange members start and end are checked and the
  HistoryItem having the IntegrationRange greatest \c start value and the
  smallest \c end value is retained. These "innermost" range \c start and \c
  end values are copied into \p start and \p end double value pointers passed
  as parameters.

  \param start pointer to double to receive the range \c start value.

  \param end pointer to double to receive the range \c end value.

  \return true if a HistoryItem of IntegrationType RT* was found, false
  otherwise.

  \sa innermostMzRange()

  \sa innermostDtRange()

*/
bool
History::innermostRtRange(double *start, double *end) const
{
  if(start == Q_NULLPTR || end == Q_NULLPTR)
    qFatal("Fatal error at %s@%d. Program aborted.", __FILE__, __LINE__);

  double tempStart = -1;
  double tempEnd   = -1;

  bool itemFound = false;
  bool firstItem = true;

  for(int iter = 0; iter < m_historyItemList.size(); ++iter)
    {
      HistoryItem *item = m_historyItemList.at(iter);

      // A history item might contain more than one integration range.
      // Ask a list of these.

      QList<IntegrationRange *> intRangeList = item->integrationRanges();

      for(int iter = 0; iter < intRangeList.size(); ++iter)
        {
          IntegrationRange *intRange = intRangeList.at(iter);
          int integType              = item->integrationType(intRange);

          if(integType & IntegrationType::RT_TO_ANY)
            {
              if(firstItem)
                {
                  tempStart = intRange->start;
                  tempEnd   = intRange->end;

                  firstItem = false;
                }
              else
                {
                  // We seek the *innermost* range, so be aware of the
                  // relational < > operators that may look used at reverse but
                  // are not.

                  if(intRange->start > tempStart)
                    tempStart = intRange->start;

                  if(intRange->end < tempEnd)
                    tempEnd = intRange->end;
                }

              // QString matchingBin= QString("%1").arg(integType, 0, 2);
              // QString matchedBin=
              // QString("%1").arg(IntegrationType::DT_TO_ANY, 0, 2);

              // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()"
              //<< "new integration range"
              //<< integType
              //<< "(" <<
              //HistoryItem::m_integrationTypeDescriptionMap.value(integType)->brief
              //<< ")"
              //"<< matches" << IntegrationType::DT_TO_ANY << "bin:" <<
              //matchingBin
              //<< "(" <<
              //HistoryItem::m_integrationTypeDescriptionMap.value(IntegrationType::RT_TO_ANY)->brief
              //<< ")"
              //<< "bin:" << matchedBin
              //<< "with tempStart:" << tempStart << "tempEnd:" << tempEnd;

              // int result = item->integrationType(intRange) &
              // IntegrationType::RT_TO_ANY; qDebug() << __FILE__ << __LINE__ <<
              // __FUNCTION__ << "()" "result:" << result << "bin:" <<
              //QString("%1").arg(result, 0, 2);


              // Let the caller know that we found at least one item and thus
              // that the start and end params now have the encountered
              // values.

              itemFound = true;
            }
          else
            // The returned intRange is Q_NULLPTR.
            continue;
        }
    }
  // We have finally iterated in all the HistoryItem elements and we can
  // return the range values.
  *start = tempStart;
  *end   = tempEnd;

  return itemFound;
}


//! Search a MZ range in the HistoryItem elements of \c this History.
/*!

  Iterate in m_historyItemList and for each HistoryItem check if they have an
  IntegrationRange of type MZ_*.  While iterating, there might be more than
  one HistoryItem having IntegrationRange of type MZ_*. Each time one is
  found, its IntegrationRange members start and end are checked and the
  HistoryItem having the IntegrationRange greatest \c start value and the
  smallest \c end value is retained. These "innermost" range \c start and \c
  end values are copied into \p start and \p end double value pointers passed
  as parameters.

  \param start pointer to double to receive the range \c start value.

  \param end pointer to double to receive the range \c end value.

  \return true if a HistoryItem of IntegrationType MZ* was found, false
  otherwise.

  \sa innermostRtRange()

  \sa innermostDtRange()

*/
bool
History::innermostMzRange(double *start, double *end) const
{
  if(start == Q_NULLPTR || end == Q_NULLPTR)
    qFatal("Fatal error at %s@%d. Program aborted.", __FILE__, __LINE__);

  double tempStart = -1;
  double tempEnd   = -1;

  bool itemFound = false;
  bool firstItem = true;

  for(int iter = 0; iter < m_historyItemList.size(); ++iter)
    {
      HistoryItem *item = m_historyItemList.at(iter);

      // A history item might contain more than one integration range.
      // Ask a list of these.

      QList<IntegrationRange *> intRangeList = item->integrationRanges();

      for(int iter = 0; iter < intRangeList.size(); ++iter)
        {
          IntegrationRange *intRange = intRangeList.at(iter);
          int integType              = item->integrationType(intRange);

          if(integType & IntegrationType::MZ_TO_ANY)
            {
              if(firstItem)
                {
                  tempStart = intRange->start;
                  tempEnd   = intRange->end;

                  firstItem = false;
                }
              else
                {

                  // We seek the *innermost* range, so be aware of the
                  // relational < > operators that may look used at reverse but
                  // are not.

                  if(intRange->start > tempStart)
                    tempStart = intRange->start;

                  if(intRange->end < tempEnd)
                    tempEnd = intRange->end;
                }

              // QString matchingBin= QString("%1").arg(integType, 0, 2);
              // QString matchedBin=
              // QString("%1").arg(IntegrationType::DT_TO_ANY, 0, 2);

              // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()"
              //<< "new integration range"
              //<< integType
              //<< "(" <<
              //HistoryItem::m_integrationTypeDescriptionMap.value(integType)->brief
              //<< ")"
              //"<< matches" << IntegrationType::DT_TO_ANY << "bin:" <<
              //matchingBin
              //<< "(" <<
              //HistoryItem::m_integrationTypeDescriptionMap.value(IntegrationType::MZ_TO_ANY)->brief
              //<< ")"
              //<< "bin:" << matchedBin
              //<< "with tempStart:" << tempStart << "tempEnd:" << tempEnd;

              // int result = item->integrationType(intRange) &
              // IntegrationType::MZ_TO_ANY; qDebug() << __FILE__ << __LINE__ <<
              // __FUNCTION__ << "()" "result:" << result << "bin:" <<
              //QString("%1").arg(result, 0, 2);


              // Let the caller know that we found at least one item and thus
              // that the start and end params now have the encountered
              // values.

              itemFound = true;
            }
        }
      // End of
      // for(int iter = 0; iter < intRangeList.size(); ++iter)
    }
  // End of
  // for(int iter = 0; iter < m_historyItemList.size(); ++iter)

  // We have finally iterated in all the HistoryItem elements and we can
  // return the range values.
  *start = tempStart;
  *end   = tempEnd;

  // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()"
  //<< "start: " << *start << "end:" << *end << "item was found:" << itemFound;

  return itemFound;
}


//! Search a DT range in the HistoryItem elements of \c this History.
/*!

  Iterate in m_historyItemList and for each HistoryItem check if they have an
  IntegrationRange of type DT_*.  While iterating, there might be more than
  one HistoryItem having IntegrationRange of type DT_*. Each time one is
  found, its IntegrationRange members start and end are checked and the
  HistoryItem having the IntegrationRange greatest \c start value and the
  smallest \c end value is retained. These "innermost" range \c start and \c
  end values are copied into \p start and \p end double value pointers passed
  as parameters.

  \param start pointer to double to receive the range \c start value.

  \param end pointer to double to receive the range \c end value.

  \return true if a HistoryItem of IntegrationType DT* was found, false
  otherwise.

  \sa innermostMzRange()

  \sa innermostRtRange()

*/
bool
History::innermostDtRange(double *start, double *end) const
{
  if(start == Q_NULLPTR || end == Q_NULLPTR)
    qFatal("Fatal error at %s@%d. Program aborted.", __FILE__, __LINE__);

  double tempStart = -1;
  double tempEnd   = -1;

  bool itemFound = false;
  bool firstItem = true;

  for(int iter = 0; iter < m_historyItemList.size(); ++iter)
    {
      HistoryItem *item = m_historyItemList.at(iter);

      // qDebug() << __FILE__ << __LINE__ << "Iterating in new history item.";

      // A history item might contain more than one integration range.
      // Ask a list of these.

      QList<IntegrationRange *> intRangeList = item->integrationRanges();

      for(int iter = 0; iter < intRangeList.size(); ++iter)
        {
          IntegrationRange *intRange = intRangeList.at(iter);
          int integType              = item->integrationType(intRange);

          if(integType & IntegrationType::DT_TO_ANY)
            {
              if(firstItem)
                {
                  tempStart = intRange->start;
                  tempEnd   = intRange->end;

                  firstItem = false;
                }
              else
                {

                  // We seek the *innermost* range, so be aware of the
                  // relational < > operators that may look used at reverse but
                  // are not.

                  if(intRange->start > tempStart)
                    tempStart = intRange->start;

                  if(intRange->end < tempEnd)
                    tempEnd = intRange->end;
                }

              // QString matchingBin= QString("%1").arg(integType, 0, 2);
              // QString matchedBin=
              // QString("%1").arg(IntegrationType::DT_TO_ANY, 0, 2);

              // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()"
              //<< "new integration range"
              //<< integType
              //<< "(" <<
              //HistoryItem::m_integrationTypeDescriptionMap.value(integType)->brief
              //<< ")"
              //"<< matches" << IntegrationType::DT_TO_ANY << "bin:" <<
              //matchingBin
              //<< "(" <<
              //HistoryItem::m_integrationTypeDescriptionMap.value(IntegrationType::DT_TO_ANY)->brief
              //<< ")"
              //<< "bin:" << matchedBin
              //<< "with tempStart:" << tempStart << "tempEnd:" << tempEnd;

              // int result = item->integrationType(intRange) &
              // IntegrationType::DT_TO_ANY; qDebug() << __FILE__ << __LINE__ <<
              // __FUNCTION__ << "()" "result:" << result << "bin:" <<
              //QString("%1").arg(result, 0, 2);


              // Let the caller know that we found at least one item and thus
              // that the start and end params now have the encountered
              // values.

              itemFound = true;
            }
        }
      // End of
      // for(int iter = 0; iter < intRangeList.size(); ++iter)
    }
  // End of
  // for(int iter = 0; iter < m_historyItemList.size(); ++iter)

  // We have finally iterated in all the HistoryItem elements and we can
  // return the range values.
  *start = tempStart;
  *end   = tempEnd;

  return itemFound;
}


//! Search an integration range in the HistoryItem elements of \c this History.
/*!

  The IntegrationRange must be for the last history item added to the history,
  as determined by looking at the date time member datum.  The found
  IntegrationRange \c start and \c end values are then copied to \p start and
  \p end parameters.

  This function is actually a dispatcher function calling specialized
  functions (see the See also section below).

  \param start pointer to double to receive the range \c start value.

  \param end pointer to double to receive the range \c end value.

  \return true if a HistoryItem was found, false otherwise.

  \sa innermostRtRange()

  \sa innermostMzRange()

  \sa innermostDtRange()

*/
bool
History::innermostRange(int integrationType, double *start, double *end) const
{
  if(integrationType & IntegrationType::RT_TO_ANY)
    return innermostRtRange(start, end);
  else if(integrationType & IntegrationType::MZ_TO_ANY)
    return innermostMzRange(start, end);
  else if(integrationType & IntegrationType::DT_TO_ANY)
    return innermostDtRange(start, end);

  return false;
}


//! Craft a string with a textual representation of integration ranges.
/*!

  For each kind of IntegrationRange (RT, MZ, DT), gets the innermost range out
  of \c this History and appends a textual representation of that integration
  range. The whole crafted string is returned.

  \return As string with all the textual representations of the found
  integration innermost ranges.

  \sa innermostRange()

*/
QString
History::innermostRangesAsText() const
{
  QString text = "History - innermost ranges:\n";

  double startValue;
  double endValue;

  if(innermostRtRange(&startValue, &endValue))
    text += QString("RT range: [%1-%2]\n")
              .arg(startValue, 0, 'f', 3)
              .arg(endValue, 0, 'f', 3);
  if(innermostMzRange(&startValue, &endValue))
    text += QString("MZ range: [%1-%2]\n")
              .arg(startValue, 0, 'f', 5)
              .arg(endValue, 0, 'f', 5);
  if(innermostDtRange(&startValue, &endValue))
    text += QString("DT range: [%1-%2]\n")
              .arg(startValue, 0, 'f', 10)
              .arg(endValue, 0, 'f', 10);

  return text;
}


//! Tells if \c this History is valid.
/*!

  A History is valid if all of its HistoryItem elements are valid.

  \return true if \c this History is valid; false otherwise.

  \sa

*/
bool
History::isValid() const
{
  for(int iter = 0; iter < m_historyItemList.size(); ++iter)
    {
      if(!m_historyItemList.at(iter)->isValid())
        return false;
    }

  return true;
}


//! Craft a string with a textual representation of \c this History
/*!

  Iterate in all the HistoryItem elements of \c this History and get, for each
  element, a string with a textual representation of its contents. Append that
  string to a local string.

  \param header string to be used as a header in the final returned string

  \param footer string to be used as a footer in the final returned string

  \param brief boolean value telling if the brief description of the
  IntegrationType is to be used. The detailed description is used if that
  boolean value is false.

  \return the string with a textual representation of \c this History.

  \sa

*/
QString
History::asText(const QString &header, const QString &footer, bool brief) const
{
  QString text = header;

  text += m_mzIntegrationParams.asText();

  for(int iter = 0; iter < m_historyItemList.size(); ++iter)
    text += m_historyItemList.at(iter)->asText(brief);

  text += footer;

  return text;
}


} // namespace msXpSmineXpert
