/* Copyright (C) 2021-2023 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
   SPDX-License-Identifier: GPL-3.0-only
   Authors: Sergey Gorbunov, Sergei Zharko [committer] */

/// \file   CaInitManager.h
/// \brief  Input data management class for the CA tracking algorithm (header)
/// \since  24.12.2021
/// \author Sergei Zharko <s.zharko@gsi.de>

#pragma once  // include this header only once per compilation unit

#include "CaDefs.h"
#include "CaIteration.h"
#include "CaObjectInitController.h"
#include "CaParameters.h"
#include "CaSimd.h"
#include "CaStationInitializer.h"
#include "KfFieldRegion.h"

#include <array>
#include <bitset>
#include <memory>  //unique_ptr
#include <numeric>
#include <set>
#include <string>
#include <type_traits>
#include <unordered_map>

namespace cbm::algo::ca
{
  /// \enum cbm::algo::ca::EDetectorID
  /// \brief Forward declaration of the tracking detectors scoped enumeration
  ///
  /// Concrete realization of this enumeration must be determined in the concrete setup class (i.e. CbmL1)
  enum class EDetectorID;
}  // namespace cbm::algo::ca

/// \brief Hash function definition for EDetectorID
template<>
struct std::hash<cbm::algo::ca::EDetectorID> {
  int operator()(cbm::algo::ca::EDetectorID t) const { return static_cast<int>(t); }
};

namespace cbm::algo::ca
{
  /// \brief Underlying integer type for the DetectorID
  using DetectorID_t = std::underlying_type_t<EDetectorID>;

  /// \class cbm::algo::ca::InitManager
  /// \brief A CA Parameters object initialization class
  ///
  /// This class provides an interface to form a solid Parameters object, which is used by the CA algorithm and
  /// the related routines. The initialization can be performed either from the detector-specific interfaces or by
  /// reading the already prepared binary wile with extention ca.par TODO:.... continue
  ///
  class InitManager {
   private:
    /// \brief Init-controller key set
    enum class EInitKey
    {
      // NOTE: Please, keep the numbers of the enumeration items in the existing order: it helps to debug the
      //       initialization with this->GetObjectInitController().ToString() method call (S.Zharko)
      kFieldFunction,                 ///< 0) If magnetic field getter function is set
      kTargetPos,                     ///< 1) If target position was defined
      kPrimaryVertexField,            ///< 2) If magnetic field value and region defined at primary vertex
      kStationsInfo,                  ///< 3) If all the planned stations were added to the manager
      kCAIterationsNumberCrosscheck,  ///< 4) If the number of CA track finder is initialized
      kCAIterations,                  ///< 5) If the CA track finder iterations were initialized
      kSearchWindows,                 ///< 6) If the hit search windows were initialized
      kGhostSuppression,              ///< 7)
      kRandomSeed,                    ///< 8) If the random seed is provided
      kStationLayoutInitialized,      ///< 9) If stations layout is initialized
      kSetupInitialized,              ///< 10) If KF-setup initialized
      kEnd                            ///< 11) [technical] number of entries in the enumeration
    };

    using DetectorIDIntMap_t = std::unordered_map<EDetectorID, int>;
    using DetectorIDSet_t    = std::set<EDetectorID>;
    using FieldFunction_t    = std::function<void(const double (&xyz)[3], double (&B)[3])>;
    using InitController_t   = ObjectInitController<static_cast<int>(EInitKey::kEnd), EInitKey>;
    template<typename T>
    using DetectorIDArr_t = std::array<T, constants::size::MaxNdetectors>;

   public:
    /// \brief Default constructor
    InitManager() = default;

    /// \brief Destructor
    ~InitManager() = default;

    /// \brief Copy constructor is forbidden
    InitManager(const InitManager& /*other*/) = delete;

    /// \brief Move constructor is forbidden
    InitManager(InitManager&& /*other*/) = delete;

    /// \brief Copy assignment operator is forbidden
    InitManager& operator=(const InitManager& /*other*/) = delete;

    /// \brief Move assignment operator is forbidden
    InitManager& operator=(InitManager&& /*other*/) = delete;

