/* BEGIN software license
 *
 * MsXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright 2009--2026 by 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 indepstopent 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>
#include <QStringList>
#include <QRegularExpression>
#include <QRegularExpressionMatch>

/////////////////////// Local includes
#include "MsXpS/libXpertMassCore/globals.hpp"
#include "MsXpS/libXpertMassCore/Utils.hpp"
#include "MsXpS/libXpertMassCore/IndexRangeCollection.hpp"

namespace MsXpS
{
namespace libXpertMassCore
{

/*!
\class MsXpS::libXpertMassCore::IndexRangeCollection
\inmodule libXpertMassCore
\ingroup XpertMassCalculations
\inheaderfile IndexRangeCollection.hpp

\brief The IndexRangeCollection class provides a collection of IndexRange
instances that enable delimiting \l{Sequence} regions of interest in a given
\l{Polymer} instance.

\sa IndexRange
*/


/*!
\variable MsXpS::libXpertMassCore::IndexRangeCollection::m_ranges

\brief The container of \l{IndexRange} instances.
*/

/*!
\variable MsXpS::libXpertMassCore::IndexRangeCollection::m_comment

\brief A comment that can be associated to this IndexRangeCollection instance..
*/

/*!
\brief Constructs an empty IndexRangeCollection instance.
*/
IndexRangeCollection::IndexRangeCollection(QObject *parent): QObject(parent)
{
}

/*!
\brief Constructs an IndexRangeCollection instance with a single IndexRange
object using \a index_start and \a index_stop.
*/
IndexRangeCollection::IndexRangeCollection(qsizetype index_start,
                                           qsizetype index_stop,
                                           QObject *parent)
  : QObject(parent)
{
  IndexRange *index_range_p = new IndexRange(index_start, index_stop, this);

  m_ranges.append(index_range_p);
}

/*!
\brief Constructs an IndexRangeCollection instance using \a index_ranges_string

\sa parseIndexRanges()
*/
IndexRangeCollection::IndexRangeCollection(const QString &index_ranges_string,
                                           Enums::LocationType location_type,
                                           QObject *parent)
{
  QList<IndexRange *> index_ranges = IndexRangeCollection::parseIndexRanges(
    index_ranges_string, location_type, parent);

  setIndexRanges(index_ranges);

  qDebug() << "With text" << index_ranges_string
           << "initialized this:" << indicesAsText();
}

/*!
\brief Constructs an IndexRangeCollection as a copy of \a other.
*/
IndexRangeCollection::IndexRangeCollection(const IndexRangeCollection &other,
                                           QObject *parent)
  : QObject(parent), m_comment(other.m_comment)
{
  qDebug() << "Pseudo copy constructing IndexRangeCollection"
           << other.indicesAsText();
  foreach(const IndexRange *item, other.m_ranges)
    m_ranges.append(new IndexRange(item->m_start, item->m_stop, this));
  qDebug() << "After copying IndexRangeCollection, this:"
           << this->indicesAsText();
}

/*!
\brief Destructs this IndexRangeCollection instance.

The \l IndexRangeCollection instances are freed.
*/
IndexRangeCollection::~IndexRangeCollection()
{
}

/*!
\brief Initializes this IndexRangeCollection instance using \a other and returns
a reference to this instance.
*/
IndexRangeCollection &
IndexRangeCollection::initialize(const IndexRangeCollection &other)
{
  m_comment = other.m_comment;

  qDeleteAll(m_ranges);
  m_ranges.clear();

  foreach(const IndexRange *item, m_ranges)
    m_ranges.append(new IndexRange(*item, this));

  return *this;
}

/*!
 \brief Returns a newly allocated IndexRangeCollection instance with parent set
 to \a parent.
 */
IndexRangeCollection *
IndexRangeCollection::clone(QObject *parent)
{
  IndexRangeCollection *copy_p = new IndexRangeCollection(parent);

  foreach(const IndexRange *item, m_ranges)
    copy_p->m_ranges.append(new IndexRange(*item, copy_p));

  return copy_p;
}

/*!
 \ b*rief Returns a newly allocated IndexRangeCollection instance initialized
 using \a other and with parent set to \a parent.
 */
IndexRangeCollection *
IndexRangeCollection::clone(const IndexRangeCollection &other, QObject *parent)
{
  IndexRangeCollection *copy_p = new IndexRangeCollection(parent);

  foreach(const IndexRange *item, other.m_ranges)
    copy_p->m_ranges.append(new IndexRange(*item, copy_p));

  return copy_p;
}

