// -*- 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_Bin_h
#define YODA_Bin_h

#include "YODA/Utils/BinUtils.h"
#include "YODA/Utils/Traits.h"
#include <iostream>

namespace YODA {

  /// @brief Arithmetic wrapper to emulate inheritance from arithmetic types
  template <typename T>
  struct ArithmeticWrapper {
    /// @name Constructors
    // {
    ArithmeticWrapper() : _storedNumber(0) {}

    ArithmeticWrapper(T num) : _storedNumber(num) {}
    // }

    /// @name Arithmetic operators
    // {

    ArithmeticWrapper<T>& operator+=(T&& rhs) {
      _storedNumber += std::forward<T>(rhs);
      return *this;
    }
    ArithmeticWrapper<T>& operator-=(T&& rhs) {
      _storedNumber -= std::forward<T>(rhs);
      return *this;
    }
    ArithmeticWrapper<T>& operator/=(T&& rhs) {
      _storedNumber /= std::forward<T>(rhs);
      return *this;
    }
    ArithmeticWrapper<T>& operator*=(T&& rhs) {
      _storedNumber *= std::forward<T>(rhs);
      return *this;
    }

    /// @note Template to make enable_if work.
    template<typename RetT = ArithmeticWrapper<T>>
    auto operator%=(T&& rhs)
    -> std::enable_if_t<std::is_integral<T>::value, RetT&> {
      _storedNumber %= std::forward<T>(rhs);
      return *this;
    }
    // }

    /// @brief Casts ArithmeticWrapper to the contained type.
    ///
    /// @note Used in arithmetic operators, so the default
    /// language arithmetic operators are used.
    operator T() { return _storedNumber; }

    /// @brief Casts ArithmeticWrapper to the contained type (const version).
    ///
    /// @note Used in raw() function of BinBase.
    operator const T&() const { return _storedNumber; }

    T _storedNumber;
  };