    /// \brief Adds a tracking station to the geometry
    ///
    /// \note The added station stays uninitialized until the Parameters object is formed
    void AddStation(const StationInitializer& station);

    /// \brief Adds tracking stations from the parameters file
    /// \param par  A valid instance of parameters, created for ACTIVE + INACTIVE tracking stations
    ///
    // TODO: Probably, we have to remove stations from the parameters and place them into a class ca::Geometery
    //       together with instances of active and geometry kf::Setup. ca::Parameters should contain only tracking
    //       parameters, which should be fully defined from the config. Search windows should be stored as an
    //       independent object, which depends on the ca::Parameters(iterations) and ca::Geometry(active station
    //       layout).
    void AddStations(const Parameters<fvec>& par);

    /// \brief Provides final checks of the parameters object
    void CheckInit();

    /// \brief Clears vector of CA track finder iterations
    void ClearCAIterations();

    /// \brief Clears vector of base setup
    void ClearSetupInfo();

    /// \brief  Forms parameters container
    /// \return Success flag: true - the container is formed, false - error while forming the container occured
    bool FormParametersContainer();

    /// \brief  Gets name of the detector
    /// \param  detId Index of the detector
    /// \return Name of the detector
    const std::string& GetDetectorName(EDetectorID detId) const { return fvDetectorNames[static_cast<int>(detId)]; }

    /// \brief Gets ghost suppression flag
    int GetGhostSuppression() const { return fParameters.fGhostSuppression; }

    /// \brief Gets a name of the main input configuration file
    const std::string& GetInputConfigMain() const { return fsConfigInputMain; }

    /// \brief Gets a name of the user input configuration file
    const std::string& GetInputConfigUser() const { return fsConfigInputMain; }

    /// \brief Gets a const reference to ca::ObjectInitController
    const InitController_t& GetInitController() const { return fInitController; }

    /// \brief Gets total number of active stations
    int GetNstationsActive() const;

    /// \brief Gets number of active stations for given detector ID
    int GetNstationsActive(EDetectorID detectorID) const;

    /// \brief Gets total number of stations, provided by setup geometry
    int GetNstationsGeometry() const;

    /// \brief Gets number of stations, provided by setup geometry for given detector ID
    int GetNstationsGeometry(EDetectorID detectorID) const;

    /// \brief Gets a name of the output configuration file
    const std::string& GetOutputConfigName() const { return fConfigOutputName; }

    /// \brief Gets a reference to the stations array
    std::vector<StationInitializer>& GetStationInfo();

    /// \brief Initializes station layout
    ///
    /// This function is to be called after all the tracking stations (StationInitializer objects) are added to the
    /// InitManager instance. After the initialization the vector of the tracking stations is sorted by z-positions
    /// and is available for modifications.
    void InitStationLayout();

    /// \brief Calculates kf::FieldValue and L1FieldReference values for a selected step in z-axis from the target position
    /// \param zStep step between nodal points
    void InitTargetField(double zStep);

    /// \brief Checks, if the detector is active
    bool IsActive(EDetectorID detectorID) const { return GetNstationsActive(detectorID) != 0; }

    /// \brief Checks, if the detector is present in the geometry
    bool IsPresent(EDetectorID detectorID) const { return GetNstationsGeometry(detectorID) != 0; }

    /// \brief Pushes an CA track finder iteration into a sequence of iteration using reference
    void PushBackCAIteration(const Iteration& iteration);

    /// \brief Pushes an CA track finder iteration into a sequence of iteration using raw pointer
    void PushBackCAIteration(const Iteration* pIteration) { PushBackCAIteration(*pIteration); }

    /// \brief Pushes an CA track finder iteration into a sequence of iteration using std::unique_ptr
    void PushBackCAIteration(const std::unique_ptr<Iteration>& puIteration) { PushBackCAIteration(*puIteration); }

    /// \brief Reads main and user parameters configs
    void ReadInputConfigs();

    /// \brief Reads geometry setup from file
    /// \param fileName  Name of input file
    void ReadGeometrySetup(const std::string& fileName);

    /// \brief Reads parameters object from boost-serialized binary file
    /// \param  fileName  Name of input file
    void ReadParametersObject(const std::string& fileName);

