// Gaudi
#include "GaudiKernel/PhysicalConstants.h"

// Tb/TbKernel
#include "TbKernel/TbFunctors.h"
#include "TbKernel/TbModule.h"

// Local
#include "TbSimpleTracking.h"

DECLARE_ALGORITHM_FACTORY(TbSimpleTracking)

//=============================================================================
// Standard constructor
//=============================================================================
TbSimpleTracking::TbSimpleTracking(const std::string& name,
                                   ISvcLocator* pSvcLocator)
    : TbAlgorithm(name, pSvcLocator),
      m_trackFit(nullptr) {

  declareProperty("ClusterLocation",
                  m_clusterLocation = LHCb::TbClusterLocation::Default);
  declareProperty("TrackLocation",
                  m_trackLocation = LHCb::TbTrackLocation::Default);
  declareProperty("TrackFitTool", m_trackFitTool = "TbTrackFit");
  declareProperty("TimeWindow", m_timeWindow = 10. * Gaudi::Units::ns);
  declareProperty("MinPlanes", m_nMinPlanes = 8);
  declareProperty("MaxDistance", m_maxDist = 0. * Gaudi::Units::mm);
  declareProperty("MaxOpeningAngle", m_maxAngle = 0.01);
  declareProperty("RecheckTrack", m_recheckTrack = true);
  declareProperty("RemoveOutliers", m_removeOutliers = true);
  declareProperty("ChargeCutLow", m_chargeCutLow = 0);
  declareProperty("MaxClusterSize", m_maxClusterSize = 10);
  declareProperty("MaxClusterWidth", m_maxClusterWidth = 3);
  declareProperty("MaxOccupancy", m_maxOccupancy = 8);
  declareProperty("DoOccupancyCut", m_doOccupancyCut = false);
  declareProperty("Monitoring", m_monitoring = false);
}

//=============================================================================
// Initialization
//=============================================================================
StatusCode TbSimpleTracking::initialize() {
  // Initialise the base class.
  StatusCode sc = TbAlgorithm::initialize();
  if (sc.isFailure()) return sc;

  m_clusters.resize(m_nPlanes, nullptr);
  // Setup the track fit tool.
  m_trackFit = tool<ITbTrackFit>(m_trackFitTool, "Fitter", this);
  if (!m_trackFit) return Error("Cannot retrieve track fit tool.");

  m_nMaxPlanes = 0;
  for (unsigned int i = 0; i < m_nPlanes; ++i) {
    if (!masked(i)) ++m_nMaxPlanes;
  }
  if (m_nMaxPlanes == 0) return Error("All planes are masked.");
  return StatusCode::SUCCESS;
}

