// -*- C++ -*-
//
// This file is part of YODA -- Yet more Objects for Data Analysis
// Copyright (C) 2008-2024 The YODA collaboration (see AUTHORS for details)
//
#ifndef YODA_Histo_h
#define YODA_Histo_h

#include "YODA/BinnedDbn.h"

namespace YODA {

  /// @brief Specialisation of the BinnedDbn for a 1D histogram
  template <typename AxisT>
  class BinnedDbn<1, AxisT>
      : public DbnStorage<1, AxisT>,
        public XAxisMixin<BinnedDbn<1, AxisT>, AxisT>,
        public XStatsMixin<BinnedDbn<1, AxisT>> {
  public:

    using HistoT = BinnedDbn<1, AxisT>;
    using BaseT = DbnStorage<1, AxisT>;
    using FillType = typename BaseT::FillType;
    using BinType = typename BaseT::BinT;
    using Ptr = std::shared_ptr<HistoT>;

    /// @brief Inherit constructors.
    using BaseT::BaseT;

    BinnedDbn() = default;
    BinnedDbn(const HistoT&) = default;
    BinnedDbn(HistoT&&) =default;
    BinnedDbn& operator =(const HistoT&) = default;
    BinnedDbn& operator =(HistoT&&) = default;
    using AnalysisObject::operator =;

    /// @brief Constructor with auto-setup of evenly spaced axes.
    ///
    /// The constructor argument uses double rather than EdgeT to
    /// allow for auto-conversion of int to double.
    ///
    /// @note This constructor is only supported when all axes are continuous.
    template <typename EdgeT = double, typename = enable_if_all_CAxisT<EdgeT, AxisT>>
    BinnedDbn(size_t nbins, double lower, double upper,
              const std::string& path = "", const std::string& title = "")
        : BaseT({nbins}, {{lower, upper}}, path, title) {}

    /// @brief Copy constructor (needed for clone functions).
    ///
    /// @note Compiler won't generate this constructor automatically.
    BinnedDbn(const BaseT& other) : BaseT(other) {}
    //
    BinnedDbn(const HistoT& other, const std::string& path) : BaseT(other, path) {}

    /// @brief Move constructor
    BinnedDbn(BaseT&& other) : BaseT(std::move(other)) {}
    //
    BinnedDbn(HistoT&& other, const std::string& path) : BaseT(std::move(other), path) {}

    /// @brief Make a copy on the stack
    HistoT clone() const noexcept {
      return HistoT(*this);
    }

    /// @brief Make a copy on the heap
    HistoT* newclone() const noexcept {
      return new HistoT(*this);
    }

    /// @brief Fill function with an explicit coordinate.
    virtual int fill(const AxisT val, const double weight = 1.0, const double fraction = 1.0) {
      return BaseT::fill({val}, weight, fraction);
    }

    /// @brief Fill function with FillType.
    virtual int fill(FillType&& coords, const double weight = 1.0, const double fraction = 1.0) {
      return BaseT::fill(std::move(coords), weight, fraction);
    }

    /// @brief Find bin index for given coordinates
    size_t indexAt(const AxisT xCoord) const noexcept {
      return BaseT::binAt( {xCoord} ).index();
    }

    /// @brief Mask/Unmask bin at given set of coordinates
    void maskBinAt(const AxisT xCoord, const bool status = true) noexcept {
      return BaseT::maskBin({xCoord}, status);
    }

  };





