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

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

// Local
#include "TbClustering.h"

DECLARE_ALGORITHM_FACTORY(TbClustering)

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

  declareProperty("HitLocation", m_hitLocation = LHCb::TbHitLocation::Default);
  declareProperty("ClusterLocation",
                  m_clusterLocation = LHCb::TbClusterLocation::Default);
  declareProperty("TimeWindow", m_twindow = 100. * Gaudi::Units::ns);
  declareProperty("SearchDist", m_searchDist = 1);
  declareProperty("ClusterErrorMethod", m_clusterErrorMethod = 0);
}

//=============================================================================
// Initialization
//=============================================================================
StatusCode TbClustering::initialize() {

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

  m_eta.resize(m_nPlanes);
  for (const auto& filename : dataSvc()->getEtaConfig()) {
    info() << "Importing eta corrections from " << filename << endmsg; 
    readEta(filename);
  }
  
  return StatusCode::SUCCESS;
}

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

  // Get the hits to be clustered - loop over planes.
  for (unsigned int i = 0; i < m_nPlanes; ++i) {
    const std::string ext = std::to_string(i);
    const std::string hitLocation = m_hitLocation + ext;
    const LHCb::TbHits* hits = getIfExists<LHCb::TbHits>(hitLocation);
    if (!hits) return Error("No hits in " + hitLocation);
    // Create a cluster container and transfer its ownership to the TES.
    LHCb::TbClusters* clusters = new LHCb::TbClusters();
    put(clusters, m_clusterLocation + ext);
    // Skip masked planes.
    if (masked(i)) continue;
    // Keep track of which hits have been clustered.
    std::vector<bool> used(hits->size(), false);
    std::vector<const LHCb::TbHit*> pixels;
    pixels.reserve(100);
    // Cycle over the (time ordered) hits.
    for (LHCb::TbHits::const_iterator it = hits->begin(); it != hits->end();
         ++it) {
      // Skip hits which are already part of a cluster.
      if (used[(*it)->key()]) continue;
      // Start a cluster from this seed pixel and tag the seed as used.
      used[(*it)->key()] = true;
      pixels.clear();
      pixels.push_back(*it);
      // Get the search range.
      const double tMax = (*it)->htime() + m_twindow;
      LHCb::TbHits::const_iterator end = std::upper_bound(
          it, hits->end(), tMax, lowerBound<const LHCb::TbHit*>());
      // Add neighbouring hits in the cluster time window.
      addNeighbouringHits(pixels, it, end, used);
      // Sort the hits by time.
      std::sort(pixels.begin(), pixels.end(),
                TbFunctors::LessByTime<const LHCb::TbHit*>());
      // Finally, set the remaining cluster attributes according to its hits.
      completeCluster(i, pixels, clusters);
    }
    // Sort the clusters by time.
    std::sort(clusters->begin(), clusters->end(),
              TbFunctors::LessByTime<LHCb::TbCluster*>());
    // Fill the counter.
    counter("NumberOfClusters" + ext) += clusters->size();
  }

  return StatusCode::SUCCESS;
}

//=============================================================================
// Finding touching hits
//=============================================================================
void TbClustering::addNeighbouringHits(std::vector<const LHCb::TbHit*>& pixels,
                                       LHCb::TbHits::const_iterator begin,
                                       LHCb::TbHits::const_iterator end,
                                       std::vector<bool>& used) {

  // Keep track of the cluster's bounding box.
  int scolmin = pixels.front()->scol();
  int scolmax = scolmin;
  int rowmin = pixels.front()->row();
  int rowmax = rowmin;
  // Try adding hits to the cluster.
  bool hitAdded = true;
  while (hitAdded) {
    hitAdded = false;
    unsigned int nUnused = 0;
    for (LHCb::TbHits::const_iterator it = begin; it != end; ++it) {
      // Skip hits which are already part of a cluster.
      if (used[(*it)->key()]) continue;
      ++nUnused;
      const int scol = (*it)->scol();
      const int row = (*it)->row();
      if (scol < scolmin - m_searchDist) continue;
      if (scol > scolmax + m_searchDist) continue;
      if (row < rowmin - m_searchDist) continue;
      if (row > rowmax + m_searchDist) continue;
      // Ask if the hit is touching the cluster (with its current set of hits).
      if (hitTouchesCluster((*it)->scol(), (*it)->row(), pixels)) {
        // Add the hit to the cluster.
        pixels.push_back(*it);
        used[(*it)->key()] = true;
        --nUnused;
        hitAdded = true;
        // Update the bounding box.
        if (scol < scolmin)
          scolmin = scol;
        else if (scol > scolmax)
          scolmax = scol;
        if (row < rowmin)
          rowmin = row;
        else if (row > rowmax)
          rowmax = row;
      }
    }
    if (nUnused == 0) break;
  }
}