    /// \brief Reads search windows from file
    /// \param  fileName  Name of input file
    void ReadSearchWindows(const std::string& fileName);

    /// \brief Sets a number of CA track finder iterations to provide initialization cross-check
    // TODO: remove this method
    void SetCAIterationsNumberCrosscheck(int nIterations);

    /// \brief Sets base configuration file
    /// \param mainConfig  Path to main configuration file
    /// \note  The base configuraiton file is mandatory until the tracking configuration is initialized from
    ///        beforehand created Parameters file.
    void SetConfigMain(const std::string& mainConfig) { fsConfigInputMain = mainConfig; }

    /// \brief Sets user configuration file
    /// \param userConfig  Path to user configuration file
    /// \note  The user configuraiton file is optional
    void SetConfigUser(const std::string& userConfig) { fsConfigInputUser = userConfig; }

    /// \brief Sets detector names
    /// \param container  Container of the detector names
    template<size_t Size>
    void SetDetectorNames(const std::array<const char*, Size>& container)
    {
      static_assert(Size <= constants::size::MaxNdetectors,
                    "Please, be ensured that the constants::size::MaxNdetectors is not lower then the "
                    "EDetectorID::kEND value, provided by your setup");
      std::copy(container.begin(), container.end(), fvDetectorNames.begin());
    }

    /// Sets a magnetic field function, which will be applied for all the stations
    void SetFieldFunction(const FieldFunction_t& fieldFcn);

    /// \brief Sets the flag to enable/disable the ghost suppression routine
    void SetGhostSuppression(int ghostSuppression);

    /// \brief Sets a name of the output configuration file
    /// \param  filename  Name of the output CA parameters configuration
    ///
    /// The output file is created from the fields, saved in the resulted Parameters object.
    void SetOutputConfigName(const std::string& filename) { fConfigOutputName = filename; }

    /// \brief Sets pseudo-random numbers generator seed
    /// \param seed  Seed value
    /// \note  The default seed is 1
    void SetRandomSeed(unsigned int seed);

    // TODO: Use kf::Setup instead
    /// \brief Sets target position
    /// \param  x  Position X component [cm]
    /// \param  y  Position Y component [cm]
    /// \param  z  Position Z component [cm]
    void SetTargetPosition(double x, double y, double z);

    /// \brief Sets upper-bound cut on max number of doublets per one singlet
    void SetMaxDoubletsPerSinglet(unsigned int value) { fParameters.fMaxDoubletsPerSinglet = value; }

    /// \brief Sets upper-bound cut on max number of triplets per one doublet
    void SetMaxTripletPerDoublets(unsigned int value) { fParameters.fMaxTripletPerDoublets = value; }

    /// \brief Sets setup
    /// \tparam  Underlying type of the setup
    template<typename DataT>
    void SetGeometrySetup(const cbm::algo::kf::Setup<DataT>& setup)
    {
      if (!fInitController.GetFlag(EInitKey::kStationLayoutInitialized)) {
        std::stringstream msg;
        msg << "ca::InitManager: setup cannot be set until the station layout is initialized";
        throw std::runtime_error(msg.str());
      }
      fParameters.fGeometrySetup = kf::Setup<fvec>(setup);
      fParameters.fActiveSetup   = fParameters.fGeometrySetup;
      // A sequence of the last inactive materials will be anyway thrown away, so it is more effective to
      // loop over stations downstream
      for (int iStGeo = setup.GetNofLayers() - 1; iStGeo >= 0; --iStGeo) {
        auto [detID, locID] = fParameters.GetStationIndexLocal(iStGeo);
        int iStActive       = fParameters.GetStationIndexActive(locID, detID);
        if (iStActive < 0) {
          fParameters.fActiveSetup.DisableLayer(detID, locID);
        }
      }
      LOG(info) << "Geometry setup:" << fParameters.fGeometrySetup.ToString(1);
      LOG(info) << "Active setup:" << fParameters.fActiveSetup.ToString(1);
      fInitController.SetFlag(EInitKey::kSetupInitialized, true);
    }