  /// @brief Specialisation of the BinnedDbn for a 2D histogram
  template <typename AxisT1, typename AxisT2>
  class BinnedDbn<2, AxisT1, AxisT2>
      : public DbnStorage<2, AxisT1, AxisT2>,
        public XAxisMixin<BinnedDbn<2, AxisT1, AxisT2>, AxisT1>,
        public XStatsMixin<BinnedDbn<2, AxisT1, AxisT2>>,
        public YAxisMixin<BinnedDbn<2, AxisT1, AxisT2>, AxisT2>,
        public YStatsMixin<BinnedDbn<2, AxisT1, AxisT2>> {
  public:

    using HistoT = BinnedDbn<2, AxisT1, AxisT2>;
    using BaseT = DbnStorage<2, AxisT1, AxisT2>;
    using FillType = typename BaseT::FillType;
    using BinType = typename BaseT::BinT;
    using Ptr = std::shared_ptr<HistoT>;

    /// @brief Inherit constructors.
    using BaseT::BaseT;

    BinnedDbn() = default;
    BinnedDbn(const HistoT&) = default;
    BinnedDbn(HistoT&&) = default;
    BinnedDbn& operator =(const HistoT&) = default;
    BinnedDbn& operator =(HistoT&&) = default;
    using AnalysisObject::operator =;

    /// @brief Constructor with auto-setup of evenly spaced axes.
    ///
    /// The constructor argument uses double rather than EdgeT to
    /// allow for auto-conversion of int to double.
    ///
    /// @note This constructor is only supported when all axes are continuous.
    template <typename EdgeT = double, typename = enable_if_all_CAxisT<EdgeT, AxisT1, AxisT2>>
    BinnedDbn(size_t nbinsX, double lowerX, double upperX,
              size_t nbinsY, double lowerY, double upperY,
              const std::string& path = "", const std::string& title = "")
        : BaseT({nbinsX, nbinsY}, {{lowerX, upperX}, {lowerY, upperY}}, path, title) {}

    /// @brief Copy constructor (needed for clone functions).
    ///
    /// @note Compiler won't generate this constructor automatically.
    BinnedDbn(const BaseT& other) : BaseT(other) {}
    //
    BinnedDbn(const HistoT& other, const std::string& path) : BaseT(other, path) {}

    /// @brief Move constructor
    BinnedDbn(BaseT&& other) : BaseT(std::move(other)) {}
    //
    BinnedDbn(HistoT&& other, const std::string& path) : BaseT(std::move(other), path) {}

    /// @brief Make a copy on the stack
    HistoT clone() const noexcept {
      return HistoT(*this);
    }

    /// @brief Make a copy on the heap
    HistoT* newclone() const noexcept {
      return new HistoT(*this);
    }

    /// @brief Fill function with two explicit coordinates.
    virtual int fill(const AxisT1 valX, const AxisT2 valY, const double weight = 1.0, const double fraction = 1.0) {
      return BaseT::fill({valX, valY}, weight, fraction);
    }

    /// @brief Fill function with FillType.
    virtual int fill(FillType&& coords, const double weight = 1.0, const double fraction = 1.0) {
      return BaseT::fill(std::move(coords), weight, fraction);
    }

    /// @brief Bin access using global index
    BinType& bin(const size_t index) noexcept {
      return BaseT::bin(index);
    }

    /// @brief Bin access using global index (const version)
    const BinType& bin(const size_t index) const noexcept {
      return BaseT::bin(index);
    }

    /// @brief Bin access using local indices
    BinType& bin(const size_t localX, const size_t localY) noexcept {
      return BaseT::bin( {localX, localY} );
    }

    /// @brief Bin access using local indices (const version)
    const BinType& bin(const size_t localX, const size_t localY) const noexcept {
      return BaseT::bin( {localX, localY} );
    }

    /// @brief Bin access using coordinates
    BinType& binAt(const AxisT1 xCoord, const AxisT2 yCoord) noexcept {
      return BaseT::binAt( {xCoord, yCoord} );
    }

    /// @brief Bin access using coordinates (const version)
    const BinType& binAt(const AxisT1 xCoord, const AxisT2 yCoord) const noexcept {
      return BaseT::binAt( {xCoord, yCoord} );
    }

    /// @brief Find bin index for given coordinates
    size_t indexAt(const AxisT1 xCoord, const AxisT2 yCoord) const noexcept {
      return BaseT::binAt( {xCoord, yCoord} ).index();
    }

    /// @brief Mask/Unmask bin at given set of coordinates
    void maskBinAt(const AxisT1 xCoord, const AxisT2 yCoord, const bool status = true) noexcept {
      return BaseT::maskBin({xCoord, yCoord}, status);
    }

  };