//=============================================================================
// Complete remaining cluster attributes
//=============================================================================
void TbClustering::completeCluster(
    const unsigned int plane, const std::vector<const LHCb::TbHit*>& pixels,
    LHCb::TbClusters* clusters) {

  // Create a new cluster object.
  LHCb::TbCluster* cluster = new LHCb::TbCluster();
  cluster->setPlane(plane);
  // Set cluster width along the column and row direction.
  auto cols = std::minmax_element(pixels.cbegin(), pixels.cend(),
                        [](const LHCb::TbHit* h1, const LHCb::TbHit* h2) {
    return h1->scol() < h2->scol();
  });
  auto rows = std::minmax_element(pixels.cbegin(), pixels.cend(),
                        [](const LHCb::TbHit* h1, const LHCb::TbHit* h2) {
    return h1->row() < h2->row(); 
  });
  const unsigned int nCols =
    1 + (*cols.second)->scol() - (*cols.first)->scol();
  const unsigned int nRows = 1 + (*rows.second)->row() - (*rows.first)->row();
  cluster->setCols(nCols);
  cluster->setRows(nRows);
  // Add the pixel hits to the cluster, sum up the charge and
  // calculate the centre of gravity.
  unsigned int tot = 0;
  double charge = 0.;
  double xLocal = 0.;
  double yLocal = 0.;
  for (auto it = pixels.cbegin(), end = pixels.cend(); it != end; ++it) {
    cluster->addToHits(*it);
    tot += (*it)->ToT();
    const double q = (*it)->charge();
    charge += q;
    double x = 0.;
    double y = 0.;
    geomSvc()->pixelToPoint((*it)->scol(), (*it)->row(), plane, x, y);
    xLocal += x * q;
    yLocal += y * q;
  }
  cluster->setToT(tot);
  cluster->setCharge(charge);
  // Assume that the cluster time is the time of the earliest hit.
  cluster->setTime(pixels.front()->time());
  cluster->setHtime(pixels.front()->htime());
  // Calculate the local and global coordinates.
  xLocal /= charge;
  yLocal /= charge;
  // Apply eta-corrections.
  etaCorrection(xLocal, yLocal, nCols, nRows, plane);
  
  cluster->setXloc(xLocal);
  cluster->setYloc(yLocal);
  const Gaudi::XYZPoint pLocal(xLocal, yLocal, 0.);
  const auto pGlobal = geomSvc()->localToGlobal(pLocal, plane);
  cluster->setX(pGlobal.x());
  cluster->setY(pGlobal.y());
  cluster->setZ(pGlobal.z());
  // Assign error estimates.
  setClusterError(cluster);
  // Add the cluster to the container.
  clusters->insert(cluster);
}

//=============================================================================
// Calculate the cluster error estimate.
//=============================================================================
void TbClustering::setClusterError(LHCb::TbCluster* cluster) const {

  // Initialise the errors to some default value (for large cluster widths).
  double dx = 0.1;
  double dy = 0.1;
  switch (m_clusterErrorMethod) {
    case 0: {
      constexpr std::array<double, 5> sigmas = {0.0027, 0.0035, 0.016, 0.045, 0.065};
      if (cluster->cols() < 6) dx = sigmas[cluster->cols() - 1];
      if (cluster->rows() < 6) dy = sigmas[cluster->rows() - 1];
      break;
    } 
    case 2:
      if (cluster->size() <= 6) {
        dx = dy = 0.004;
      } else {
        // HS: not sure this makes sense
        const double sigmaHit = (1. / sqrt(12.)) * Tb::PixelPitch;
        dx = sigmaHit * cluster->cols();
        dy = sigmaHit * cluster->rows();
      }
      break;
    default:
      dx = dy = 0.004;
      break;
  }
  cluster->setXErr(dx);
  cluster->setYErr(dy);
  cluster->setWx(1. / (dx * dx));
  cluster->setWy(1. / (dy * dy));
}


