/**
 * \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 "pappsomspp/core/msrun/private/timsmsrunreader.h"
#include "../../vendors/tims/timsddaprecursors.h"
#include "../../vendors/tims/timsdiaslices.h"
#include "pappsomspp/core/exception/exceptionnotimplemented.h"
#include "pappsomspp/core/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)
    {
      qDebug() << "tims_frame_id=" << tims_frame_id;

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

      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 frame_scan_count = tims_frame_csp->getTotalNumberOfScans();

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

      // Now, with or without the peak list, we have to craft a qualified mass
      // spectrum that will hold all the metadata about the data potentially
      // found in it.
      QualifiedMassSpectrum mass_spectrum;

      MassSpectrumId spectrum_id;

      // FIXME: is this correct ? The spectrum index should be set later
      // when creating mass spectra that correspond to scans inside frames, no ?
      spectrum_id.setSpectrumIndex(tims_frame_id);
      spectrum_id.setMsRunId(getMsRunId());

      // FIXME: this is never true.
      if(!tims_frame_id)
        qDebug() << "20240724 - Now setting a spectrum index of 0";

      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 values.
      unsigned int frame_ms_level = tims_frame_csp.get()->getMsLevel();
      mass_spectrum.setMsLevel(frame_ms_level);

      std::vector<TimsDdaPrecursors::SpectrumDescr> dda_frame_precursor_spectrum_descr_list;

      TimsDiaSlices::MsMsWindowGroup *p_dia_window_group = nullptr;
      std::size_t frame_global_slice_begin               = 0;

      if(frame_ms_level > 1)
        {
          qDebug() << "Frame MS level:" << frame_ms_level;

          if(msp_timsData.get()->isDdaRun())
            {
              TimsDdaPrecursors *dda_precursors_p = msp_timsData.get()->getTimsDdaPrecursorsPtr();

              dda_frame_precursor_spectrum_descr_list =
                dda_precursors_p->getSpectrumDescrListByFrameId(tims_frame_id);

              qDebug() << "Got frame's" << dda_frame_precursor_spectrum_descr_list.size()
                       << "precursors for MS level" << frame_ms_level;

              QString frame_precursors;

              for(auto desc : dda_frame_precursor_spectrum_descr_list)
                {
                  frame_precursors += QString("(%1, %2, %3\n)")
                                        .arg(desc.isolationMz)
                                        .arg(desc.isolationWidth)
                                        .arg(desc.ms1_index);
                }

              qDebug() << "20240724 - The count of precursors:"
                       << dda_frame_precursor_spectrum_descr_list.size() << "\n"
                       << frame_precursors;
            }

          if(msp_timsData.get()->isDiaRun())
            {
              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.

      // FIXME: if we remove m_scanCount and then add
      // mobility_scan_num_range_begin, does that mean that the
      // m_globalScanIndex of the frame is the index of the last mobility scan
      // of that frame ?

      // FIXME: is this the reason we never have a scan_index = 0 ?
      std::size_t scan_index = current_frame_record.m_globalScanIndex -
                               current_frame_record.m_scanCount + mobility_scan_num_range_begin;

      // FIXME: is this needed ?
      // QualifiedMassSpectrum mass_spectrum_no_precursor_data(mass_spectrum);

      // Iterate in the frame's scans, only in the asked range. In each frame, these scan numbers
      // start from 0 and end at frame_scan_count - 1, that is, these are not global scan numbers.
      for(quint32 iter_scan_index = mobility_scan_num_range_begin;
          iter_scan_index <= mobility_scan_num_range_end;
          ++iter_scan_index, ++scan_index)
        {
          // FIXME: I do not understand the logic here: we are copying mass
          // spectra around? mass_spectrum = mass_spectrum_no_precursor_data;
          QualifiedMassSpectrum qualified_mass_spectrum(mass_spectrum);
          qualified_mass_spectrum.getMassSpectrumId().setSpectrumIndex(scan_index);

          qualified_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_scan_index:" << iter_scan_index;

          // Arrival time
          qualified_mass_spectrum.setDtInMilliSeconds(
            tims_frame_csp.get()->getDriftTimeInMilliseconds(iter_scan_index));

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

#if 0
          // Debugging code to check that it is true that there is
          // systematically a single precursor per mobility scan.
          // See FIXME below in the code.

          std::size_t per_scan_index_count_of_matching_precursors = 0;

          for(TimsDdaPrecursors::SpectrumDescr prec_spec_desc :
              dda_precursor_spectrum_descr_list)
            {
              std::size_t scan_index = (std::size_t)iter_scan_index;

              if(scan_index >= precursor_spectrum_descr_iterator->scan_mobility_start &&
                 scan_index <= precursor_spectrum_descr_iterator->scan_mobility_end)
                {
                  ++per_scan_index_count_of_matching_precursors;

                  qDebug() << "20240724 - Found a precursor desc for scan_index"
                           << scan_index
                           << "that matched the mobility scan range:"
                           << precursor_spectrum_descr_iterator->isolationMz << ","
                           << precursor_spectrum_descr_iterator->isolationWidth;

                  qualified_mass_spectrum.appendPrecursorIonData(
                    precursor_spectrum_descr_iterator->precursor_ion_data);

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

                  qualified_mass_spectrum.setParameterValue(
                    QualifiedMassSpectrumParameter::IsolationMz,
                    precursor_spectrum_descr_iterator->isolationMz);
                  qualified_mass_spectrum.setParameterValue(
                    QualifiedMassSpectrumParameter::IsolationMzWidth,
                    precursor_spectrum_descr_iterator->isolationWidth);

                  qualified_mass_spectrum.setParameterValue(
                    QualifiedMassSpectrumParameter::CollisionEnergy,
                    precursor_spectrum_descr_iterator->collisionEnergy);
                  qualified_mass_spectrum.setParameterValue(
                    QualifiedMassSpectrumParameter::BrukerPrecursorIndex,
                    (quint64)precursor_spectrum_descr_iterator->precursor_id);
                }
            }


          qDebug() << "20240724 - For scan_index" << scan_index << "found"
                   << per_scan_index_count_of_matching_precursors
                   << "matching precursors";

          if(per_scan_index_count_of_matching_precursors > 1)
            qFatal();
#endif

          // FIXME: this code seems to assume that there should be only one
          // precursor in the currently iterated scan index. Is this necessarily
          // true ?

          if(dda_frame_precursor_spectrum_descr_list.size() > 0)
            {
              std::size_t local_iter_scan_index = (std::size_t)iter_scan_index;

              auto precursor_spectrum_descr_iterator = std::find_if(
                dda_frame_precursor_spectrum_descr_list.begin(),
                dda_frame_precursor_spectrum_descr_list.end(),
                [local_iter_scan_index](const TimsDdaPrecursors::SpectrumDescr &spectrum_descr) {
                  if(local_iter_scan_index < spectrum_descr.scan_mobility_start)
                    return false;
                  if(local_iter_scan_index > spectrum_descr.scan_mobility_end)
                    return false;
                  return true;
                });

              if(precursor_spectrum_descr_iterator != dda_frame_precursor_spectrum_descr_list.end())
                {
                  qDebug() << "local_iter_scan_index=" << local_iter_scan_index
                           << " spectrum_descr.scan_mobility_end="
                           << precursor_spectrum_descr_iterator->scan_mobility_end;

                  qualified_mass_spectrum.appendPrecursorIonData(
                    precursor_spectrum_descr_iterator->precursor_ion_data);

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

                  qDebug() << "Native precursor ID:"
                           << qualified_mass_spectrum.getPrecursorNativeId();

                  // FIXME:
                  // The code below makes the program crash at
                  // src/pappsomspp/msrun/msrundatasettree.cpp @ 110
                  // "ERROR could not find a tree node matching the index"
                  // because ms1_index is not found in any spectrum of the tree.

                  // qualified_mass_spectrum.setPrecursorSpectrumIndex(
                  //   precursor_spectrum_descr_iterator->ms1_index);
                  qDebug();

                  qualified_mass_spectrum.setParameterValue(
                    QualifiedMassSpectrumParameter::IsolationMz,
                    precursor_spectrum_descr_iterator->isolationMz);
                  qualified_mass_spectrum.setParameterValue(
                    QualifiedMassSpectrumParameter::IsolationMzWidth,
                    precursor_spectrum_descr_iterator->isolationWidth);

                  qualified_mass_spectrum.setParameterValue(
                    QualifiedMassSpectrumParameter::CollisionEnergy,
                    precursor_spectrum_descr_iterator->collisionEnergy);

                  qualified_mass_spectrum.setParameterValue(
                    QualifiedMassSpectrumParameter::BrukerPrecursorIndex,
                    (quint64)precursor_spectrum_descr_iterator->precursor_id);
                }
              qDebug();
            }

          qDebug();

          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;

                  qualified_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));

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

                  qualified_mass_spectrum.setParameterValue(
                    QualifiedMassSpectrumParameter::CollisionEnergy,
                    it_dia_window->CollisionEnergy);
                }
            }

          qDebug();

          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();

              qualified_mass_spectrum.setEmptyMassSpectrum(false);

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


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

              qDebug() << "RT (s):" << qualified_mass_spectrum.getRtInSeconds();
            }
          else
            {
              qualified_mass_spectrum.setEmptyMassSpectrum(true);
            }

          qDebug();
          handler.setQualifiedMassSpectrum(qualified_mass_spectrum);
          qDebug();
          ++number_of_mobility_scans_set_as_qualified_mass_spectra;
        }

      qDebug();
    }

  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();
}

std::size_t
TimsMsRunReader::spectrumStringIdentifier2SpectrumIndex(const QString &spectrum_identifier
                                                        [[maybe_unused]])
{
  throw ExceptionNotImplemented(
    QObject::tr("%1 %2 %3 not implemented").arg(__FILE__).arg(__FUNCTION__).arg(__LINE__));
}