/*!
\brief Sets the comment to \a comment.
*/
void
IndexRangeCollection::setComment(const QString &comment)
{
  m_comment = comment;
}

/*!
\brief Returns the comment.
*/
QString
IndexRangeCollection::getComment() const
{
  return m_comment;
}

/*!
\brief Returns a const reference to the IndexRange container.
*/
const QList<IndexRange *> &
IndexRangeCollection::getRangesCstRef() const
{
  return m_ranges;
}

/*!
\brief Returns a reference to the IndexRange container.
*/
QList<IndexRange *> &
IndexRangeCollection::getRangesRef()
{
  return m_ranges;
}

/*!
\brief Parses \a index_ranges_string and returns a container with all the
IndexRange entities parsed and parentship set to \a parent.

If \a location_type is Enums::LocationType::POSITIION, the parsed values are
decremented by one unit to convert from Position to Index. The values stored in
the \c IndexRangeCollection are always indices.


If \a location_type is Enums::LocationType::INDEX, the parsed values are stored
as is.

The \a index_ranges_string might contain one or more substrings in
the format "[15-220]",  like "[0-20][0-30][10-50][15-80][25-100][30-100]".

If an error occurs, an empty container is returned.

A sanity check is performed: the counts of '[' and of ']' must be the same. At
the stop of the parse, the size of IndexRangeCollection must be same as the
count of
'['.

If that check fails, an empty container is returned.
*/
QList<IndexRange *>
IndexRangeCollection::parseIndexRanges(const QString &index_ranges_string,
                                       Enums::LocationType location_type,
                                       QObject *parent)
{
  qDebug() << "Parsing text:" << index_ranges_string;

  QString local_index_ranges_string = index_ranges_string;
  local_index_ranges_string = Utils::unspacify(local_index_ranges_string);

  //  Sanity check
  qsizetype opening_brackets_count = local_index_ranges_string.count('[');
  qsizetype closing_brackets_count = local_index_ranges_string.count(']');

  if(opening_brackets_count != closing_brackets_count)
    {
      qDebug() << "The string does not represent bona fide IndexRange "
                  "descriptions.";

      return QList<IndexRange *>();
    }

  QRegularExpression sub_regexp("\\[(\\d+)-(\\d+)\\]");

  bool ok = false;
  QList<IndexRange *> parsed_index_ranges;

  for(const QRegularExpressionMatch &match :
      sub_regexp.globalMatch(local_index_ranges_string))
    {
      QString sub_match = match.captured(0);

      qDebug() << "sub_match all captured" << sub_match;

      QString start_string = match.captured(1);
      QString stop_string  = match.captured(2);

      int start = start_string.toInt(&ok);

      if(!ok)
        qFatal() << "Failed to parse the IndexRange instance(s)";
      int stop = stop_string.toInt(&ok);

      if(!ok)
        qFatal() << "Failed to parse the IndexRange instance(s)";

      if(location_type == Enums::LocationType::POSITION)
        {
          // Convert from input positions to local indices
          --start;
          --stop;
        }

      qDebug() << "start and stop:" << start << "and" << stop;

      // << "These values will be sorted in ascending order upon IndexRange
      // construction";
      parsed_index_ranges.append(new IndexRange(start, stop, parent));
    }

  //  Sanity check
  if(parsed_index_ranges.size() != opening_brackets_count)
    {
      qDebug() << "The string does not represent bona fide IndexRange "
                  "descriptions.";
      parsed_index_ranges.clear();
    }

  qDebug() << "Could parse" << parsed_index_ranges.size() << "index ranges";

  return parsed_index_ranges;
}

//////////////// OPERATORS /////////////////////

/*!
 \brief Returns true if this IndexRangeCollection instance is identical to \a
 other, false otherwise.
 */
bool
IndexRangeCollection::operator==(const IndexRangeCollection &other) const
{
  if(m_comment != other.m_comment)
    return false;

  if(m_ranges.size() != other.m_ranges.size())
    return false;

  for(qsizetype iter = 0; iter < m_ranges.size(); ++iter)
    if(*m_ranges.at(iter) != *other.m_ranges.at(iter))
      return false;

  return true;
}

/*!
 \brief Returns true if this IndexRangeCollection is different than \a other,
 false otherwise.

 This funtion returns the negated result of operator==().
 */
bool
IndexRangeCollection::operator!=(const IndexRangeCollection &other) const
{
  return !operator==(other);
}