  /// @brief Specialisation of the BinnedDbn for a 3D histogram
  template <typename AxisT1, typename AxisT2, typename AxisT3>
  class BinnedDbn<3, AxisT1, AxisT2, AxisT3>
      : public DbnStorage<3, AxisT1, AxisT2, AxisT3>,
        public XAxisMixin<BinnedDbn<3, AxisT1, AxisT2, AxisT3>, AxisT1>,
        public XStatsMixin<BinnedDbn<3, AxisT1, AxisT2, AxisT3>>,
        public YAxisMixin<BinnedDbn<3, AxisT1, AxisT2, AxisT3>, AxisT2>,
        public YStatsMixin<BinnedDbn<3, AxisT1, AxisT2, AxisT3>>,
        public ZAxisMixin<BinnedDbn<3, AxisT1, AxisT2, AxisT3>, AxisT3>,
        public ZStatsMixin<BinnedDbn<3, AxisT1, AxisT2, AxisT3>> {
  public:

    using HistoT = BinnedDbn<3, AxisT1, AxisT2, AxisT3>;
    using BaseT = DbnStorage<3, AxisT1, AxisT2, AxisT3>;
    using FillType = typename BaseT::FillType;
    using BinType = typename BaseT::BinT;
    using Ptr = std::shared_ptr<HistoT>;

    /// @brief Inherit constructors.
    using BaseT::BaseT;

    BinnedDbn() = default;
    BinnedDbn(const HistoT&) = default;
    BinnedDbn(HistoT&&) = default;
    BinnedDbn& operator =(const HistoT&) = default;
    BinnedDbn& operator =(HistoT&&) = default;
    using AnalysisObject::operator =;

    /// @brief Constructor with auto-setup of evenly spaced axes.
    ///
    /// The constructor argument uses double rather than EdgeT to
    /// allow for auto-conversion of int to double.
    ///
    /// @note This constructor is only supported when all axes are continuous.
    template <typename EdgeT = double, typename = enable_if_all_CAxisT<EdgeT, AxisT1, AxisT2, AxisT3>>
    BinnedDbn(size_t nbinsX, double lowerX, double upperX,
              size_t nbinsY, double lowerY, double upperY,
              size_t nbinsZ, double lowerZ, double upperZ,
              const std::string& path = "", const std::string& title = "")
         : BaseT({nbinsX, nbinsY, nbinsZ},
                 {{lowerX, upperX}, {lowerY, upperY}, {lowerZ, upperZ}},
                 path, title) {}

    /// @brief Copy constructor (needed for clone functions).
    ///
    /// @note Compiler won't generate this constructor automatically.
    BinnedDbn(const BaseT& other) : BaseT(other) {}
    //
    BinnedDbn(const HistoT& other, const std::string& path) : BaseT(other, path) {}

    /// @brief Move constructor
    BinnedDbn(BaseT&& other) : BaseT(std::move(other)) {}
    //
    BinnedDbn(HistoT&& other, const std::string& path) : BaseT(std::move(other), path) {}

    /// @brief Make a copy on the stack
    HistoT clone() const noexcept {
      return HistoT(*this);
    }

    /// @brief Make a copy on the heap
    HistoT* newclone() const noexcept {
      return new HistoT(*this);
    }

    /// @brief Fill function with three explicit coordinates.
    virtual int fill(const AxisT1 valX, const AxisT2 valY, const AxisT3 valZ, const double weight = 1.0, const double fraction = 1.0) {
      return BaseT::fill({valX, valY, valZ}, weight, fraction);
    }

    /// @brief Fill function with FillType.
    virtual int fill(FillType&& coords, const double weight = 1.0, const double fraction = 1.0) {
      return BaseT::fill(std::move(coords), weight, fraction);
    }

    /// @brief Bin access using global index
    BinType& bin(const size_t index) noexcept {
      return BaseT::bin(index);
    }

    /// @brief Bin access using global index (const version)
    const BinType& bin(const size_t index) const noexcept {
      return BaseT::bin(index);
    }

    /// @brief Bin access using local indices
    BinType& bin(const size_t localX, const size_t localY, const size_t localZ) noexcept {
      return BaseT::bin( {localX, localY, localZ} );
    }

    /// @brief Bin access using local indices (const version)
    const BinType& bin(const size_t localX, const size_t localY, const size_t localZ) const noexcept {
      return BaseT::bin( {localX, localY, localZ} );
    }

    /// @brief Bin access using coordinates
    BinType& binAt(const AxisT1 xCoord, const AxisT2 yCoord, const AxisT3 zCoord) noexcept {
      return BaseT::binAt( {xCoord, yCoord, zCoord} );
    }

    /// @brief Bin access using coordinates (const version)
    const BinType& binAt(const AxisT1 xCoord, const AxisT2 yCoord, const AxisT3 zCoord) const noexcept {
      return BaseT::binAt( {xCoord, yCoord, zCoord} );
    }

    /// @brief Find bin index for given coordinates
    size_t indexAt(const AxisT1 xCoord, const AxisT2 yCoord, const AxisT3 zCoord) const noexcept {
      return BaseT::binAt( {xCoord, yCoord, zCoord} ).index();
    }

    /// @brief Mask/Unmask bin at given set of coordinates
    void maskBinAt(const AxisT1 xCoord, const AxisT2 yCoord, const AxisT3 zCoord, const bool status = true) noexcept {
      return BaseT::maskBin({xCoord, yCoord, zCoord}, status);
    }

  };


  /// Define dimension-specific short-hands (Cython sugar)
  template<typename A1>
  using BinnedHisto1D = BinnedHisto<A1>;

  template <typename A1, typename A2>
  using BinnedHisto2D = BinnedHisto<A1, A2>;

  template <typename A1, typename A2, typename A3>
  using BinnedHisto3D = BinnedHisto<A1, A2, A3>;

  /// Anonymous namespace to limit visibility
  namespace {
    template <class T>
    struct HistoMaker;

    template<size_t... Is>
    struct HistoMaker<std::index_sequence<Is...>> {
      using type = BinnedHisto< std::decay_t<decltype((void)Is, std::declval<double>())>... >;
    };
  }

  template<size_t N>
  using HistoND = typename HistoMaker<std::make_index_sequence<N>>::type;

  /// User-friendly familiar names (continuous axes only)
  using Histo1D = BinnedHisto<double>;
  using Histo2D = BinnedHisto<double,double>;
  using Histo3D = BinnedHisto<double,double,double>;

}

#endif
