/* Copyright (C) 2009-2023 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
   SPDX-License-Identifier: GPL-3.0-only
   Authors: Valentina Akishina, Ivan Kisel, Sergey Gorbunov [committer], Igor Kulakov, Sergei Zharko, Maksym Zyzak */

/*
 *=====================================================
 *
 *  CBM Level 1 4D Reconstruction
 *
 *  Authors: V.Akishina, I.Kisel,  S.Gorbunov, I.Kulakov, M.Zyzak
 *  Documentation: V.Akishina
 *
 *  e-mail : v.akishina@gsi.de
 *
 *=====================================================
 *
 *  Finds tracks using the Cellular Automaton algorithm
 *
 */

#include "CaTrackFinder.h"

#include "CaTimer.h"
#include "CaTrack.h"
#include "CaTriplet.h"

#include <chrono>
#include <fstream>
#include <sstream>
#include <thread>


namespace cbm::algo::ca
{
  using constants::phys::ProtonMassD;
  using constants::phys::SpeedOfLightInv;
  using constants::phys::SpeedOfLightInvD;

  // -------------------------------------------------------------------------------------------------------------------
  //
  TrackFinder::TrackFinder(const ca::Parameters<fvec>& pars, const fscal mass, const ca::TrackingMode& mode,
                           TrackingMonitorData& monitorData, int nThreads, double& recoTime)
    : fParameters(pars)
    , fDefaultMass(mass)
    , fTrackingMode(mode)
    , fMonitorData(monitorData)
    , fvMonitorDataThread(nThreads)
    , fvWData(nThreads)
    , fNofThreads(nThreads)
    , fCaRecoTime(recoTime)
    , fvRecoTracks(nThreads)
    , fvRecoHitIndices(nThreads)
    , fWindowLength((ca::TrackingMode::kMcbm == mode) ? 500 : 10000)
  {
    assert(fNofThreads > 0);

    for (int iThread = 0; iThread < fNofThreads; ++iThread) {
      fvRecoTracks[iThread].SetName(std::string("TrackFinder::fvRecoTracks_") + std::to_string(iThread));
      fvRecoHitIndices[iThread].SetName(std::string("TrackFinder::fvRecoHitIndices_") + std::to_string(iThread));
    }
  }