//=============================================================================
// Read the eta correction parameters from file.
//=============================================================================
void TbClustering::readEta(const std::string& filename) {

  TbCondFile f(filename);
  if (!f.is_open()) {
    warning() << "Cannot open " << filename << endmsg;
    return;
  }
  unsigned int indexPlane = 999;
  unsigned int indexXy = 0;
  unsigned int indexCol = 0;
  unsigned int nLines = 0;
  while (!f.eof()) {
    std::string line = "";
    ++nLines;
    if (!f.getLine(line)) continue;
    if (line.find("Plane") != std::string::npos) {
      indexPlane = 999;
      std::string key = "";
      std::string id = "";
      std::string xy = "";
      int cols = 0;
      if (!f.split(line, ' ', key, id, xy, cols)) {
        warning() << "Error reading line " << nLines << endmsg;
        continue;
      }
      // Find the index corresponding to the given plane name. 
      for (unsigned int i = 0; i < m_nPlanes; ++i) {
        if (id == geomSvc()->modules()[i]->id()) {
          indexPlane = i;
          break;
        }
      } 
      if (indexPlane == 999) {
        warning() << "Module " << id 
                  << " is not in the alignment file" << endmsg;
        continue;
      }
      // Check whether this set of parameters is for the x or the y direction.
      if (xy == "x" || xy == "X") {
        indexXy = 0;
      } else if (xy == "y" || xy == "Y") {
        indexXy = 1;
      } else {
        warning() << "Unexpected direction specifier " << xy 
                  << " (expected x or y). Skip this parameter set." << endmsg;
        indexPlane = 999;
        continue;
      }
      // Check the cluster width.
      if (cols < 2) {
        warning() << "Unexpected cluster width (" << cols
                  << "). Skip this parameter set." << endmsg;
        indexPlane = 999;
        continue; 
      }
      indexCol = cols - 2;
      if (m_eta[indexPlane][indexXy].size() < indexCol + 1) {
        m_eta[indexPlane][indexXy].resize(indexCol + 1);
      }
      const std::string rowcol = indexXy == 0 ? " columns." : " rows.";
      info() << "Reading eta-correction parameters for plane " << indexPlane
             << " for clusters covering " << cols << rowcol << endmsg;
      m_eta[indexPlane][indexXy][indexCol].clear();
    } else {
      if (indexPlane == 999) continue;
      // Read the intra-pixel interval and the coefficients of the polynomial.
      double xmin = 0., xmax = 0.;
      double a = 0., b = 0., c = 0.;
      if (!f.split(line, ' ', xmin, xmax, a, b, c)) {
        warning() << "Error reading line " << nLines << endmsg;
        continue;
      }
      EtaCorrection corr;
      corr.xmin = xmin;
      corr.xmax = xmax;
      corr.coefficients = {a, b, c};
      m_eta[indexPlane][indexXy][indexCol].push_back(corr);
    }
  }

}

//=============================================================================
// Calculate the eta correction terms.
//=============================================================================
void TbClustering::etaCorrection(double& xLocal, double& yLocal,
                                 const unsigned int nCols, 
                                 const unsigned int nRows, 
                                 const unsigned int plane) const { 

  // No point to continue in case of single-pixel clusters.
  if (nCols * nRows == 1) return;
  // Calculate the intra-pixel coordinates of the cluster.
  unsigned int scol = 0, row = 0;
  geomSvc()->pointToPixel(xLocal, yLocal, plane, scol, row);
  double x0 = 0., y0 = 0.;
  geomSvc()->pixelToPoint(scol, row, plane, x0, y0);
  // TODO: this only works for single-chip assemblies.
  const double x = xLocal - x0 + 0.5 * Tb::PixelPitch;
  const double y = yLocal - y0 + 0.5 * Tb::PixelPitch;
  // Calculate the correction terms.
  if (nCols > 1 && m_eta[plane][0].size() > nCols - 2) {
    const auto& corrections = m_eta[plane][0][nCols - 2];
    for (const auto& corr : corrections) {
      if (x >= corr.xmin && x < corr.xmax) {
        xLocal += corr.coefficients[0] + corr.coefficients[1] * x + 
                  corr.coefficients[2] * x * x;
        break;
      }
    }
  } 
  if (nRows > 1 && m_eta[plane][1].size() > nRows - 2) {
    const auto& corrections = m_eta[plane][1][nRows - 2];
    for (const auto& corr : corrections) {
      if (y >= corr.xmin && y < corr.xmax) {
        yLocal += corr.coefficients[0] + corr.coefficients[1] * y + 
                  corr.coefficients[2] * y * y;
        break;
      }
    }
  } 
}
