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

#include "YODA/AnalysisObject.h"
#include "YODA/Fillable.h"
#include "YODA/Dbn.h"
#include "YODA/Point.h"
#include "YODA/Estimate0D.h"
#include "YODA/Scatter.h"
#include "YODA/Exceptions.h"
#include <vector>
#include <string>
#include <map>
#include <tuple>
#include <memory>

namespace YODA {


  /// A weighted counter.
  class Counter : public AnalysisObject, public Fillable {
  public:


    using FillType = std::tuple<>;
    using Ptr = std::shared_ptr<Counter>;
    using AnalysisObject::operator =;

    /// @name Constructors
    /// @{

    /// Default constructor
    Counter(const std::string& path="", const std::string& title="")
      : AnalysisObject("Counter", path, title) { }


    /// @brief Constructor accepting an explicit Dbn0D.
    ///
    /// Intended both for internal persistency and user use.
    Counter(const Dbn0D& dbn,
            const std::string& path="", const std::string& title="")
            : AnalysisObject("Counter", path, title), _dbn(dbn) { }


    /// @brief Constructor accepting an explicit rvalue Dbn0D.
    ///
    /// Intended both for internal persistency and user use.
    Counter(Dbn0D&& dbn,
            const std::string& path="", const std::string& title="")
            : AnalysisObject("Counter", path, title), _dbn(std::move(dbn)) { }


    /// @brief Constructor accepting a double (treated as the weight of a single fill).
    ///
    /// Intended for user convenience only, so Counter can be treated as a number.
    Counter(double w,
            const std::string& path="", const std::string& title="")
            : AnalysisObject("Counter", path, title) { _dbn.fill(w); }


    /// Copy constructor with optional new path
    /// @todo Don't copy the path?
    Counter(const Counter& c, const std::string& path="")
            : AnalysisObject("Counter", (path.size() == 0) ? c.path() : path, c, c.title()),
              _dbn(c._dbn) { }

    /// Move constructor with optional new path
    Counter(Counter&& c, const std::string& path="")
            : AnalysisObject("Counter", (path.size() == 0) ? c.path() : path, c, c.title()),
              _dbn(std::move(c._dbn)) { }


    /// Assignment operator
    Counter& operator = (const Counter& c) noexcept {
      if (this != &c) {
        AnalysisObject::operator = (c);
        _dbn = c._dbn;
      }
      return *this;
    }

    /// Move operator
    Counter& operator = (Counter&& c) noexcept {
      if (this != &c) {
        AnalysisObject::operator = (c);
        _dbn = std::move(c._dbn);
      }
      return *this;
    }

    /// Make a copy on the stack
    Counter clone() const {
      return Counter(*this);
    }

    /// Make a copy on the heap, via 'new'
    Counter* newclone() const {
      return new Counter(*this);
    }

    /// @}


    /// @name I/O
    /// @{

    // @brief Render information about this AO
    void _renderYODA(std::ostream& os, const int width = 13) const noexcept {
      os << std::setw(width) << std::left << "# sumW" << "\t"
         << std::setw(width) << std::left << "sumW2"  << "\t"
         << "numEntries\n";
      os << std::setw(width) << std::left << sumW() << "\t";
      os << std::setw(width) << std::left << sumW2() << "\t";
      os << std::setw(width) << std::left << numEntries() << "\n";
    }

    // @brief Render scatter-like information about this AO
    void _renderFLAT(std::ostream& os, const int width) const noexcept {
      Scatter1D tmp = this->mkScatter();
      tmp._renderYODA(os, width);
    }

    /// @}


    /// @name Dimensions
    /// @{

    /// @brief Total dimension of this data object
    size_t dim() const noexcept { return 1; }

    /// @brief Fill dimension of this data object
    size_t fillDim() const noexcept { return 0; }

    /// @}


    /// @name Modifiers
    /// @{

    /// Fill histo by value and weight
    virtual int fill(double weight=1.0, double fraction=1.0) {
      _dbn.fill(weight, fraction);
      return 0;
    }