    /// \brief Sets misalignment parameters in X direction
    /// \param  detectorId  Index of the detector system
    /// \param  x  Misalignment tolerance in x [cm]
    /// \param  y  Misalignment tolerance in y [cm]
    /// \param  t  Misalignment tolerance in t [ns]
    void SetMisalignmentTolerance(EDetectorID detectorId, double x, double y, double t)
    {
      fParameters.fMisalignmentX[static_cast<int>(detectorId)] = x;
      fParameters.fMisalignmentY[static_cast<int>(detectorId)] = y;
      fParameters.fMisalignmentT[static_cast<int>(detectorId)] = t;
    }

    /// \brief Sets default fitter mass
    /// \param mass Particle mass [GeV/c2]
    void SetDefaultMass(double mass) { fParameters.fDefaultMass = mass; }

    /// \brief  Takes parameters object from the init-manager instance
    /// \return A parameter object
    Parameters<fvec>&& TakeParameters();

    /// \brief Writes parameters object from boost-serialized binary file
    /// \param  fileName  Name of input file
    void WriteParametersObject(const std::string& fileName) const;

    // ***************************
    // ** Flags for development **
    // ***************************

    /// \brief Ignore hit search areas
    void DevSetIgnoreHitSearchAreas(bool value = true) { fParameters.fDevIsIgnoreHitSearchAreas = value; }

    /// \brief Force use of the original field (not approximated)
    void DevSetUseOfOriginalField(bool value = true) { fParameters.fDevIsUseOfOriginalField = value; }

    /// \brief Flag to match doublets using MC information
    void DevSetIsMatchDoubletsViaMc(bool value = true) { fParameters.fDevIsMatchDoubletsViaMc = value; }

    /// \brief Flag to match triplets using Mc information
    void DevSetIsMatchTripletsViaMc(bool value = true) { fParameters.fDevIsMatchTripletsViaMc = value; }

    /// \brief Flag to match triplets using Mc information
    void DevSetIsExtendTracksViaMc(bool value = true) { fParameters.fDevIsExtendTracksViaMc = value; }

    /// \brief Flag to match triplets using Mc information
    void DevSetIsSuppressOverlapHitsViaMc(bool value = true) { fParameters.fDevIsSuppressOverlapHitsViaMc = value; }

    /// \brief Flag to use estimated hit search windows
    /// \param true   estimated search windows will be used in track finder
    /// \param false  the Kalman filter is be used in track finder
    void DevSetIsParSearchWUsed(bool value = true) { fParameters.fDevIsParSearchWUsed = value; }

   private:
    /// \brief Checker for Iteration container initialization (sets EInitKey::kCAIterations)
    /// \return true If all Iteration objects were initialized properly
    void CheckCAIterationsInit();

    /// \brief Checker for StationInitializer set initialization (sets EInitKey::kStationsInfo)
    /// \return true If all StationInitializer objects were initialized properly. Similar effect can be achieved by
    void CheckStationsInfoInit();

    /// \brief Returns station layout into undefined condition
    void ClearStationLayout();

    InitController_t fInitController{};              ///< Initialization flags
    DetectorIDArr_t<std::string> fvDetectorNames{};  ///< Names of the detectors

    double fTargetZ{0.};  ///< Target position z component in double precision

    std::vector<StationInitializer> fvStationInfo{};  ///< Vector of StationInitializer objects (active + inactive)

    /// A function which returns magnetic field vector B in a radius-vector xyz
    FieldFunction_t fFieldFunction{[](const double (&)[3], double (&)[3]) {}};
    // NOTE: Stations of the detectors which are not assigned as active, are not included in the tracking!

    // TODO: remove
    int fCAIterationsNumberCrosscheck{-1};  ///< Number of iterations to be passed (must be used for cross-checks)

    Parameters<fvec> fParameters{};  ///< CA parameters object
    // TODO: With a separate KF-framework instance we need to figure it out, how to store and read the corresponding
    //       parameters (essential for the online reconstruction!!!)

    std::string fsConfigInputMain = "";  ///< name for the input configuration file
    std::string fsConfigInputUser = "";  ///< name for the input configuration file
    std::string fConfigOutputName = "";  ///< name for the output configuration file

    bool fbConfigIsRead       = false;  ///< Flag, if configuration file was read
    bool fbGeometryConfigLock = false;  ///< Lock geometry initialization
  };


}  // namespace cbm::algo::ca