/* BEGIN software license
 *
 * msXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright(C) 2009,...,2019 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
 */


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


/////////////////////// Qt includes
#include <QDebug>


/////////////////////// pappsomspp includes


/////////////////////// Local includes
#include "ProcessingFlow.hpp"
#include "ProcessingStep.hpp"


namespace MsXpS
{
namespace minexpert
{


ProcessingFlow::ProcessingFlow()
{
}


ProcessingFlow::ProcessingFlow(MsRunDataSetCstSPtr ms_run_data_set_csp)
  : mcsp_msRunDataSet(ms_run_data_set_csp)
{
}

ProcessingFlow::ProcessingFlow(const ProcessingFlow &other)
  : std::vector<MsXpS::minexpert::ProcessingStepCstSPtr>(),
    mcsp_msRunDataSet(other.mcsp_msRunDataSet),
    m_defaultMzIntegrationParams(other.m_defaultMzIntegrationParams),
    m_defaultMsFragmentationSpec(other.m_defaultMsFragmentationSpec)
{
  assign(other.begin(), other.end());
  m_filterMap = other.m_filterMap;
}


ProcessingFlow::~ProcessingFlow()
{
  // Now clear the vector.
  clear();
}


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

  mcsp_msRunDataSet            = other.mcsp_msRunDataSet;
  m_defaultMzIntegrationParams = other.m_defaultMzIntegrationParams;
  m_defaultMsFragmentationSpec = other.m_defaultMsFragmentationSpec;

  assign(other.begin(), other.end());

  m_filterMap = other.m_filterMap;

