/**
 * \file pappsomspp/msrun/private/timsmsrunreader.h
 * \date 05/09/2019
 * \author Olivier Langella
 * \brief MSrun file reader for native Bruker TimsTOF raw data
 */

/*******************************************************************************
 * Copyright (c) 2019 Olivier Langella <Olivier.Langella@u-psud.fr>.
 *
 * This file is part of the PAPPSOms++ library.
 *
 *     PAPPSOms++ 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.
 *
 *     PAPPSOms++ 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 PAPPSOms++.  If not, see <http://www.gnu.org/licenses/>.
 *
 ******************************************************************************/

#include "timsmsrunreader.h"
#include "../../vendors/tims/timsddaprecursors.h"
#include "../../vendors/tims/timsdiaslices.h"
#include "../../exception/exceptionnotimplemented.h"
#include "../../exception/exceptioninterrupted.h"
#include <QDebug>

using namespace pappso;

TimsMsRunReader::TimsMsRunReader(MsRunIdCstSPtr &msrun_id_csp)
  : TimsMsRunReaderBase(msrun_id_csp)
{
  initialize();
}
TimsMsRunReader::TimsMsRunReader(const TimsMsRunReaderBase &msrun_reader_base)
  : TimsMsRunReaderBase(msrun_reader_base)
{
  initialize();
}


TimsMsRunReader::~TimsMsRunReader()
{
}


pappso::MassSpectrumSPtr
TimsMsRunReader::massSpectrumSPtr([[maybe_unused]] std::size_t spectrum_index)
{
  throw ExceptionNotImplemented(
    QObject::tr("Not yet implemented in TimsMsRunReader %1.\n").arg(__LINE__));
  return pappso::MassSpectrumSPtr();
}


pappso::MassSpectrumCstSPtr
TimsMsRunReader::massSpectrumCstSPtr(std::size_t spectrum_index)
{
  return msp_timsData->getMassSpectrumCstSPtrByGlobalScanIndex(spectrum_index);
}


QualifiedMassSpectrum
TimsMsRunReader::qualifiedMassSpectrum(std::size_t spectrum_index,
                                       bool want_binary_data) const
{

  QualifiedMassSpectrum mass_spectrum;

  msp_timsData->getQualifiedMassSpectrumByGlobalScanIndex(
    getMsRunId(), mass_spectrum, spectrum_index, want_binary_data);
  return mass_spectrum;
}


void
TimsMsRunReader::readSpectrumCollection(
  SpectrumCollectionHandlerInterface &handler)
{
  readSpectrumCollectionByMsLevel(handler, 0);
}