/*!
\brief Clears this IndexRangeCollection's container of IndexRange instances and
adds one IndexRange using \a start and \a stop.
*/
void
IndexRangeCollection::setIndexRange(qsizetype start, qsizetype stop)
{
  qDeleteAll(m_ranges);
  m_ranges.clear();

  m_ranges.push_back(new IndexRange(start, stop, this));
}

/*!
\brief Clears this IndexRangeCollection's container of IndexRange instances and
adds one \a index_range IndexRange.
*/
void
IndexRangeCollection::setIndexRange(const IndexRange &index_range)
{
  qDeleteAll(m_ranges);
  m_ranges.clear();

  m_ranges.push_back(new IndexRange(index_range, this));
}

/*!
\brief Clears this IndexRangeCollection's container of IndexRange instances and
adds the IndexRange instances contained in the \a index_ranges container.
*/
void
IndexRangeCollection::setIndexRanges(const QList<IndexRange *> &index_ranges)
{
  qDeleteAll(m_ranges);
  m_ranges.clear();

  foreach(const IndexRange *item, index_ranges)
    m_ranges.append(new IndexRange(*item, this));
}

/*!
\brief Clears this IndexRangeCollection's container of IndexRange and adds the
IndexRange instances contained in the \a index_ranges collection.
*/
void
IndexRangeCollection::setIndexRanges(const IndexRangeCollection &index_ranges)
{
  qDeleteAll(m_ranges);
  m_ranges.clear();

  foreach(const IndexRange *item, index_ranges.m_ranges)
    m_ranges.append(new IndexRange(*item, this));
}

/*!
\brief Creates the IndexRange instances based on \a index_ranges_string
and adds them to this IndexRange container.

\note The container of IndexRange instances is first emptied.

If there are not multiple regions, the format of the \a index_ranges_string
is:

\code
"[228-246]"
\endcode

If there are multiple regions (for example
when a cross-link exists), the format changes to account for the multiple
regions:

\code
"[228-246][276-282][247-275]"
\endcode

\note It is expected that the values in the index_ranges_string are \e
positions strings and not \e indices.

Returns the count of added IndexRange instances or -1 if an error occurred.
*/
qsizetype
IndexRangeCollection::setIndexRanges(const QString &index_ranges_string,
                                     Enums::LocationType location_type)
{
  // We get a string in the form [xxx-yyy] (there can be more than
  // one such element, if cross-linked oligomers are calculated, like
  // "[228-246][276-282][247-275]". Because that string comes from
  // outside code, it is expected to contains positions
  // and not indices. So we have to decrement the start and stop
  // values by one.

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

  QList<IndexRange *> index_ranges =
    parseIndexRanges(index_ranges_string, location_type);

  setIndexRanges(index_ranges);

  return size();
}

/*!
\brief Adds one IndexRange using \a start and \a stop.
*/
void
IndexRangeCollection::appendIndexRange(qsizetype start, qsizetype stop)
{
  m_ranges.append(new IndexRange(start, stop, this));
}

/*!
\brief Adds one IndexRange as a copy of \a index_range.
*/
void
IndexRangeCollection::appendIndexRange(const IndexRange &index_range)
{
  m_ranges.append(new IndexRange(index_range, this));
}

/*!
\brief Adds IndexRange instances as copies of the instances in \a
index_ranges.
*/
void
IndexRangeCollection::appendIndexRanges(const QList<IndexRange *> &index_ranges)
{
  foreach(const IndexRange *item, index_ranges)
    m_ranges.append(new IndexRange(*item, this));
}

/*!
\brief Adds IndexRange instances as copies of the instances in \a
index_ranges.
*/
void
IndexRangeCollection::appendIndexRanges(
  const IndexRangeCollection &index_ranges)
{
  foreach(const IndexRange *item, index_ranges.m_ranges)
    m_ranges.append(new IndexRange(*item, this));
}

//////////////// ACCESSING FUNCTIONS /////////////////////
/*!
\brief Returns a const reference to the IndexRange at \a index.

An index that is out of bounds is fatal.
*/
const IndexRange &
IndexRangeCollection::getRangeCstRefAt(qsizetype index) const
{
  if(index >= size())
    qFatal() << "Programming error. Index is out of bounds.";

  return *m_ranges.at(index);
}

/*!
\brief Returns reference to the IndexRange at \a index.

An index that is out of bounds is fatal.
*/
IndexRange &
IndexRangeCollection::getRangeRefAt(qsizetype index)
{
  if(index >= size())
    qFatal() << "Programming error. Index is out of bounds.";

  return *m_ranges.at(index);
}