  return *this;
}


void
ProcessingFlow::setMsRunDataSetCstSPtr(MsRunDataSetCstSPtr ms_run_data_set_csp)
{
  mcsp_msRunDataSet = ms_run_data_set_csp;
}


MsRunDataSetCstSPtr
ProcessingFlow::getMsRunDataSetCstSPtr() const
{
  return mcsp_msRunDataSet;
}


void
ProcessingFlow::setDefaultMzIntegrationParams(
  const pappso::MzIntegrationParams &mz_integration_params)
{
  m_defaultMzIntegrationParams = mz_integration_params;
}


const pappso::MzIntegrationParams &
ProcessingFlow::getDefaultMzIntegrationParams() const
{
  return m_defaultMzIntegrationParams;
}


void
ProcessingFlow::setDefaultMsFragmentationSpec(
  const MsFragmentationSpec &ms_fragmentation_spec)
{
  // qDebug();

  m_defaultMsFragmentationSpec = ms_fragmentation_spec;
}


const MsFragmentationSpec &
ProcessingFlow::getDefaultMsFragmentationSpec() const
{
  return m_defaultMsFragmentationSpec;
}


std::vector<ProcessingStepCstSPtr>
ProcessingFlow::steps()
{
  std::vector<ProcessingStepCstSPtr> steps;

  steps.assign(begin(), end());

  return steps;
}


const std::map<QString, pappso::FilterNameInterfaceCstSPtr> &
ProcessingFlow::getFilterMap() const
{
  return m_filterMap;
}


pappso::FilterNameInterfaceCstSPtr
ProcessingFlow::filter(const QString &filter_name) const
{
  // qDebug() << "In ProcessingFlow:" << this;
  // qDebug() << "Searching for filter:" << filter_name;

  std::map<QString, pappso::FilterNameInterfaceCstSPtr>::const_iterator
    found_iterator = m_filterMap.find(filter_name);

  if(found_iterator == m_filterMap.end())
    return nullptr;

  return found_iterator->second;
}


void
ProcessingFlow::addFilter(pappso::FilterNameInterfaceCstSPtr filter_csp)
{
  QString filter_name = filter_csp->name();

  using Pair     = std::pair<QString, pappso::FilterNameInterfaceCstSPtr>;
  using Map      = std::map<QString, pappso::FilterNameInterfaceCstSPtr>;
  using Iterator = Map::iterator;

  std::pair<Iterator, bool> res =
    m_filterMap.insert(Pair(filter_name, filter_csp));

  if(!res.second)
    {
      // Insertion failed because one other same filter by that name was already
      // in the map. Set the new value.

      res.first->second = filter_csp;
    }
  // else: the insert operation was performed fine.
}


std::size_t
ProcessingFlow::removeFilter(const QString &filter_name)
{
  return m_filterMap.erase(filter_name);
}


bool
ProcessingFlow::innermostRange(
  ProcessingType src_processing_type_mask,
  std::pair<double, double> &out_range,
  std::vector<ProcessingStepCstSPtr> *out_processing_steps_p,
  bool store_only_2d_steps,
  bool store_only_rhomboid) const
{
  // qDebug() << "Searching for innermost range for source ProcessingType mask:"
  //          << src_processing_type_mask.toBriefDesc() << "in a flow of" <<
  //          size()
  //          << "steps";

  // We are asked to get the innermost range for all steps having their
  // processing type matching the processing_type_mask.

  // processing_type_mask specifies a source processing type, that might be
  // someghing like "ANY_RT" or "ANY_DT" or "ANY_MZ".

  // We iterate in the steps of this processing flow and check that their source
  // type matches processing_type_mask. If so, we look at the axis data kind and
  // check for the innermost range.

  ProcessingType src_processing_type("NOT_SET");
  ProcessingType x_src_processing_type("NOT_SET");
  ProcessingType y_src_processing_type("NOT_SET");

  double inner_range_start = std::numeric_limits<double>::quiet_NaN();
  double inner_range_end   = std::numeric_limits<double>::quiet_NaN();

  bool is_first_iterated_item = true;

  ProcessingStepCstSPtr innermost_step_csp = nullptr;

  // A ProcessingFlow is a vector of ProcessingStepCstSPtr.

  for(auto &step_csp : *this)
    {
      // qDebug() << "Iterating in step:" << step_csp->toString();

      double range_start;
      double range_end;

      ProcessingType iter_x_processing_type = step_csp->m_xSrcType;
      ProcessingType iter_y_processing_type = step_csp->m_ySrcType;

      // qDebug() << "Iterating in ProcessingStep of x and y src types:"
      //<< iter_x_processing_type.toBriefDesc() << "and"
      //<< iter_y_processing_type.toBriefDesc();

      // Check the processing type. We may get here any kind of mono- or
      // bi-dimensional processing type, like MZ_TO_ANY or DT_RT_TO_ANY.

      if(!iter_x_processing_type.bitMatches(src_processing_type_mask) &&
         !iter_y_processing_type.bitMatches(src_processing_type_mask))
        {
          // qDebug() << "The iterated step has none of its source types
          // matching " "the searched type. Skipping it.";

          continue;
        }

      // Is the currently iterated step mono- or two-dimensional ?
      if(step_csp->msp_integrationScope->is1D())
        {
          // This is the simplest situation, there is only one dimension, the x
          // dimension, that is of interest.

          // qDebug() << "That step is 1D";

          step_csp->msp_integrationScope->range(
            pappso::Axis::x, range_start, range_end);

          if(is_first_iterated_item)
            {
              inner_range_start = range_start;
              inner_range_end   = range_end;

              // qDebug() << "First iterated step, with inner range:"
              //<< inner_range_start << "-" << inner_range_end;

              if(!store_only_2d_steps)
                {
                  // qDebug() << "Step is 1D, setting step as innermost step as"
                  //             "requested.";
                  innermost_step_csp = step_csp;
                }

              is_first_iterated_item = false;
            }
          else
            {
              // qDebug() << "Not first iterated step.";

              // This is not the first step we iterate into,
              // so there are already range values, that we
              // need to compare with those of this last step so as
              // to update the innermost range values if necessary.

              // This variable will report if the currently iterated-into step
              // has range values that make them innermost with respect to
              // the previous got ones.
              bool was_at_least_one_range_value_inner = false;

              // If the new x-axis value is greater than the previous one,
              // then that means that we are reducing the range at the left
              // side, so this new range start value has to become the new
              // innermost range start value.

              if(range_start > inner_range_start)
                {
                  inner_range_start = range_start;

                  was_at_least_one_range_value_inner = true;

                  // qDebug() << "New inner range start:" << inner_range_start;
                }

              // If the new x-axis value is smaller that the previous one,
              // then that means that we are reducing the range at the right
              // side, so this new range end value has to become the new
              // innmermost range end value.
              if(range_end < inner_range_end)
                {
                  inner_range_end = range_end;

                  was_at_least_one_range_value_inner = true;
                  // qDebug() << "New inner range end:" << inner_range_end;
                }

              if(was_at_least_one_range_value_inner && !store_only_2d_steps)
                {
                  // qDebug() << "Even if 1D, setting step as innermost step as
                  // " "requested.";
                  innermost_step_csp = step_csp;
                }
            }

          continue;
        }
      else
        {
          // qDebug() << "That step is 2D, either Rect or Rhomb";

          // At this point, we know the selection polygon is either Rectangle or
          // Rhomboid, which means we have to check what actual source types the
          // iterated step has because we need to know what is the type of the
          // different selection polygon axes. Indeed, we will compare only
          // range values from the iterated step for the right either horizontal
          // (X) or vertical (Y) dimension to the ones already encountered in
          // this iteration over all the steps of the processing flow.

          if(step_csp->m_xSrcType.bitMatches(src_processing_type_mask))
            {
              // qDebug() << "Step's m_xSrcType.bitMatches"
              //          << src_processing_type_mask.toBriefDesc();

              // The x axis is the axis we need to check the innermost range of.
              // Get the range values.

              //*step_csp->m_selectionPolygon.rangeX(range_start, range_end);
              step_csp->msp_integrationScope->range(
                pappso::Axis::x, range_start, range_end);

              // qDebug() << "Gotten range values:" << range_start << "-"
              //          << range_end;
            }
          else if(step_csp->m_ySrcType.bitMatches(src_processing_type_mask))
            {
              // qDebug() << "Step's m_ySrcType.bitMatches"
              //          << src_processing_type_mask.toBriefDesc();

              // The y axis is the axis we need to check the innermost range of.
              // Get the range values.

              //**step_csp->m_selectionPolygon.rangeY(range_start, range_end);
              step_csp->msp_integrationScope->range(
                pappso::Axis::y, range_start, range_end);

              // qDebug() << "Gotten range values:" << range_start << "-"
              //          << range_end;
            }
          else
            qFatal("Programming error.");

          // And now use the gotten range values of the currently iterated step
          // to establish if that dimension is inner or not.

          if(is_first_iterated_item)
            {
              // Special situation where we are just setting values because
              // this is the first step that we iterate into and thus
              // we have no other values to perform comparisons with, whe
              // just seed the system.
              inner_range_start = range_start;
              inner_range_end   = range_end;

              // qDebug() << "First iterated step, with inner range:"
              //<< inner_range_start << "-" << inner_range_end;

              // qDebug() << "Setting step as innermost step.";
              innermost_step_csp = step_csp;

              is_first_iterated_item = false;
            }
          else
            {
              // qDebug() << "Not first iterated step.";

              // This is not the first step we iterate into,
              // so there are already range values, that we
              // need to compare with those of this last step so as
              // to update the innermost range values if necessary.

              // This variable will report if the currently iterated-into step
              // has range values that make them innermost with respect to
              // the previous got ones.
              bool was_at_least_one_range_value_inner = false;

              if(range_start > inner_range_start)
                {
                  inner_range_start = range_start;

                  was_at_least_one_range_value_inner = true;

                  // qDebug() << "New inner range start:" << inner_range_start;
                }

              if(range_end < inner_range_end)
                {
                  inner_range_end = range_end;

                  was_at_least_one_range_value_inner = true;

                  // qDebug() << "New inner range end:" << inner_range_end;
                }

              if(was_at_least_one_range_value_inner)
                {
                  // qDebug() << "Setting step as innermost step.";
                  // innermost_step_csp = step_csp;
                }
            }

          // Done, go to next step.
          continue;
        }
    }

  // qDebug() << "Finished iterating in the vector of steps.";

  // At this point we have iterated in all the steps. To know if at least one
  // step was found matching the src_processing_type_mask, we can test the
  // inner_range_(start|end) values against NaN.

  // We can't just check innermost_step_csp because the params might
  // tell not to save the step.

  if(std::isnan(inner_range_start) || std::isnan(inner_range_end))
    return false;

  // We know we can update the values in the variables passed as out params.
  out_range.first  = inner_range_start;
  out_range.second = inner_range_end;

  // If the callers provides a vector of steps, then document the step.
  // Check for options, nonetheless.

  bool should_store_the_step_in_the_end = false;

  if(out_processing_steps_p != nullptr)
    {
      // qDebug() << "The caller wants the innermost step to be stored.";

      if(innermost_step_csp != nullptr)
        {
          // qDebug() << "One innermost step was indeed found.";

          if(!store_only_rhomboid)
            {
              // qDebug()
              //<< "The caller wants *all* 2D steps, not only the skewed ones.";
              should_store_the_step_in_the_end = true;
            }
          else
            {
              if(innermost_step_csp->msp_integrationScope->isRhomboid())
                {
                  should_store_the_step_in_the_end = true;
                  // qDebug() << "The caller wants *only* the 2D skewed steps
                  // and "
                  //             "the step *is* rhomboid";
                }
              // else
              //   {
              //     qDebug() << "The caller wants *only* the 2D rhomboid steps
              //     "
              //                 "and sthe step is *not* rhomboid";
              //   }
            }
        }
      // else
      // qDebug() << "Not a single step was found.";
    }
  // else
  // qDebug() << "The caller does *not* want the innermost step to be stored.";

  // At this point, if we have to store the step, check that another
  // call by another processing type mask has not set it already
  // (typically ANY_RT and ANY_DT with steps DT_RT).

  if(should_store_the_step_in_the_end)
    {
      // qDebug() << "Really, the step should be stored.";

      // We only push back the range if it has not been found
      // already. Note that this function might be called multiple
      // times with different src_processing_type_mask values.

      bool was_step_found = false;

      for(auto &iter_step_csp : *out_processing_steps_p)
        {
          if(iter_step_csp->m_dateAndTime == innermost_step_csp->m_dateAndTime)
            {
              was_step_found = true;

              // qDebug() << "Did not store the step because it had been "
              //"stored already.";

              break;
            }
        }

      if(!was_step_found)
        {
          // qDebug() << "The step is actually skewed, so storing it:"
          //<< innermost_step_p->toString();

          out_processing_steps_p->push_back(innermost_step_csp);
        }
    }

  // Always return true because we had returned false already when we
  // discovered that the processing types did not match.

  return true;
}


bool
ProcessingFlow::innermostRange(
  const QString src_processing_type_mask,
  std::pair<double, double> &out_range,
  std::vector<ProcessingStepCstSPtr> *out_processing_steps_p,
  bool store_only_2d_steps,
  bool store_only_skewed) const
{
  return innermostRange(ProcessingType(src_processing_type_mask),
                        out_range,
                        out_processing_steps_p,
                        store_only_2d_steps,
                        store_only_skewed);
}


#if 0
    bool
      ProcessingFlow::innermostRtRange(
          std::pair<ProcessingType, ProcessingStep *> &pair) const
      {
        return innermostRange(ProcessingType("RT_TO_ANY"), pair);
      }