//=============================================================================
// Main execution
//=============================================================================
StatusCode TbSimpleTracking::execute() {

  // Get the clusters on each plane.
  for (unsigned int i = 0; i < m_nPlanes; ++i) {
    if (masked(i)) continue;
    const std::string clusterLocation = m_clusterLocation + std::to_string(i);
    LHCb::TbClusters* clusters = getIfExists<LHCb::TbClusters>(clusterLocation);
    if (!clusters) {
      return Error("No clusters in " + LHCb::TbClusterLocation::Default);
    }
    m_clusters[i] = clusters;
  }

  if (m_doOccupancyCut) findHighOccupancies();
  unsigned int nKilledOccupancyCut = 0;
  // Check if there is already a track container.
  LHCb::TbTracks* tracks = getIfExists<LHCb::TbTracks>(m_trackLocation);
  if (!tracks) {
    // Create a track container and transfer its ownership to the TES.
    tracks = new LHCb::TbTracks();
    put(tracks, m_trackLocation);
  }
  LHCb::TbTrack* track = nullptr;
  const unsigned int lastPlane = m_nPlanes - 2;
  for (unsigned int i = 0; i < lastPlane; ++i) {
    if (masked(i)) continue;
    // Get the next valid plane.
    unsigned int j = i + 1;
    while (masked(j) && j <= lastPlane) ++j;
    if (j > lastPlane) break;
    LHCb::TbClusters::iterator first0 = m_clusters[i]->begin();
    LHCb::TbClusters::iterator first1 = m_clusters[j]->begin();
    LHCb::TbClusters::iterator end0 = m_clusters[i]->end();
    LHCb::TbClusters::iterator end1 = m_clusters[j]->end();
    for (LHCb::TbClusters::iterator it0 = first0; it0 != end0; ++it0) {
      // Skip clusters already assigned to tracks.
      if ((*it0)->associated()) continue;
      // Skip clusters with too low ToT.
      if ((*it0)->ToT() < m_chargeCutLow) continue;
      // Skip large clusters.
      if ((*it0)->size() > m_maxClusterSize) continue;
      if (((*it0)->cols() > m_maxClusterWidth) || 
          ((*it0)->rows() > m_maxClusterWidth)) continue;
      const double t0 = (*it0)->htime();
      const double tMin = t0 - m_timeWindow;
      const double tMax = t0 + m_timeWindow;
      // Loop over the hits in the second plane.
      for (LHCb::TbClusters::iterator it1 = first1; it1 != end1; ++it1) {
        const double t1 = (*it1)->htime();
        // Skip hits below the time limit.
        if (t1 < tMin) {
          // Update the search range.
          first1 = it1 + 1;
          continue;
        }
        // Stop search when above the time limit.
        if (t1 > tMax) break;
        // Skip clusters already assigned to tracks.
        if ((*it1)->associated()) continue;
        // Skip clusters with too low charge.
        if ((*it1)->ToT() < m_chargeCutLow) continue;
        // Skip large clusters.
        if ((*it1)->size() > m_maxClusterSize) continue;
        if (((*it1)->cols() > m_maxClusterWidth) || 
            ((*it1)->rows() > m_maxClusterWidth)) continue;
        if (!track)
          track = new LHCb::TbTrack();
        else
          track->clearClusters();

        track->addToClusters(*it0);
        track->addToClusters(*it1);
        if (!extendTrack(track, true)) continue;
        // Fit the track.
        m_trackFit->fit(track);
        if (m_recheckTrack) recheckTrack(track);
        // Check if there are enough measurements on the track.
        if (track->clusters().size() < m_nMinPlanes) continue;
        // Check if there are enough unused clusters on the track.
        /*
        unsigned int nUnused = 0;
        for (const LHCb::TbCluster* cluster : track->clusters()) {
          if (!cluster->associated()) ++nUnused;
        }
        if (nUnused < 5) continue;
        */
        if (!lowClusterOccupancy(track->htime())) {
          ++nKilledOccupancyCut;
          continue;
        }
        auto clusters = const_cast<SmartRefVector<LHCb::TbCluster>&>(track->clusters());
        // Sort the clusters by plane.
        std::sort(clusters.begin(), clusters.end(),
                  TbFunctors::LessByZ<const LHCb::TbCluster*>());
        // Tag the clusters as used.
        for (auto itc = clusters.begin(), end = clusters.end(); itc != end;
             ++itc) {
          (*itc)->setAssociated(true);
        }
        // Calculate the global timestamp and add the track to the container.
        track->setTime(timingSvc()->localToGlobal(track->htime()));
        tracks->insert(track);
        track = nullptr;
      }
    }
  }
  if (track) delete track;

  // Sort the tracks by time.
  std::sort(tracks->begin(), tracks->end(),
            TbFunctors::LessByTime<const LHCb::TbTrack*>());
  // Fill the counters.
  counter("Number of tracks") += tracks->size();
  counter("Number of tracks removed by occupancy cut") += nKilledOccupancyCut;
  appendTrackingEfficiencies();

  return StatusCode::SUCCESS;
}