/*!
\brief Returns reference to the IndexRange at \a index.

An index that is out of bounds is fatal.
*/
IndexRange *
IndexRangeCollection::getRangeAt(qsizetype index)
{
  if(index < 0 || index >= size())
    qFatal() << "Programming error. Index is out of bounds.";

  return m_ranges.at(index);
}

/*!
\brief Returns a constant iterator to the IndexRange at \a index.

An index that is out of bounds is fatal.
*/
const QList<IndexRange *>::const_iterator
IndexRangeCollection::getRangeCstIteratorAt(qsizetype index) const
{
  if(index < 0 || index >= size())
    qFatal() << "Programming error. Index is out of bounds.";

  return m_ranges.cbegin() + index;
}

/*!
\brief Returns an iterator to the IndexRange at \a index.

An index that is out of bounds is fatal.
*/
const QList<IndexRange *>::iterator
IndexRangeCollection::getRangeIteratorAt(qsizetype index)
{
  if(index < 0 || index >= size())
    qFatal() << "Programming error. Index is out of bounds.";

  return m_ranges.begin() + index;
}

/*!
\brief Returns the left-most start value throughout all the IndexRange
instances.
*/
qsizetype
IndexRangeCollection::leftMostIndexRangeStart() const
{
  qDebug() << "IndexRangeCollection:" << this->indicesAsText();

  qsizetype left_most_value = std::numeric_limits<qsizetype>::max();

  foreach(const IndexRange *item, m_ranges)
    {
      if(item->m_start < left_most_value)
        left_most_value = item->m_start;
    }

  return left_most_value;
}

/*!
\brief Returns a container with the indices of the IndexRange instances that
have the smallest start value.

Searches all the IndexRange instances in this IndexRangeCollection instance that
share the same IndexRange::start value that is actually the smallest such start
value in the container. Each found IndexRange instance's index in this
IndexRangeCollection instance is added to the container.

\sa indicesOfRightMostIndexRanges(), isLeftMostIndexRange(),
rightMostIndexRangeStop()
*/
QList<qsizetype>
IndexRangeCollection::indicesOfLeftMostIndexRanges() const
{
  QList<qsizetype> indices;

  if(!size())
    return indices;

  qsizetype left_most_value = leftMostIndexRangeStart();

  // At this point we know what's the leftmost index. We can use that
  // index to search for all the items that are also leftmost.

  for(qsizetype iter = 0; iter < m_ranges.size(); ++iter)
    {
      if(m_ranges.at(iter)->m_start == left_most_value)
        indices.push_back(iter);
    }

  return indices;
}

/*!
\brief Returns true if \a index_range is the left-most IndexRange
instance in this IndexRangeCollection instance, false otherwise.
*/
bool
IndexRangeCollection::isLeftMostIndexRange(const IndexRange &index_range) const
{
  // Is index_range the leftmost sequence range of *this
  // IndexRangeCollection instance ?

  qsizetype value = index_range.m_start;

  foreach(const IndexRange *item, m_ranges)
    {
      if(item->m_start < value)
        return false;
    }

  return true;
}

/*!
\brief Return the right most stop ordinate throughout all the IndexRanges
instances.
 */
qsizetype
IndexRangeCollection::rightMostIndexRangeStop() const
{
  qDebug() << "IndexRangeCollection:" << this->indicesAsText();

  qsizetype rightMostValue = std::numeric_limits<qsizetype>::min();

  foreach(const IndexRange *item, m_ranges)
    {
      if(item->m_stop > rightMostValue)
        rightMostValue = item->m_stop;
    }

  return rightMostValue;
}

/*!
\brief Returns a container with the indices of the IndexRange instances that
have the greater stop value.

Searches all the IndexRange instances in this IndexRangeCollection instance that
share the same IndexRange::stop value that is actually the greatest such stopt
value in the container. Each found IndexRange instance's index in this
IndexRangeCollection instance is added to the container.
\sa indicesOfLeftMostIndexRanges(), isLeftMostIndexRange(),
isRightMostIndexRange()
*/
QList<qsizetype>
IndexRangeCollection::indicesOfRightMostIndexRanges() const
{
  QList<qsizetype> indices;

  if(!size())
    return indices;

  qsizetype right_most_value = rightMostIndexRangeStop();

  // At this point we now what's the rightmost index. We can use
  // that index to search for all the items that are also
  // rightmost.

  for(qsizetype iter = 0; iter < m_ranges.size(); ++iter)
    {
      if(m_ranges.at(iter)->m_stop == right_most_value)
        indices.push_back(iter);
    }

  return indices;
}