void
TimsMsRunReader::readSpectrumCollection2(
  const MsRunReadConfig &config, SpectrumCollectionHandlerInterface &handler)
{

  qDebug().noquote() << "Reading the spectrum collection with this "
                        "specific configuration:"
                     << config.toString();

  std::vector<std::size_t> subset_of_tims_frame_ids;

  bool asked_ion_mobility_scan_num_range = false;

  quint32 mobility_scan_num_range_begin =
    std::numeric_limits<quint32>::quiet_NaN();
  quint32 mobility_scan_num_range_end =
    std::numeric_limits<quint32>::quiet_NaN();
  quint32 mobility_scan_num_range_width =
    std::numeric_limits<quint32>::quiet_NaN();

  double mobility_one_over_k0_range_begin =
    std::numeric_limits<double>::quiet_NaN();
  double mobility_one_over_k0_range_end =
    std::numeric_limits<double>::quiet_NaN();

  if(!config
        .getParameterValue(
          MsRunReadConfigParameter::TimsFrameIonMobScanIndexBegin)
        .isNull() &&
     !config
        .getParameterValue(
          MsRunReadConfigParameter::TimsFrameIonMobScanIndexEnd)
        .isNull())
    {
      mobility_scan_num_range_begin =
        config
          .getParameterValue(
            MsRunReadConfigParameter::TimsFrameIonMobScanIndexBegin)
          .toUInt();
      mobility_scan_num_range_end =
        config
          .getParameterValue(
            MsRunReadConfigParameter::TimsFrameIonMobScanIndexEnd)
          .toUInt();

      // We need the range width below.
      mobility_scan_num_range_width =
        mobility_scan_num_range_end + 1 - mobility_scan_num_range_begin;

      asked_ion_mobility_scan_num_range = true;

      // Be sure to check in the frames loop below that the user might
      // have asked for an ion mobility range but on the basis of the 1/K0 unit.
    }

  const std::vector<FrameIdDescr> &frame_id_descr_list =
    msp_timsData->getFrameIdDescrList();

  // Just for the feedback to the user.
  std::size_t scan_count = 0;

  for(auto const &frame_record : msp_timsData->getTimsFrameRecordList())
    {
      if(handler.shouldStop())
        {
          // qDebug() << "The operation was cancelled. Breaking the loop.";
          throw ExceptionInterrupted(
            QObject::tr("Reading timsTOF data cancelled by the user."));
        }

      if(frame_record.frame_id == 0)
        continue;

      if(!config.acceptRetentionTimeInSeconds(frame_record.frame_time))
        continue;

      std::size_t ms_level = 2;
      if(frame_record.msms_type == 0)
        ms_level = 1;

      if(!config.acceptMsLevel(ms_level))
        continue;

      subset_of_tims_frame_ids.push_back(frame_record.frame_id);

      if(mobility_scan_num_range_width)
        {
          scan_count += mobility_scan_num_range_width;
        }
      else
        {
          scan_count += frame_id_descr_list[frame_record.frame_id].m_scanCount;
        }
    }

  // At this point, we have a subset of frame records.
  std::size_t frame_count = subset_of_tims_frame_ids.size();
  qDebug() << "The number of retained RT range- and MS level-matching frames : "
           << frame_count;
  // Inform the handler of the spectrum list so that it can handle feedback to
  // the user.
  handler.spectrumListHasSize(scan_count);

  // Check for m/z range selection
  double mz_range_begin = -1;
  double mz_range_end   = -1;

  if(!config.getParameterValue(MsRunReadConfigParameter::MzRangeBegin)
        .isNull() &&
     !config.getParameterValue(MsRunReadConfigParameter::MzRangeEnd).isNull())
    {
      mz_range_begin =
        config.getParameterValue(MsRunReadConfigParameter::MzRangeBegin)
          .toDouble();

      mz_range_end =
        config.getParameterValue(MsRunReadConfigParameter::MzRangeEnd)
          .toDouble();

      // qDebug() << "The m/z range asked is: " << mz_range_begin
      //          << "--" << mz_range_end;
    }

  // Check for m/z resolution downgrading (mz bins merge)
  // The idea is that we merge a number of mz indices into a single index,
  // which is essentially an increase of the m/z bin size, and therefore
  // a reduction of the resolution/definition of the mass spectrum.
  std::size_t mz_index_merge_window = 0;
  if(!config
        .getParameterValue(
          MsRunReadConfigParameter::TimsFrameMzIndexMergeWindow)
        .isNull())
    {
      mz_index_merge_window =
        config
          .getParameterValue(
            MsRunReadConfigParameter::TimsFrameMzIndexMergeWindow)
          .toUInt();

      // qDebug() << "mz_index_merge_window=" << mz_index_merge_window;
    }

  std::size_t number_of_mobility_scans_set_as_qualified_mass_spectra = 0;

  for(std::size_t tims_frame_id : subset_of_tims_frame_ids)
    {
      if(handler.shouldStop())
        {
          // qDebug() << "The operation was cancelled. Breaking the loop.";
          throw ExceptionInterrupted(
            QObject::tr("Reading timsTOF data cancelled by the user."));
        }

      qDebug() << "tims_frame_id=" << tims_frame_id;

      const FrameIdDescr &current_frame_record =
        frame_id_descr_list[tims_frame_id];

      TimsFrameCstSPtr tims_frame_csp =
        msp_timsData->getTimsFrameCstSPtrCached(tims_frame_id);

      qDebug() << "tims_frame_id=" << tims_frame_id;

      // If the user wants to select 1/Ko values in a given range, we need to
      // compute the ion mobility scan value starting from that 1/Ko value in
      // *each* frame. Note that the computed mobility_scan_num_begin and
      // mobility_scan_num_end would override thoses possibly set with
      // TimsFramesMsRunReader_mobility_index_begin/end above.

      if(!config
            .getParameterValue(
              MsRunReadConfigParameter::TimsFrameIonMobOneOverK0Begin)
            .isNull() &&
         !config
            .getParameterValue(
              MsRunReadConfigParameter::TimsFrameIonMobOneOverK0End)
            .isNull())
        {
          mobility_one_over_k0_range_begin =
            config
              .getParameterValue(
                MsRunReadConfigParameter::TimsFrameIonMobOneOverK0Begin)
              .toDouble();

          mobility_one_over_k0_range_end =
            config
              .getParameterValue(
                MsRunReadConfigParameter::TimsFrameIonMobOneOverK0End)
              .toDouble();

          mobility_scan_num_range_begin =
            tims_frame_csp.get()->getScanIndexFromOneOverK0(
              mobility_one_over_k0_range_begin);

          mobility_scan_num_range_end =
            tims_frame_csp.get()->getScanIndexFromOneOverK0(
              mobility_one_over_k0_range_end);

          asked_ion_mobility_scan_num_range = true;
        }

      qDebug() << "tims_frame_id=" << tims_frame_id;
      // Now that we know if the user has asked for an ion mobility range,
      // either using scan indices or 1/K0 values, we need to double check the
      // range borders.

      quint32 count_of_mobility_scans = tims_frame_csp->getTotalNumberOfScans();

      if(asked_ion_mobility_scan_num_range)
        {
          if(mobility_scan_num_range_end > (count_of_mobility_scans - 1))
            {
              mobility_scan_num_range_end = count_of_mobility_scans - 1;
            }
        }
      else
        {
          mobility_scan_num_range_begin = 0;
          mobility_scan_num_range_end   = count_of_mobility_scans - 1;
        }

      qDebug() << "tims_frame_id=" << tims_frame_id;
      // Now, with or without the peak list, we have to craft a qualified mass
      // spectrum that will hold all the data about the data in it.
      QualifiedMassSpectrum mass_spectrum;

      MassSpectrumId spectrum_id;

      spectrum_id.setSpectrumIndex(tims_frame_id);
      spectrum_id.setMsRunId(getMsRunId());

      mass_spectrum.setMassSpectrumId(spectrum_id);

      qDebug() << "tims_frame_id=" << tims_frame_id;
      // We want to document the retention time!
      mass_spectrum.setRtInSeconds(tims_frame_csp.get()->getRtInSeconds());

      // We do want to document the ms level of the spectrum and possibly
      // the precursor's m/z and charge.
      unsigned int frame_ms_level = tims_frame_csp.get()->getMsLevel();
      mass_spectrum.setMsLevel(frame_ms_level);

      qDebug() << "tims_frame_id=" << tims_frame_id;
      std::vector<TimsDdaPrecursors::SpectrumDescr> dda_spectrum_descr_list;
      TimsDiaSlices::MsMsWindowGroup *p_dia_window_group = nullptr;
      std::size_t frame_global_slice_begin               = 0;
      if(frame_ms_level > 1)
        {
          if(msp_timsData.get()->isDdaRun())
            {
              TimsDdaPrecursors *dda_precursors_p =
                msp_timsData.get()->getTimsDdaPrecursorsPtr();
              dda_spectrum_descr_list =
                dda_precursors_p->getSpectrumDescrListByFrameId(tims_frame_id);
            }

          if(msp_timsData.get()->isDiaRun())
            {

              qDebug() << "tims_frame_id=" << tims_frame_id;
              p_dia_window_group = msp_timsData.get()
                                     ->getTimsDiaSlicesPtr()
                                     ->getMapFrame2WindowGroupPtr()
                                     .at(tims_frame_id);
              frame_global_slice_begin =
                msp_timsData.get()
                  ->getTimsDiaSlicesPtr()
                  ->getGlobalSliceIndexBeginByFrameId(tims_frame_id);
            }
        }

      qDebug() << "tims_frame_id=" << tims_frame_id;
      // The scan index is the index of the scan in the *whole* mass data file,
      // it is a sequential number of scans over all the frames.
      std::size_t scan_index = current_frame_record.m_globalScanIndex -
                               current_frame_record.m_scanCount +
                               mobility_scan_num_range_begin;

      QualifiedMassSpectrum mass_spectrum_no_precursor_data(mass_spectrum);
      for(quint32 iter_scan_index = mobility_scan_num_range_begin;
          iter_scan_index <= mobility_scan_num_range_end;
          iter_scan_index++)
        {
          mass_spectrum = mass_spectrum_no_precursor_data;
          mass_spectrum.getMassSpectrumId().setSpectrumIndex(scan_index);

          mass_spectrum.getMassSpectrumId().setNativeId(
            QString("frame_id=%1 scan_index=%2 global_scan_index=%3")
              .arg(tims_frame_id)
              .arg(iter_scan_index)
              .arg(scan_index));

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

          // qDebug() << "Scan's mz_minimum_index:" << mz_minimum_index_out
          //          << "and mz_maximum_index:" << mz_maximum_index_out;

          // Arrival time
          mass_spectrum.setDtInMilliSeconds(

            tims_frame_csp.get()->getDriftTimeInMilliseconds(iter_scan_index));
          // 1/K0
          mass_spectrum.setParameterValue(
            QualifiedMassSpectrumParameter::IonMobOneOverK0,
            tims_frame_csp.get()->getOneOverK0Transformation(iter_scan_index));

          if(dda_spectrum_descr_list.size() > 0)
            {
              std::size_t scan_index = (std::size_t)iter_scan_index;
              auto it_spectrum_descr = std::find_if(
                dda_spectrum_descr_list.begin(),
                dda_spectrum_descr_list.end(),
                [scan_index](
                  const TimsDdaPrecursors::SpectrumDescr &spectrum_descr) {
                  if(scan_index < spectrum_descr.scan_mobility_start)
                    return false;
                  if(scan_index > spectrum_descr.scan_mobility_end)
                    return false;
                  return true;
                });

              if(it_spectrum_descr != dda_spectrum_descr_list.end())
                {
                  qDebug() << "scan_index=" << scan_index
                           << " spectrum_descr.scan_mobility_end="
                           << it_spectrum_descr->scan_mobility_end;
                  mass_spectrum.appendPrecursorIonData(
                    it_spectrum_descr->precursor_ion_data);

                  mass_spectrum.setPrecursorNativeId(
                    QString(
                      "frame_id=%1 begin=%2 end=%3 precursor=%4 idxms1=%5")
                      .arg(it_spectrum_descr->parent_frame)
                      .arg(it_spectrum_descr->scan_mobility_start)
                      .arg(it_spectrum_descr->scan_mobility_end)
                      .arg(it_spectrum_descr->precursor_id)
                      .arg(it_spectrum_descr->ms1_index));

                  mass_spectrum.setParameterValue(
                    QualifiedMassSpectrumParameter::IsolationMz,
                    it_spectrum_descr->isolationMz);
                  mass_spectrum.setParameterValue(
                    QualifiedMassSpectrumParameter::IsolationMzWidth,
                    it_spectrum_descr->isolationWidth);

                  mass_spectrum.setParameterValue(
                    QualifiedMassSpectrumParameter::CollisionEnergy,
                    it_spectrum_descr->collisionEnergy);
                  mass_spectrum.setParameterValue(
                    QualifiedMassSpectrumParameter::BrukerPrecursorIndex,
                    (quint64)it_spectrum_descr->precursor_id);
                }
            }
          if(p_dia_window_group != nullptr)
            {


              std::size_t scan_index = (std::size_t)iter_scan_index;
              auto it_dia_window     = std::find_if(
                p_dia_window_group->begin(),
                p_dia_window_group->end(),
                [scan_index](const TimsDiaSlices::MsMsWindow &dia_window) {
                  if(scan_index < dia_window.ScanNumBegin)
                    return false;
                  if(scan_index > dia_window.ScanNumEnd)
                    return false;
                  return true;
                });

              if(it_dia_window != p_dia_window_group->end())
                {
                  qDebug() << "scan_index=" << scan_index
                           << " it_dia_window->ScanNumEnd="
                           << it_dia_window->ScanNumEnd;

                  mass_spectrum.setPrecursorNativeId(
                    QString("window_group=%1 begin=%2 end=%3 frame=%4 scan=%5 "
                            "global_slice_id=%6")
                      .arg(it_dia_window->WindowGroup)
                      .arg(it_dia_window->ScanNumBegin)
                      .arg(it_dia_window->ScanNumEnd)
                      .arg(msp_timsData.get()
                             ->getTimsDiaSlicesPtr()
                             ->getLastMs1FrameIdByMs2FrameId(tims_frame_id))
                      .arg(iter_scan_index)
                      .arg(it_dia_window->SliceIndex +
                           frame_global_slice_begin));

                  mass_spectrum.setParameterValue(
                    QualifiedMassSpectrumParameter::IsolationMz,
                    it_dia_window->IsolationMz);
                  mass_spectrum.setParameterValue(
                    QualifiedMassSpectrumParameter::IsolationMzWidth,
                    it_dia_window->IsolationWidth);

                  mass_spectrum.setParameterValue(
                    QualifiedMassSpectrumParameter::CollisionEnergy,
                    it_dia_window->CollisionEnergy);
                }
            }
          if(config.needPeakList())
            {
              quint32 mz_minimum_index_out = 0;
              quint32 mz_maximum_index_out = 0;

              auto raw_trace =
                tims_frame_csp.get()->getMobilityScan(iter_scan_index,
                                                      mz_index_merge_window,
                                                      mz_range_begin,
                                                      mz_range_end,
                                                      mz_minimum_index_out,
                                                      mz_maximum_index_out);

              // qDebug() << "Ion mobility scan's raw trace size:"
              //          << raw_trace.size();

              mass_spectrum.setEmptyMassSpectrum(false);

              mass_spectrum.setParameterValue(
                QualifiedMassSpectrumParameter::TimsFrameMzIndexBegin,
                mz_minimum_index_out);
              mass_spectrum.setParameterValue(
                QualifiedMassSpectrumParameter::TimsFrameMzIndexEnd,
                mz_maximum_index_out);


              qDebug();
              mass_spectrum.setMassSpectrumSPtr(
                std::make_shared<MassSpectrum>(raw_trace));

              qDebug() << mass_spectrum.getRtInSeconds();
            }
          else
            {
              mass_spectrum.setEmptyMassSpectrum(true);
            }

          // qDebug() << "mz_index_merge_window=" << mz_index_merge_window;
          handler.setQualifiedMassSpectrum(mass_spectrum);
          ++number_of_mobility_scans_set_as_qualified_mass_spectra;
          scan_index++;
        }
    }

  qDebug() << "Total number of loaded mass spectra:"
           << number_of_mobility_scans_set_as_qualified_mass_spectra;
}

void
TimsMsRunReader::readSpectrumCollectionByMsLevel(
  SpectrumCollectionHandlerInterface &handler, unsigned int ms_level)
{

  qDebug();

  try
    {

      if(msp_timsData->isDdaRun())
        {
          msp_timsData.get()
            ->getTimsDdaPrecursorsPtr()
            ->rawReaderSpectrumCollectionByMsLevel(
              getMsRunId(), handler, ms_level);
        }
    }

  catch(ExceptionInterrupted &)
    {
      qDebug() << "Reading of MS data interrupted by the user.";
    }

  // Now let the loading handler know that the loading of the data has ended.
  // The handler might need this "signal" to perform additional tasks or to
  // cleanup cruft.

  // qDebug() << "Loading ended";
  handler.loadingEnded();
}


std::size_t
TimsMsRunReader::spectrumListSize() const
{
  return msp_timsData->getTotalScanCount();
}