  /// @brief Bin base class consisting of mix of histogram bin content and
  /// space characteristics of this bin (widths, min, max, mid, etc...)
  ///
  /// @note Since the BinBase class inherits from the content of the
  /// histogram bin (to emulate its behavior), an arithmetic wrapper is
  /// introduced to inherit from arithmetic types (it's not possible to
  /// inherit from fundamental types by default).
  ///
  /// @note We use CRTP to introduce dimension-specific member functions
  /// like xWidth(), yMid() to avoid the user having to call
  /// e.g. bin(i).width<N>() or similar.
  template <typename T, typename BinningT>
  class BinBase : public std::conditional_t<std::is_arithmetic<T>::value,
                                            ArithmeticWrapper<T>, T> {
  protected:

    /// @name Utilities
    // {
    using isArithmetic = std::conditional_t<std::is_arithmetic<T>::value,
                                            std::true_type, std::false_type>;

    using BaseT = std::conditional_t<isArithmetic::value,
                                     ArithmeticWrapper<T>, T>;

    template <size_t axisNum>
    using axisEdgeT = typename BinningT::template getAxisT<axisNum>::EdgeT;

    // }

  public:
    /// @name Constructors
    // {

    // For self-consistency, the user should
    // always pass the parent binning!
    BinBase() = delete;

    // Default copy constructor for STL vector
    BinBase(const BinBase& rhs) = default;

    // Default move constructor for STL vector
    BinBase(BinBase&& rhs) = default;


    BinBase(size_t binIndex, const BinningT& binning)
        : _binIndex(binIndex), _binning(&binning) { }

    /// @brief Setting constructor
    BinBase(const T& storedVal, size_t binIndex, const BinningT& binning)
        : BaseT(storedVal), _binIndex(binIndex), _binning(&binning) { }

    BinBase(T&& storedVal, size_t binIndex, const BinningT& binning)
        : BaseT(std::move(storedVal)), _binIndex(binIndex), _binning(&binning) { }

    BinBase(const BinBase& other, const BinningT& binning)
        : BaseT(other), _binIndex(other._binIndex), _binning(&binning) { }

    /// @brief Assignment operator of an rvalue content type
    ///
    /// @note Cython is not a fan of perfect forwarding yet
    BinBase& operator=(BaseT&& rhs) noexcept {
      //BaseT::operator=(std::forward<BaseT>(rhs));
      BaseT::operator=(std::move(rhs));
      return *this;
    }

    /// @brief Assignment operator of a content type
    BinBase& operator=(const BaseT& rhs) noexcept {
      BaseT::operator=(rhs);
      return *this;
    }

    /// @brief Copy assignment operator
    ///
    /// @note _binIdx is not altered to keep correct indices while using
    /// std::vector<...>().erase() on _bins (bins storage).
    BinBase& operator=(const BinBase& rhs) noexcept {
      if (this != &rhs) {
        BaseT::operator=(rhs);
      }
      return *this;
    }

    /// @brief Move assignment operator
    ///
    /// @note _binIdx is not altered to keep correct indices while using
    /// std::vector<...>().erase() on _bins (bins storage).
    BinBase& operator=(BinBase&& rhs) noexcept {
      if (this != &rhs) {
        BaseT::operator=(std::move(rhs));
      }
      return *this;
    }

    // @}

    /// @name Utility methods
    // @{

    /// @brief return stored content
    const T& raw() const noexcept {
      return *this;
    }

    /// @brief return stored index
    size_t index() const noexcept {
      return _binIndex;
    }

    bool isMasked() const noexcept {
      return _binning->isMasked(_binIndex);
    }

    bool isVisible() const noexcept {
      return _binning->isVisible(_binIndex);
    }

    // @}

    /// @name Bin space characteristics
    // @{

    /// @brief Differential volume of this bin (i.e. product of bin widths)
    double dVol() const noexcept {
      return _binning->dVol(_binIndex);
    }

    /// @brief Width of this bin along a specific axis.
    ///
    /// @note Bin width is the projection of the
    /// bin surface (its area) along a specific axis.
    /// Therefore, "bin.width()" will not compile:
    /// it's not an intrinsic property of the bin.
    /// One should always specify *along which axis*.
    /// Convenient short-hands like bin.xWidth() etc.
    /// are being defined via CRTP Mixin, though.
    ///
    /// @note Only supported for continuous axes.
    template <size_t dimNum>
    enable_if_CAxisT<axisEdgeT<dimNum>> width() const noexcept {
      const auto& axis = _binning->template axis<dimNum>();
      size_t binIdx = _binning->globalToLocalIndices(_binIndex)[dimNum];
      return axis.width(binIdx);
    }

    /// @brief Maximum of this bin interval.
    ///
    /// @note Only supported for continuous axes.
    template <size_t dimNum>
    enable_if_CAxisT<axisEdgeT<dimNum>> max() const noexcept {
      const auto& axis = _binning->template axis<dimNum>();
      size_t binIdx = _binning->globalToLocalIndices(_binIndex)[dimNum];
      return axis.max(binIdx);
    }

    /// @brief Minimum of this bin interval.
    ///
    /// @note Only supported for continuous axes.
    template <size_t dimNum>
    enable_if_CAxisT<axisEdgeT<dimNum>> min() const noexcept {
      const auto& axis = _binning->template axis<dimNum>();
      size_t binIdx = _binning->globalToLocalIndices(_binIndex)[dimNum];
      return axis.min(binIdx);
    }

    /// @brief Middle of this bin interval.
    ///
    /// @note Only supported for continuous axes.
    template <size_t dimNum>
    enable_if_CAxisT<axisEdgeT<dimNum>> mid() const noexcept {
      const auto& axis = _binning->template axis<dimNum>();
      size_t binIdx = _binning->globalToLocalIndices(_binIndex)[dimNum];
      return axis.mid(binIdx);
    }

    /// @brief Edge of this bin.
    ///
    /// @note Only supported for discrete axes.
    template <size_t dimNum>
    enable_if_DAxisT<axisEdgeT<dimNum>> edge() const noexcept {
      const auto& axis = _binning->template axis<dimNum>();
      size_t binIdx = _binning->globalToLocalIndices(_binIndex)[dimNum];
      return axis.edge(binIdx);
    }

    // @}

  protected:

    const size_t _binIndex;

    const BinningT* _binning;
  };


  /// @brief generic Bin version that derives from BinBase
  template <size_t N, typename T, typename BinningT>
  class Bin : public BinBase<T, BinningT> {