//=============================================================================
// Extrapolate and try to add clusters to a given seed track
//=============================================================================
bool TbSimpleTracking::extendTrack(LHCb::TbTrack* track, const bool fwd) {

  unsigned int nAdded = 0;
  // Count planes without matching clusters.
  unsigned int nMissed = 0;
  const LHCb::TbCluster* c1 = track->clusters().front();
  const LHCb::TbCluster* c2 = track->clusters().back();
  if (!fwd) {
    c2 = c1;
    c1 = track->clusters().at(1);
  }
  double t = c2->htime();
  // Calculate the track slopes based on the first two clusters on the track.
  double td = 1. / (c2->z() - c1->z());
  double tx = (c2->x() - c1->x()) * td;
  double ty = (c2->y() - c1->y()) * td;
  unsigned int plane = c2->plane();
  if (fwd) {
    plane += 1;
  } else {
    plane -= 1;
  }
  while (plane < m_nPlanes) {
    // Calculate the extrapolated position on this plane. 
    const double dz = geomSvc()->modules().at(plane)->z() - c1->z();
    const double xPred = c1->x() + tx * dz;
    const double yPred = c1->y() + ty * dz;
    // Calculate the tolerance window.
    const double tol = fabs(dz * m_maxAngle) + m_maxDist;
    // Try adding clusters on this plane.
    const LHCb::TbCluster* c3 = bestCluster(plane, xPred, yPred, t, tol);
    if (c3) {
      // Add the cluster.
      track->addToClusters(c3);
      ++nAdded;
      // Reset the missed plane counter.
      nMissed = 0;
      // Update the pair of clusters to be used for extrapolating.
      if ((c3->cols() <= m_maxClusterWidth) && 
          (c3->rows() <= m_maxClusterWidth)) {
        c1 = c2;
        c2 = c3;
        // Update the track slopes.
        td = 1. / (c2->z() - c1->z());
        tx = (c2->x() - c1->x()) * td;
        ty = (c2->y() - c1->y()) * td;
      }
      // Update the predicted timestamp.
      t = c3->htime();
    } else {
      // No matching cluster.
      ++nMissed;
      if (nMissed > 1) break;
    }
    if (fwd) {
      ++plane;
    } else {
      if (plane == 0) break;
      --plane;
    }
  }
  return nAdded > 0;
}

//=============================================================================
// Get the closest cluster to a given point on the plane
//=============================================================================
const LHCb::TbCluster* TbSimpleTracking::bestCluster(const unsigned int plane, 
    const double xPred, const double yPred,
    const double tPred, const double tol) {
  if (masked(plane) || m_clusters[plane]->empty()) return nullptr;
  // Get the first cluster within the time window.
  const double tMin = tPred - m_timeWindow;
  LHCb::TbClusters::iterator end = m_clusters[plane]->end();
  LHCb::TbClusters::iterator it =
      std::lower_bound(m_clusters[plane]->begin(), end, tMin, lowerBound());
  if (it == end) return nullptr;
  const double tMax = tPred + m_timeWindow;
  LHCb::TbCluster* best = nullptr;
  double dtBest = m_timeWindow;
  for (; it != end; ++it) {
    const double t = (*it)->htime();
    if ((*it)->ToT() < m_chargeCutLow) continue;
    if ((*it)->size() > m_maxClusterSize) continue;
    // Stop searching when outside the time window.
    if (t > tMax) break;
    const double dt = fabs(t - tPred);
    if (m_monitoring) {
      const std::string title = "Plane" + std::to_string(plane);
      plot(t - tPred, "delT" + title, title, -100, 100, 200);
    }
    if (dt < dtBest) {
      // Check if the cluster is within the spatial tolerance window.
      const double dx = xPred - (*it)->x();
      const double dy = yPred - (*it)->y();
      if (m_monitoring) {
        const std::string title = "Plane " + std::to_string(plane); 
        plot(dx, "delXY/" + title, title, -3, 3, 200);
        plot(dy, "delXY/" + title, title, -3, 3, 200);
      }
      if (fabs(dx) > tol || fabs(dy) > tol) continue;
      // Update the best cluster.
      dtBest = dt;
      best = *it;
    }
  }
  return best;
}