  // -------------------------------------------------------------------------------------------------------------------
  //CBM Level 1 4D Reconstruction
  //Finds tracks using the Cellular Automaton algorithm
  //
  TrackFinder::Output_t TrackFinder::FindTracks(const InputData& input, TimesliceHeader& tsHeader)
  {
    Output_t output;
    Vector<Track>& recoTracks        = output.first;   //reconstructed tracks
    Vector<ca::HitIndex_t>& recoHits = output.second;  //packed hits of reconstructed tracks

    if (input.GetNhits() < 1) {
      LOG(warn) << "No hits were passed to the ca::TrackFinder. Stopping the routine";
      return output;
    }

    //
    // The main CaTrackFinder routine
    // It splits the input data into sub-timeslices
    // and runs the track finder over the sub-slices
    //
    fMonitorData.StartTimer(ETimer::Tracking);
    fMonitorData.StartTimer(ETimer::PrepareTimeslice);
    fMonitorData.IncrementCounter(ECounter::TrackingCall);
    fMonitorData.IncrementCounter(ECounter::RecoHit, input.GetNhits());

    auto timerStart = std::chrono::high_resolution_clock::now();

    auto& wDataThread0 = fvWData[0];  // NOTE: Thread 0 must be always defined

    // ----- Reset data arrays -----------------------------------------------------------------------------------------

    wDataThread0.HitKeyFlags().reset(input.GetNhitKeys(), 0);

    fHitTimeInfo.reset(input.GetNhits());

    // TODO: move these values to Parameters namespace (S.Zharko)

    // length of sub-TS
    const fscal minProtonMomentum = 0.1;
    const fscal preFactor         = sqrt(1. + ProtonMassD * ProtonMassD / (minProtonMomentum * minProtonMomentum));
    const fscal targX             = fParameters.GetTargetPositionX()[0];
    const fscal targY             = fParameters.GetTargetPositionY()[0];
    const fscal targZ             = fParameters.GetTargetPositionZ()[0];

    fStatTsStart    = std::numeric_limits<fscal>::max();  // end time of the TS
    fStatTsEnd      = 0.;                                 // end time of the TS
    fStatNhitsTotal = 0;

    // calculate possible event time for the hits (fHitTimeInfo array)
    for (int iStream = 0; iStream < input.GetNdataStreams(); ++iStream) {

      fscal maxTimeBeforeHit = std::numeric_limits<fscal>::lowest();
      const int nStreamHits  = input.GetStreamNhits(iStream);
      fStatNhitsTotal += nStreamHits;

      for (int ih = 0; ih < nStreamHits; ++ih) {

        ca::HitIndex_t caHitId      = input.GetStreamStartIndex(iStream) + ih;
        const ca::Hit& h            = input.GetHit(caHitId);
        const ca::Station<fvec>& st = fParameters.GetStation(h.Station());
        const fscal dx              = h.X() - targX;
        const fscal dy              = h.Y() - targY;
        const fscal dz              = h.Z() - targZ;
        const fscal l               = sqrt(dx * dx + dy * dy + dz * dz);
        const fscal timeOfFlightMin = l * SpeedOfLightInv;
        const fscal timeOfFlightMax = 1.5 * l * preFactor * SpeedOfLightInvD;
        const fscal dt              = h.RangeT();
        // TODO: Is it possible, that the proton mass selection affects the search of heavier particles?

        CaHitTimeInfo& info = fHitTimeInfo[caHitId];
        info.fEventTimeMin  = st.timeInfo ? (h.T() - dt - timeOfFlightMax) : -1.e10;
        info.fEventTimeMax  = st.timeInfo ? (h.T() + dt - timeOfFlightMin) : 1.e10;

        // NOTE: if not a MT part, use wDataThread0.IsHitKeyUsed, it will be later copied to other threads
        if (info.fEventTimeMin > 500.e6 || info.fEventTimeMax < -500.) {  // cut hits with bogus start time > 500 ms
          wDataThread0.IsHitKeyUsed(h.FrontKey()) = 1;
          wDataThread0.IsHitKeyUsed(h.BackKey())  = 1;
          LOG(error) << "CATrackFinder: skip bogus hit " << h.ToString();
          continue;
        }
        maxTimeBeforeHit       = std::max(maxTimeBeforeHit, info.fEventTimeMax);
        info.fMaxTimeBeforeHit = maxTimeBeforeHit;
        fStatTsStart           = std::min(fStatTsStart, info.fEventTimeMax);
        fStatTsEnd             = std::max(fStatTsEnd, info.fEventTimeMin);
      }

      fscal minTimeAfterHit = std::numeric_limits<fscal>::max();
      // loop in the reverse order to fill CaHitTimeInfo::fMinTimeAfterHit fields

      for (int ih = nStreamHits - 1; ih >= 0; --ih) {
        ca::HitIndex_t caHitId = input.GetStreamStartIndex(iStream) + ih;
        const ca::Hit& h       = input.GetHit(caHitId);
        if (wDataThread0.IsHitKeyUsed(h.FrontKey()) || wDataThread0.IsHitKeyUsed(h.BackKey())) {
          continue;
        }  // the hit is skipped
        CaHitTimeInfo& info   = fHitTimeInfo[caHitId];
        minTimeAfterHit       = std::min(minTimeAfterHit, info.fEventTimeMin);
        info.fMinTimeAfterHit = minTimeAfterHit;
      }

      if (0) {
        static int tmp = 0;
        if (tmp < 10000) {
          tmp++;
          LOG(warning) << "\n\n stream " << iStream << " hits " << nStreamHits << "\n\n";
          for (int ih = 0; (ih < nStreamHits) && (tmp < 10000); ++ih) {
            ca::HitIndex_t caHitId = input.GetStreamStartIndex(iStream) + ih;
            const ca::Hit& h       = input.GetHit(caHitId);
            if (wDataThread0.IsHitKeyUsed(h.FrontKey()) || wDataThread0.IsHitKeyUsed(h.BackKey())) {
              continue;
            }  // the hit is skipped
            CaHitTimeInfo& info = fHitTimeInfo[caHitId];
            if (h.Station() < 4) {
              tmp++;
              LOG(warning) << " hit sta " << h.Station() << " stream " << iStream << " time " << h.T() << " event time "
                           << info.fEventTimeMin << " .. " << info.fEventTimeMax << " max time before hit "
                           << info.fMaxTimeBeforeHit << " min time after hit " << info.fMinTimeAfterHit;
            }
          }
        }
      }
    }
    // all hits belong to one sub-timeslice; 1 s is the maximal length of the TS
    fStatTsEnd = std::clamp(fStatTsEnd, fStatTsStart, fStatTsStart + 1.e9f);

    LOG(debug) << "CA tracker process time slice " << fStatTsStart * 1.e-6 << " -- " << fStatTsEnd * 1.e-6
               << " [ms] with " << fStatNhitsTotal << " hits";

    int nWindows = static_cast<int>((fStatTsEnd - fStatTsStart) / fWindowLength) + 1;
    if (nWindows < 1) {  // Situation, when fStatTsEnd == fStatTsStart
      nWindows = 1;
    }

    // int nWindowsThread = nWindows / fNofThreads;
    // LOG(info) << "CA: estimated number of time windows: " << nWindows;

    std::vector<std::pair<fscal, fscal>> vWindowRangeThread(fNofThreads);
    {  // Estimation of number of hits in time windows
      //Timer time;
      //time.Start();
      const HitIndex_t nHitsTot = input.GetNhits();
      const int nSt             = fParameters.GetNstationsActive();

      // Count number of hits per window and station
      std::vector<HitIndex_t> nHitsWindowSta(nWindows * nSt, 0);

      for (HitIndex_t iHit = 0; iHit < nHitsTot; ++iHit) {
        const auto& hit  = input.GetHit(iHit);
        const auto& info = fHitTimeInfo[iHit];
        int iWindow      = static_cast<int>((info.fEventTimeMin - fStatTsStart) / fWindowLength);
        if (iWindow < 0) {
          iWindow = 0;
        }
        if (iWindow >= nWindows) {
          LOG(error) << "ca: Hit out of range: iHit = " << iHit << ", min. event time = " << info.fEventTimeMin * 1.e-6
                     << " ms, window = " << iWindow;
          continue;
        }
        ++nHitsWindowSta[hit.Station() + iWindow * nSt];
      }

      // Remove hits from the "monster events"
      if (ca::TrackingMode::kMcbm == fTrackingMode) {
        const auto maxNofHitsSta = static_cast<HitIndex_t>(50 * fWindowLength / 1.e3);
        for (auto& content : nHitsWindowSta) {
          if (content > maxNofHitsSta) {
            content = 0;
          }
        }
      }

      // Integrate number of hits per window
      std::vector<HitIndex_t> nHitsWindow;
      nHitsWindow.reserve(nWindows);
      HitIndex_t nHitsCollected = 0;
      for (auto it = nHitsWindowSta.begin(); it != nHitsWindowSta.end(); it += nSt) {
        nHitsCollected = nHitsWindow.emplace_back(std::accumulate(it, it + nSt, nHitsCollected));
      }

      // Get time range for threads
      const HitIndex_t nHitsPerThread = nHitsCollected / fNofThreads;
      auto windowIt                   = nHitsWindow.begin();
      vWindowRangeThread[0].first     = fStatTsStart;
      for (int iTh = 1; iTh < fNofThreads; ++iTh) {
        windowIt                           = std::lower_bound(windowIt, nHitsWindow.end(), iTh * nHitsPerThread);
        const size_t iWbegin               = std::distance(nHitsWindow.begin(), windowIt) + 1;
        vWindowRangeThread[iTh].first      = fStatTsStart + iWbegin * fWindowLength;
        vWindowRangeThread[iTh - 1].second = vWindowRangeThread[iTh].first;
      }
      vWindowRangeThread[fNofThreads - 1].second = fStatTsEnd;

      //time.Stop();
      //LOG(info) << "Thread boarders estimation time: " << time.GetTotalMs() << " ms";
      LOG(debug) << "Fraction of hits from monster events: "
                 << static_cast<double>(nHitsTot - nHitsCollected) / nHitsTot;
    }

    // cut data into sub-timeslices and process them one by one
    //for (int iThread = 0; iThread < fNofThreads; ++iThread) {
    //  vWindowStartThread[iThread] = fStatTsStart + iThread * nWindowsThread * fWindowLength;
    //  vWindowEndThread[iThread] =
    //    vWindowStartThread[iThread] + nWindowsThread * fWindowLength;
    //}

    for (int iThread = 0; iThread < fNofThreads; ++iThread) {
      auto& entry  = vWindowRangeThread[iThread];
      double start = entry.first * 1.e-6;
      double end   = entry.second * 1.e-6;
      LOG(debug) << "Thread: " << iThread << " from " << start << " ms  to " << end << " ms (delta = " << end - start
                 << " ms)";
    }

    // Statistics for monitoring
    std::vector<int> vStatNwindows(fNofThreads), vStatNhitsProcessed(fNofThreads);

    fMonitorData.StopTimer(ETimer::PrepareTimeslice);
    // Save tracks
    if (fNofThreads == 1) {
      this->FindTracksThread(input, 0, std::ref(vWindowRangeThread[0]), std::ref(vStatNwindows[0]),
                             std::ref(vStatNhitsProcessed[0]));
      fMonitorData.StartTimer(ETimer::StoreTracksFinal);
      recoTracks = std::move(fvRecoTracks[0]);
      recoHits   = std::move(fvRecoHitIndices[0]);
      fMonitorData.StopTimer(ETimer::StoreTracksFinal);
    }
    else {
      std::vector<std::thread> vThreadList;
      vThreadList.reserve(fNofThreads);
      for (int iTh = 0; iTh < fNofThreads; ++iTh) {
        vThreadList.emplace_back(&TrackFinder::FindTracksThread, this, std::ref(input), iTh,
                                 std::ref(vWindowRangeThread[iTh]), std::ref(vStatNwindows[iTh]),
                                 std::ref(vStatNhitsProcessed[iTh]));
      }
      for (auto& th : vThreadList) {
        if (th.joinable()) {
          th.join();
        }
      }
      fMonitorData.StartTimer(ETimer::StoreTracksFinal);
      auto Operation  = [](size_t acc, const auto& v) { return acc + v.size(); };
      int nRecoTracks = std::accumulate(fvRecoTracks.begin(), fvRecoTracks.end(), 0, Operation);
      int nRecoHits   = std::accumulate(fvRecoHitIndices.begin(), fvRecoHitIndices.end(), 0, Operation);
      recoTracks.reserve(nRecoTracks);
      recoHits.reserve(nRecoHits);
      for (int iTh = 0; iTh < fNofThreads; ++iTh) {
        recoTracks.insert(recoTracks.end(), fvRecoTracks[iTh].begin(), fvRecoTracks[iTh].end());
        recoHits.insert(recoHits.end(), fvRecoHitIndices[iTh].begin(), fvRecoHitIndices[iTh].end());
      }
      fMonitorData.StopTimer(ETimer::StoreTracksFinal);
    }

    fMonitorData.IncrementCounter(ECounter::RecoTrack, recoTracks.size());
    fMonitorData.IncrementCounter(ECounter::RecoHitUsed, recoHits.size());

    auto timerEnd = std::chrono::high_resolution_clock::now();
    fCaRecoTime   = (double) (std::chrono::duration<double>(timerEnd - timerStart).count());

    // Add thread monitors to the main monitor
    for (auto& monitor : fvMonitorDataThread) {
      fMonitorData.AddMonitorData(monitor, true);
      //fMonitorData.AddMonitorData(monitor);
      monitor.Reset();
    }

    const int statNhitsProcessedTotal = std::accumulate(vStatNhitsProcessed.begin(), vStatNhitsProcessed.end(), 0);
    const int statNwindowsTotal       = std::accumulate(vStatNwindows.begin(), vStatNwindows.end(), 0);

    // Filling TS headear
    tsHeader.Start() = fStatTsStart;
    tsHeader.End()   = fStatTsEnd;

    fMonitorData.StopTimer(ETimer::Tracking);

    LOG(debug) << "CA tracker: time slice finished. Reconstructed " << recoTracks.size() << " tracks with "
               << recoHits.size() << " hits. Processed " << statNhitsProcessedTotal << " hits in " << statNwindowsTotal
               << " time windows. Reco time " << fCaRecoTime / 1.e9 << " s";
    return output;
  }