    bool
      ProcessingFlow::innermostMzRange(double &range_start, double &range_end) const
      {
        return innermostRange(ProcessingType("MZ_TO_ANY"), range_start, range_end);
      }


    bool
      ProcessingFlow::innermostMzRange(
          std::pair<ProcessingType, ProcessingStep *> &pair) const
      {
        return innermostRange(ProcessingType("MZ_TO_ANY"), pair);
      }

#endif

size_t
ProcessingFlow::greatestMsLevel() const
{
  // We want to know what is the greatest MS level in the default ms
  // frag spec and in all the ms frag specs of the various processing
  // specs.

  size_t greatest_ms_level = m_defaultMsFragmentationSpec.getMsLevel();

  for(auto &&step : *this)
    {
      if(step->getMsLevel() > greatest_ms_level)
        {
          greatest_ms_level = step->getMsLevel();
        }
    }

  // qDebug() << "The greatest MS level of the whole processing flow:"
  //<< greatest_ms_level;

  return greatest_ms_level;
}


ProcessingStepCstSPtr
ProcessingFlow::oldestStep() const
{
  // We rely on the QDateTime instance in the steps to check which of
  // the step was created last.

  if(!size())
    return nullptr;

  if(size() == 1)
    return front();

  ProcessingStepCstSPtr oldest_step_csp = front();

  // Seed the temporary date and time with the first step in the
  // vector.
  QDateTime temp_date_time = front()->m_dateAndTime;

  for(std::vector<ProcessingStepCstSPtr>::const_iterator iterator = begin() + 1;
      iterator != end();
      ++iterator)
    {
      // Is the iterated step older than temp_date_time?
      if((*iterator)->m_dateAndTime < temp_date_time)
        {
          oldest_step_csp = *iterator;
          temp_date_time  = (*iterator)->m_dateAndTime;
        }
    }

  return oldest_step_csp;
}


ProcessingStepCstSPtr
ProcessingFlow::mostRecentStep() const
{
  // We rely on the QDateTime instance in the steps to check which of
  // the step was created last.

  // Calling from on an empty container causes undefined behaviour.
  if(!size())
    return nullptr;

  if(size() == 1)
    return front();

  ProcessingStepCstSPtr most_recent_step_csp = nullptr;

  QDateTime date_time;
  date_time.setDate(QDate(1900, 1, 1));

  for(auto &&step_p : *this)
    {
      // qDebug();

      if(step_p->m_dateAndTime > date_time)
        most_recent_step_csp = step_p;
    }

  return most_recent_step_csp;
}


std::vector<ProcessingStepCstSPtr>
ProcessingFlow::stepsMatchingDestType(const ProcessingType &processing_type,
                                      bool with_valid_fragmentation_spec) const
{
  std::vector<ProcessingStepCstSPtr> processing_steps;

  for(auto &&step_csp : *this)
    {

      if(step_csp->m_destType.bitMatches(processing_type))
        {
          if(with_valid_fragmentation_spec)
            {
              if(step_csp->getMsFragmentationSpec().isValid())
                processing_steps.push_back(step_csp);
            }
          else
            processing_steps.push_back(step_csp);
        }
    }

  return processing_steps;
}


std::vector<ProcessingStepCstSPtr>
ProcessingFlow::stepsMatchingDestType(const QString &processing_type,
                                      bool with_valid_fragmentation_spec) const
{
  return stepsMatchingDestType(ProcessingType(processing_type),
                               with_valid_fragmentation_spec);
}


ProcessingStepCstSPtr
ProcessingFlow::closestOlderStep(const QDateTime &date_time) const
{
  // qDebug() << "comparison date time:" << date_time
  //<< "number of steps:" << size();

  // We want a step that is older than date_time. But we also want
  // that the step be the most recent older step, that is, the closest
  // older step.
  if(!size())
    return nullptr;

  if(size() == 1)
    {
      if(front()->m_dateAndTime < date_time)
        {
          // qDebug() << "Returning unique older:" <<
          // front()->m_dateAndTime;
          return front();
        }
    }
  else
    return nullptr;


  // Seed the temporary date time with the oldest step's date and
  // time.

  ProcessingStepCstSPtr older_step_csp = nullptr;

  for(std::vector<ProcessingStepCstSPtr>::const_iterator iterator = begin();
      iterator != end();
      ++iterator)
    {
      // qDebug() << "Iterating in time:" <<
      // (*iterator)->m_dateAndTime;

      if((*iterator)->m_dateAndTime < date_time)
        {
          // qDebug() << "iter time is less than comparison date
          // time.";

          if(older_step_csp == nullptr)
            {
              // First iteration for which the date is less than
              // comparison time.
              older_step_csp = *iterator;

              continue;
            }

          // The currently iterated step is older than date_time,
          // fine, but is it more recent that the temporaray time ? If
          // so set it as the last older step.

          if((*iterator)->m_dateAndTime > older_step_csp->m_dateAndTime)
            {
              // qDebug() << "more than that: iter time is also more
              // recent than " "last itered time.";

              older_step_csp = *iterator;
            }
        }
    }

  return older_step_csp;
}


const pappso::MzIntegrationParams *
ProcessingFlow::mostRecentMzIntegrationParams() const
{

  // qDebug() << "*this processing flow:" << this->toString();

  ProcessingStepCstSPtr most_recent_step_csp = mostRecentStep();

  if(most_recent_step_csp != nullptr)
    {
      if(most_recent_step_csp->mpa_mzIntegrationParams != nullptr)
        {
          // qDebug()
          //<< "Returning immediately the most recent step's mz integ.
          // params.";

          return most_recent_step_csp->mpa_mzIntegrationParams;
        }
    }
  else
    {
      // qDebug() << "Not a single most recent step there. The flow
      // seems empty."; Apparently there is not a single step in the
      // processing flow.
      return nullptr;
    }

  // If the most recent step had no mz integration params set, we need
  // to iterate back in time.
  while(1)
    {
      // Get a step older than the most recent step.

      const ProcessingStepCstSPtr closest_older_step_csp =
        closestOlderStep(most_recent_step_csp->m_dateAndTime);

      if(closest_older_step_csp != nullptr)
        {
          // qDebug()
          //<< "iterating in infinite while loop with closest older
          // step
          // time:"
          //<< closest_older_step_p->m_dateAndTime;

          // Found an older step, but check if it has mz integration
          // params set to it.
          if(closest_older_step_csp->mpa_mzIntegrationParams != nullptr)
            {
              // qDebug() << "Found a step that is closest and that
              // has mz "
              //"integration params.";

              return closest_older_step_csp->mpa_mzIntegrationParams;
            }
          else
            {
              // qDebug() << "Continuing in the infinite while loop.";
              continue;
            }
        }
      else
        {
          // There are no more older steps.
          // qDebug()
          //<< "There are no more closest older steps. Returning
          // nullptr";

          return nullptr;
        }
    }

  return nullptr;
}


QString
ProcessingFlow::toString(int offset, const QString &spacer) const
{
  QString lead;

  for(int iter = 0; iter < offset; ++iter)
    lead += spacer;

  QString text = lead;
  // text += "Processing flow:\n";

  // text += lead;
  // text += spacer;
  text += "Sample name:\n";
  if(mcsp_msRunDataSet == nullptr)
    text += "nullptr";
  else
    text += mcsp_msRunDataSet->getMsRunId()->getSampleName();
  text += "\n\n";

  // text += lead;
  // text += spacer;
  // text += "Default m/z integration params:\n";
  text += m_defaultMzIntegrationParams.toString(offset, spacer);
  text += "\n";

  // text += lead;
  // text += spacer;
  // text += "Default MS fragmentation spec:\n";
  text += m_defaultMsFragmentationSpec.toString(offset, spacer);
  text += "\n";

  // Only show the steps if there are any !
  if(size())
    {
      // text += lead;
      // text += spacer;
      // text += "The steps:\n";

      for(auto &&step : *this)
        {
          text += step->toString(offset, spacer);
          // text += "\n";
        }
    }

  // Now handle the filter map. We'll only report the names of filters.

  if(m_filterMap.size())
    {
      text += "\nFilters:\n";

      for(std::map<QString, pappso::FilterNameInterfaceCstSPtr>::const_iterator
            iterator = m_filterMap.begin();
          iterator != m_filterMap.end();
          ++iterator)
        {
          text += iterator->first;
          text += "\n";
        }
    }

  return text;
}


} // namespace minexpert


} // namespace MsXpS