//=============================================================================
// Remove outliers and try to find additional clusters on a track candidate.
//=============================================================================
void TbSimpleTracking::recheckTrack(LHCb::TbTrack* track) {

  const unsigned int nClusters = track->clusters().size();
  if (nClusters < 3 || nClusters >= m_nMaxPlanes) return;
  // Spatial window for adding clusters to the track.
  const double tol = 0.1;
  // Keep track of the planes which already have a cluster on the track.
  std::vector<bool> planeAssociated(m_nPlanes, false);
  // Get the straight-line fit parameters.
  const double x0 = track->firstState().x();
  const double y0 = track->firstState().y();
  const double tx = track->firstState().tx();
  const double ty = track->firstState().ty();
  auto clusters = track->clusters();
  const unsigned int nSizeBefore = track->size();
  for (auto it = clusters.begin(); it != clusters.end(); ) {
    auto cluster = *it;
    const unsigned int plane = cluster->plane();
    ++it;
    // Remove outliers if requested.
    const double dx = x0 + tx * cluster->z() - cluster->x();
    const double dy = y0 + ty * cluster->z() - cluster->y();
    if (m_removeOutliers && (fabs(dx) > tol || fabs(dy) > tol)) {
      track->removeFromClusters(cluster);
    } else {
      planeAssociated[plane] = true;
    }
  }
  const unsigned int nSizeAfter = track->size();
  plot(nSizeBefore - nSizeAfter, "NOutliersRemoved", "NOutliersRemoved", -0.5, 10.5, 11);
  for (unsigned int i = m_nPlanes; i-- > 0;) {
    if (masked(i) || planeAssociated[i]) continue;
    // Calculate the track intercept on the plane.
    const Gaudi::XYZPoint interceptG = geomSvc()->intercept(track, i);
    const LHCb::TbCluster* cluster = bestCluster(
        i, interceptG.x(), interceptG.y(), track->htime(), tol);
    if (cluster) {
      track->addToClusters(cluster);
      m_trackFit->fit(track);
    }
  }
}

//=============================================================================

void TbSimpleTracking::findHighOccupancies() {

  m_htimesHighOccupancy.clear();
  // Find the first non-masked plane.
  unsigned int seedPlane = 0;
  while (masked(seedPlane)) ++seedPlane;
  for (LHCb::TbClusters::const_iterator iSeed = m_clusters[seedPlane]->begin();
       iSeed != m_clusters[seedPlane]->end(); ++iSeed) {
    uint nClustersPerSeed = 0;
    for (unsigned int i = 0; i < m_nPlanes; ++i) {
      if (masked(i)) continue;
      for (LHCb::TbClusters::const_iterator it = m_clusters[i]->begin();
           it != m_clusters[i]->end(); ++it) {
        if (fabs((*iSeed)->htime() - (*it)->htime()) < m_timeWindow)
          nClustersPerSeed++;
      }
    }
    if (nClustersPerSeed > m_maxOccupancy)
      m_htimesHighOccupancy.push_back((*iSeed)->htime());
  }
}

//=============================================================================

bool TbSimpleTracking::lowClusterOccupancy(const double t) const {
  if (!m_doOccupancyCut) return true;
  for (unsigned int i = 0; i < m_htimesHighOccupancy.size(); ++i) {
    if (fabs(t - m_htimesHighOccupancy[i]) < m_timeWindow) return false;
  }
  return true;
}

//=============================================================================

void TbSimpleTracking::appendTrackingEfficiencies() {
  for (unsigned int i = 0; i < m_nPlanes; ++i) {
    if (masked(i)) continue;
    for (LHCb::TbClusters::const_iterator it = m_clusters[i]->begin();
         it != m_clusters[i]->end(); ++it) {
      counter("nClusters")++;
      if ((*it)->size() <= m_maxClusterSize &&
          (*it)->ToT() >= m_chargeCutLow &&
          lowClusterOccupancy((*it)->htime())) {
        counter("nClusters selected for tracking")++;
        if ((*it)->associated()) counter("nClusters associated")++;
      }
    }
  }
}

//=============================================================================