    virtual int fill(FillType&&, double weight=1.0, double fraction=1.0) {
      return fill(weight, fraction);
    }

    /// @brief Reset the histogram.
    ///
    /// Keep the binning but set all bin contents and related quantities to zero
    virtual void reset() {
      _dbn.reset();
    }


    /// Rescale as if all fill weights had been different by factor @a scalefactor.
    void scaleW(double scalefactor) {
      setAnnotation("ScaledBy", annotation<double>("ScaledBy", 1.0) * scalefactor);
      _dbn.scaleW(scalefactor);
    }

    /// @}


    /// @name Data access
    /// @{

    /// Get the number of fills
    double numEntries(bool=false) const { return _dbn.numEntries(); }

    /// Get the effective number of fills
    double effNumEntries(bool=false) const { return _dbn.effNumEntries(); }

    /// Get the sum of weights
    double sumW(bool=false) const { return _dbn.sumW(); }

    /// Get the sum of squared weights
    double sumW2(bool=false) const { return _dbn.sumW2(); }

    /// Get the value
    double val(bool=false) const { return sumW(); }

    /// Get the uncertainty on the value
    /// @todo Implement on Dbn0D and feed through to this and Dbn1D, 2D, etc.
    double err() const { return sqrt(sumW2()); }

    /// Get the relative uncertainty on the value
    /// @todo Implement on Dbn0D and feed through to this and Dbn1D, 2D, etc.
    // double err() const { return _dbn.err(); }
    double relErr() const {
      /// @todo Throw excp if sumW2 is 0?
      return sumW2() != 0 ? err()/sumW() : 0;
    }

    /// @}


    /// @name Internal state access and modification (mainly for persistency use)
    /// @{

    /// Get the internal distribution object
    const Dbn0D& dbn() const {
      return _dbn;
    }

    /// Set the internal distribution object: CAREFUL!
    void setDbn(const Dbn0D& dbn) {
      _dbn = dbn;
    }

    /// Set the internal distribution object: CAREFUL!
    void setDbn(Dbn0D&& dbn) {
      _dbn = std::move(dbn);
    }

    /// Set the internal distribution object: CAREFUL!
    void set(const double numEntries, const double sumW, const double sumW2) {
      _dbn.set(numEntries, sumW, sumW2);
    }

    // /// Set the whole object state
    // void setState(const Dbn0D& dbn, const AnalysisObject::Annotations& anns=AnalysisObject::Annotations()) {
    //   setDbn(dbn);
    //   setAnnotations(anns);
    // }

    /// @}


    /// @name Adding and subtracting counters
    /// @{

    /// Add another counter to this
    Counter& operator += (const Counter& toAdd) {
      if (AO::hasAnnotation("ScaledBy")) AO::rmAnnotation("ScaledBy");
      _dbn += toAdd._dbn;
      return *this;
    }
    //
    Counter& operator += (Counter&& toAdd) {
      if (AO::hasAnnotation("ScaledBy")) AO::rmAnnotation("ScaledBy");
      _dbn += std::move(toAdd._dbn);
      return *this;
    }

    /// Subtract another counter from this
    Counter& operator -= (const Counter& toSubtract) {
      if (AO::hasAnnotation("ScaledBy")) AO::rmAnnotation("ScaledBy");
      _dbn -= toSubtract._dbn;
      return *this;
    }
    //
    Counter& operator -= (Counter&& toSubtract) {
      if (AO::hasAnnotation("ScaledBy")) AO::rmAnnotation("ScaledBy");
      _dbn -= std::move(toSubtract._dbn);
      return *this;
    }

    /// Increment as if by a fill of weight = 1
    /// @note This is post-increment only, i.e. cn++ not ++cn
    Counter& operator ++ () {
      if (AO::hasAnnotation("ScaledBy")) AO::rmAnnotation("ScaledBy");
      *this += 1;
      return *this;
    }