  protected:

    using BaseT = BinBase<T, BinningT>;

  public:

    using BaseT::BaseT;
    using BaseT::operator=;

    // For self-consistency, the user should
    // always pass the parent binning!
    Bin() = delete;

    /* This would have been a nice idea, except we cannot guarantee
      that the content type even has the concept of a value.
    double value(const bool divbyvol = false) const noexcept {
      const double scale = divbyvol? BaseT::dVol() : 1.0;
      if constexpr (hasFillDim<T>::value) {
        if constexpr (T::FillDim::value > N) {
          return BaseT::mean(T::FillDim::value) / scale;// profiles
        }
        return BaseT::sumW() / scale; // histograms
      }
      return BaseT::value() / scale; // other
    }

    /// @brief Error on the value of the bin
    double valueErr(const bool divbyvol = false) const noexcept {
      const double scale = divbyvol? BaseT::dVol() : 1.0;
      if constexpr (hasFillDim<T>::value) {
        if constexpr (T::FillDim::value > N) { // profiles
          return BaseT::stdErr(T::FillDim::value) / scale;
        }
        return BaseT::errW() / scale; // histograms
      }
      return BaseT::valueErr() / scale; // other
    }

    /// @brief Relative size of the bin error
    double relErr() const noexcept {
      return value()? valueErr() / value() : std::numeric_limits<double>::quiet_NaN();
    }

    /// @brief Density of the bin
    double density() const noexcept {
      return value() / BaseT::dVol();
    }

    /// @brief Error on the bin density
    double densityErr() const noexcept {
      return valueErr() / BaseT::dVol();
    }*/

  };


  /// @brief CRTP specialisation in 1D
  template<typename T, typename BinningT>
  class Bin<1, T, BinningT>
               : public BinBase<T, BinningT>,
                 public XBinMixin<Bin<1, T, BinningT>,
                                  typename BinningT::template getEdgeT<0>> {
  protected:

    using BaseT = BinBase<T, BinningT>;

    template <size_t axisNum>
    using axisEdgeT = typename BinningT::template getAxisT<axisNum>::EdgeT;

  public:

    using BaseT::BaseT;
    using BaseT::operator=;

    // For self-consistency, the user should
    // always pass the parent binning!
    Bin() = delete;

    /// @brief Differential length of this bin (i.e. the bin width)
    double dLen() const noexcept { return BaseT::dVol(); }

  };


  /// @brief CRTP specialisation in 2D
  template<typename T, typename BinningT>
  class Bin<2, T, BinningT>
               : public BinBase<T, BinningT>,
                 public XBinMixin<Bin<2, T, BinningT>,
                                  typename BinningT::template getEdgeT<0>>,
                 public YBinMixin<Bin<2, T, BinningT>,
                                  typename BinningT::template getEdgeT<1>> {

  protected:

    using BaseT = BinBase<T, BinningT>;

    template <size_t axisNum>
    using axisEdgeT = typename BinningT::template getAxisT<axisNum>::EdgeT;

  public:

    using BaseT::BaseT;
    using BaseT::operator=;

    // For self-consistency, the user should
    // always pass the parent binning!
    Bin() = delete;

    /// @brief Differential area of this bin (i.e. product of bin widths)
    double dArea() const noexcept { return BaseT::dVol(); }

  };


  /// @brief CRTP specialisation in 3D
  template<typename T, typename BinningT>
  class Bin<3, T, BinningT>
               : public BinBase<T, BinningT>,
                 public XBinMixin<Bin<3, T, BinningT>,
                                  typename BinningT::template getEdgeT<0>>,
                 public YBinMixin<Bin<3, T, BinningT>,
                                  typename BinningT::template getEdgeT<1>>,
                 public ZBinMixin<Bin<3, T, BinningT>,
                                  typename BinningT::template getEdgeT<2>> {

  protected:

    using BaseT = BinBase<T, BinningT>;

    template <size_t axisNum>
    using axisEdgeT = typename BinningT::template getAxisT<axisNum>::EdgeT;

  public:

    using BaseT::BaseT;
    using BaseT::operator=;

    // For self-consistency, the user should
    // always pass the parent binning!
    Bin() = delete;

  };

}

#endif