/*!
\brief Returns true if \a index_range is the right-most IndexRange
instance in this IndexRangeCollection instance, false otherwise.
*/
bool
IndexRangeCollection::isRightMostIndexRange(const IndexRange &index_range) const
{
  // Is index_range the rightmost sequence range of *this
  // IndexRangeCollection instance ?

  qsizetype value = index_range.m_stop;

  foreach(const IndexRange *item, m_ranges)
    {
      if(item->m_stop > value)
        return false;
    }

  return true;
}

/*!
\brief Returns an \l IndexRange containing the leftmost start index and the
rightmost stop index,  effectively providing the most inclusive index range.
*/
IndexRange *
IndexRangeCollection::mostInclusiveLeftRightIndexRange() const
{
  return new IndexRange(leftMostIndexRangeStart(),
                        rightMostIndexRangeStop(),
                        const_cast<IndexRangeCollection *>(this));
}

/*!
\brief Returns true if \a index is found to be between the start and stop
indices of at least one IndexRange instance in this IndexRangeCollection
instance, false otherwise.

If \a globally is set to true, then returns true if \a index is contained
in the interval [smallest - greatest] indices throughout all the IndexRange
instances.

\sa leftMostIndexRangeStart(), rightMostIndexRangeStop()
*/
bool
IndexRangeCollection::encompassIndex(qsizetype index, bool globally) const
{
  if(globally)
    return (index >= leftMostIndexRangeStart() &&
            index <= rightMostIndexRangeStop());

  foreach(const IndexRange *item, m_ranges)
    {
      if(item->m_start <= index && item->m_stop >= index)
        return true;
    }

  return false;
}

/*!
\brief Returns true if at least two IndexRange instances overlap.

Two IndexRange instances overlap if the second's start value is less than
the first's stop value and greater than the first's start value:

|-----------------|
         |=================|

         or

                    |-----------------|
         |=================|

\sa encompassIndex()
*/
bool
IndexRangeCollection::overlap() const
{
  // Return true if there are overlapping regions in this
  // IndexRangeCollection instance.

  if(size() < 2)
    return false;

  foreach(const IndexRange *first_item, m_ranges)
    {
      qsizetype first_1  = first_item->m_start;
      qsizetype second_1 = first_item->m_stop;

      foreach(const IndexRange *second_item, m_ranges)
        {
          if(second_item == first_item)
            continue;

          qsizetype first_2 = second_item->m_start;

          if(first_2 <= second_1 && first_2 >= first_1)
            return true;
        }
    }

  return false;
}

/*!
\brief Returns a string documenting the IndexRange instances in this
IndexRanges instance.

Each IndexRange instance is described like the following, with the values
being the start and stop index values in the IndexRange:

\code
[156-350]
\endcode

\sa positionsAsText()
*/
QString
IndexRangeCollection::indicesAsText() const
{
  QString text;

  foreach(const IndexRange *item, m_ranges)
    {
      text += QString("[%1-%2]").arg(item->m_start).arg(item->m_stop);
    }

  return text;
}

/*!
\brief Returns a string documenting the IndexRangeCollection in this
IndexRanges.

Each IndexRange instance is described like the following with the values
being the start and stop position values in the IndexRange (start + 1,  stop
+1):

\code
[157-351]
\endcode

\note The values reported are not \e indices, but \e positions.

\sa indicesAsText()
*/
QString
IndexRangeCollection::positionsAsText() const
{
  QString text;

  foreach(const IndexRange *item, m_ranges)
    {
      text += QString("[%1-%2]").arg(item->m_start + 1).arg(item->m_stop + 1);
    }

  return text;
}

/*!
\brief Returns the size of the container of IndexRange instances.
*/
qsizetype
IndexRangeCollection::size() const
{
  return m_ranges.size();
}

/*!
\brief Clears all the members of this IndexRangeCollection instance.
*/
void
IndexRangeCollection::clear()
{
  m_comment = "";
  m_ranges.clear();
}

void
IndexRangeCollection::registerJsConstructor(QJSEngine *engine)

{
  if(!engine)
    {
      qDebug() << "Cannot register IndexRangeCollection class: engine is null";
      return;
    }

  // Register the meta object as a constructor

  QJSValue jsMetaObject =
    engine->newQMetaObject(&IndexRangeCollection::staticMetaObject);
  engine->globalObject().setProperty("IndexRangeCollection", jsMetaObject);
}


} // namespace libXpertMassCore
} // namespace MsXpS