    /// Increment as if by a fill of weight = -1
    /// @note This is post-decrement only, i.e. cn-- not --cn
    Counter& operator -- () {
      if (AO::hasAnnotation("ScaledBy")) AO::rmAnnotation("ScaledBy");
      *this -= 1;
      return *this;
    }

    /// Scale by a double (syntactic sugar for @c scaleW(s))
    Counter& operator *= (double s) {
      if (AO::hasAnnotation("ScaledBy")) AO::rmAnnotation("ScaledBy");
      scaleW(s);
      return *this;
    }

    /// Inverse-scale by a double (syntactic sugar for @c scaleW(1/s))
    Counter& operator /= (double s) {
      if (AO::hasAnnotation("ScaledBy")) AO::rmAnnotation("ScaledBy");
      scaleW(1/s);
      return *this;
    }

    /// @}

    /// @name Type reduction
    /// @{

    inline Estimate0D mkEstimate(const std::string& path = "", const std::string& source = "") const {
      Estimate0D rtn;
      for (const std::string& a : annotations()) {
        if (a != "Type")  rtn.setAnnotation(a, annotation(a));
      }
      rtn.setAnnotation("Path", path);

      rtn.setVal(val());
      if (numEntries()) { // only set uncertainty for filled Dbns
        rtn.setErr(err(), source);
      }
      return rtn;
    }

    inline Scatter1D mkScatter(const std::string& path = "") const {
      Scatter1D rtn;
      for (const std::string& a : annotations()) {
        if (a != "Type")  rtn.setAnnotation(a, annotation(a));
      }
      rtn.setAnnotation("Path", path);

      rtn.addPoint(Point1D(val(), err()));
      return rtn;
    }

    /// @brief Return an inert version of the analysis object (e.g. scatter, estimate)
    AnalysisObject* mkInert(const std::string& path = "",
                            const std::string& source = "") const noexcept {
      return mkEstimate(path, source).newclone();
    }

    /// @}

    /// @name MPI (de-)serialisation
    //@{

    size_t lengthContent(bool = false) const noexcept {
      return _dbn._lengthContent();
    }

    std::vector<double> serializeContent(bool = false) const noexcept {
      return _dbn._serializeContent();
    }

    void deserializeContent(const std::vector<double>& data) {
      _dbn._deserializeContent(data);
    }

    // @}

  private:

    /// @name Data
    /// @{

    /// Contained 0D distribution
    Dbn0D _dbn;

    /// @}

  };


  /// @name Combining counters: global operators
  /// @{

  /// Add two counters
  inline Counter operator + (Counter first, const Counter& second) {
    first += second;
    return first;
  }

  /// Add two counters
  inline Counter operator + (Counter first, Counter&& second) {
    first += std::move(second);
    return first;
  }

  /// Subtract two counters
  inline Counter operator - (Counter first, const Counter& second) {
    first -= second;
    return first;
  }

  /// Subtract two counters
  inline Counter operator - (Counter first, Counter&& second) {
    first -= std::move(second);
    return first;
  }

  /// Divide two counters, with an uncorrelated error treatment
  Estimate0D divide(const Counter& numer, const Counter& denom);

  /// Divide two Counter objects
  inline Estimate0D operator / (const Counter& numer, const Counter& denom) {
    return divide(numer, denom);
  }

  /// Divide two Counter objects
  inline Estimate0D operator / (Counter&& numer, const Counter& denom) {
    return divide(std::move(numer), denom);
  }

  /// Divide two Counter objects
  inline Estimate0D operator / (const Counter& numer, Counter&& denom) {
    return divide(numer, std::move(denom));
  }

  /// Divide two Counter objects
  inline Estimate0D operator / (Counter&& numer, Counter&& denom) {
    return divide(std::move(numer), std::move(denom));
  }

  /// @todo Add divide functions/operators on pointers

  /// @brief Calculate an efficiency ratio of two counters
  ///
  /// Note that an efficiency is not the same thing as a standard division of two
  /// histograms: the errors must be treated as correlated
  Estimate0D efficiency(const Counter& accepted, const Counter& total);

  /// @}


}

#endif