  // -------------------------------------------------------------------------------------------------------------------
  //
  void TrackFinder::FindTracksThread(const InputData& input, int iThread, std::pair<fscal, fscal>& windowRange,
                                     int& statNwindows, int& statNhitsProcessed)
  {
    //std::stringstream filename;
    //filename << "./dbg_caTrackFinder::FindTracksThread_" << iThread << ".txt";
    //std::ofstream out(filename.str());
    Timer timer;
    timer.Start();

    auto& monitor = fvMonitorDataThread[iThread];
    monitor.StartTimer(ETimer::TrackingThread);
    monitor.StartTimer(ETimer::PrepareThread);

    // Init vectors
    auto& tracks     = fvRecoTracks[iThread];
    auto& hitIndices = fvRecoHitIndices[iThread];
    auto& wData      = fvWData[iThread];
    {
      const int nStations          = fParameters.GetNstationsActive();
      const size_t nHitsTot        = input.GetNhits();
      const size_t nHitsExpected   = 2 * nHitsTot;
      const size_t nTracksExpected = 2 * nHitsTot / nStations;
      tracks.clear();
      tracks.reserve(nTracksExpected / fNofThreads);
      hitIndices.clear();
      hitIndices.reserve(nHitsExpected / fNofThreads);
      if (iThread != 0) {
        wData.HitKeyFlags() = fvWData[0].HitKeyFlags();
      }
      for (int iS = 0; iS < nStations; ++iS) {
        wData.TsHitIndices(iS).clear();
        wData.TsHitIndices(iS).reserve(nHitsTot);
      }
    }

    // Begin and end index of hit-range for streams
    std::vector<std::pair<HitIndex_t, HitIndex_t>> streamHitRanges(input.GetNdataStreams(), {0, 0});

    // Define first hit, skip all the hits, which are before the first window
    for (size_t iStream = 0; iStream < streamHitRanges.size(); ++iStream) {
      auto& range  = streamHitRanges[iStream];
      range.first  = input.GetStreamStartIndex(iStream);
      range.second = input.GetStreamStopIndex(iStream);

      for (HitIndex_t caHitId = range.first; caHitId < range.second; ++caHitId) {
        const ca::Hit& h = input.GetHit(caHitId);
        if (wData.IsHitKeyUsed(h.FrontKey()) || wData.IsHitKeyUsed(h.BackKey())) {
          continue;
        }
        const CaHitTimeInfo& info = fHitTimeInfo[caHitId];
        if (info.fMaxTimeBeforeHit < windowRange.first) {
          range.first = caHitId + 1;
        }
        if (info.fMinTimeAfterHit > windowRange.second) {
          range.second = caHitId;
        }
      }
    }

    int statLastLogTimeChunk = -1;

    // Track finder algorithm for the time window
    ca::TrackFinderWindow trackFinderWindow(fParameters, fDefaultMass, fTrackingMode, monitor);
    trackFinderWindow.InitTimeslice(input.GetNhitKeys());

    monitor.StopTimer(ETimer::PrepareThread);

    while (true) {
      monitor.IncrementCounter(ECounter::SubTS);
      // select the sub-slice hits
      for (int iS = 0; iS < fParameters.GetNstationsActive(); ++iS) {
        wData.TsHitIndices(iS).clear();
      }
      bool areUntouchedDataLeft = false;  // is the whole TS processed

      // TODO: SG: skip empty regions and start the subslice with the earliest hit

      statNwindows++;
      //out << statNwindows << ' ';

      monitor.StartTimer(ETimer::PrepareWindow);

      for (auto& range : streamHitRanges) {
        for (HitIndex_t caHitId = range.first; caHitId < range.second; ++caHitId) {
          const ca::Hit& h = input.GetHit(caHitId);
          if (wData.IsHitKeyUsed(h.FrontKey()) || wData.IsHitKeyUsed(h.BackKey())) {
            // the hit is already reconstructed
            continue;
          }
          const CaHitTimeInfo& info = fHitTimeInfo[caHitId];
          if (info.fEventTimeMax < windowRange.first) {
            // the hit belongs to previous sub-slices
            continue;
          }
          if (info.fMinTimeAfterHit > windowRange.first + fWindowLength) {
            // this hit and all later hits are out of the sub-slice
            areUntouchedDataLeft = true;
            break;
          }
          if (info.fEventTimeMin > windowRange.first + fWindowLength) {
            // the hit is too late for the sub slice
            areUntouchedDataLeft = true;
            continue;
          }

          // the hit belongs to the sub-slice
          wData.TsHitIndices(h.Station()).push_back(caHitId);
          if (info.fMaxTimeBeforeHit < windowRange.first + fWindowLength) {
            range.first = caHitId + 1;  // this hit and all hits before are before the overlap
          }
        }
      }

      //out << statNwindowHits << ' ';
      //if (statNwindowHits == 0) {  // Empty window
      //  monitor.StopTimer(ETimer::PrepareWindow);
      //  out << 0 << ' ' << 0 << ' ' << 0 << '\n';
      //  continue;
      //}

      if (ca::TrackingMode::kMcbm == fTrackingMode) {
        // cut at 50 hits per station per 1 us.
        int maxStationHits = (int) (50 * fWindowLength / 1.e3);
        for (int ista = 0; ista < fParameters.GetNstationsActive(); ++ista) {
          int nHitsSta = static_cast<int>(wData.TsHitIndices(ista).size());
          if (nHitsSta > maxStationHits) {
            wData.TsHitIndices(ista).clear();
          }
        }
      }

      int statNwindowHits = 0;
      for (int ista = 0; ista < fParameters.GetNstationsActive(); ++ista) {
        statNwindowHits += wData.TsHitIndices(ista).size();
      }
      statNhitsProcessed += statNwindowHits;

      // print the LOG for every 10 ms of data processed
      if constexpr (0) {
        int currentChunk = (int) ((windowRange.first - fStatTsStart) / 10.e6);
        if (!areUntouchedDataLeft || currentChunk > statLastLogTimeChunk) {
          statLastLogTimeChunk = currentChunk;
          double dataRead = 100. * (windowRange.first + fWindowLength - fStatTsStart) / (fStatTsEnd - fStatTsStart);
          if (dataRead > 100.) {
            dataRead = 100.;
          }
          LOG(debug) << "CA tracker process sliding window N " << statNwindows << ": time " << windowRange.first / 1.e6
                     << " ms + " << fWindowLength / 1.e3 << " us) with " << statNwindowHits << " hits. "
                     << " Processing " << dataRead << " % of the TS time and "
                     << 100. * statNhitsProcessed / fStatNhitsTotal << " % of TS hits."
                     << " Already reconstructed " << tracks.size() << " tracks on thread #" << iThread;
        }
      }

      //out << statNwindowHits << ' ';
      monitor.StopTimer(ETimer::PrepareWindow);

      //Timer trackingInWindow;  //DBG
      //trackingInWindow.Start();
      monitor.StartTimer(ETimer::TrackingWindow);
      trackFinderWindow.CaTrackFinderSlice(input, wData);
      monitor.StopTimer(ETimer::TrackingWindow);
      //trackingInWindow.Stop();
      //out << trackingInWindow.GetTotalMs() << ' ';

      // save reconstructed tracks with no hits in the overlap region
      //if (windowRange.first > 13.23e6 && windowRange.first < 13.26e6) {
      windowRange.first += fWindowLength;
      // we should add hits from reconstructed but not stored tracks to the new sub-timeslice
      // we do it in a simple way by extending the tsStartNew
      // TODO: only add those hits from the region before tsStartNew that belong to the not stored tracks
      //out << fvWData[iThread].RecoHitIndices().size() << ' ';
      //out << fvWData[iThread].RecoTracks().size() << '\n';

      monitor.StartTimer(ETimer::StoreTracksWindow);
      auto trackFirstHit = wData.RecoHitIndices().begin();

      for (const auto& track : wData.RecoTracks()) {

        const bool isTrackCompletelyInOverlap =
          std::all_of(trackFirstHit, trackFirstHit + track.fNofHits, [&](int caHitId) {
            CaHitTimeInfo& info = fHitTimeInfo[caHitId];
            return info.fEventTimeMax >= windowRange.first;
          });

        // Don't save tracks from the overlap region, since they might have additional hits in the next subslice.
        // Don't reject tracks in the overlap when no more data are left
        const bool useFlag = !isTrackCompletelyInOverlap || !areUntouchedDataLeft;

        for (int i = 0; i < track.fNofHits; i++) {
          const int caHitId                = *(trackFirstHit + i);
          const auto& h                    = input.GetHit(caHitId);
          wData.IsHitKeyUsed(h.FrontKey()) = static_cast<int>(useFlag);
          wData.IsHitKeyUsed(h.BackKey())  = static_cast<int>(useFlag);

          if (useFlag) {
            hitIndices.push_back(caHitId);
          }
        }
        if (useFlag) {
          tracks.push_back(track);
        }

        trackFirstHit += track.fNofHits;
      }  // sub-timeslice tracks
      monitor.StopTimer(ETimer::StoreTracksWindow);

      if (windowRange.first > windowRange.second) {
        break;
      }
      if (!areUntouchedDataLeft) {
        break;
      }
    }  // while(true)
    monitor.StopTimer(ETimer::TrackingThread);
    //timer.Stop();
    //LOG(info) << "CA: finishing tracking on thread " << iThread << " (time: " << timer.GetTotalMs() << " ms, "
    //          << "hits processed: " << statNhitsProcessed << ", "
    //          << "hits used: " << hitIndices.size() << ')';
  }
}  // namespace cbm::algo::ca