// -*- 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_Estimate0D_h
#define YODA_Estimate0D_h

#include "YODA/AnalysisObject.h"
#include "YODA/Exceptions.h"
#include "YODA/Estimate.h"
#include "YODA/Scatter.h"

#ifdef HAVE_HDF5
#include "YODA/Utils/H5Utils.h"
#endif

#include <string>

namespace YODA {

  /// @brief An estimate in 0D
  ///
  /// This class linkes the Estimate class with
  /// AnalysisObject, so it can be used as a type
  /// reduction for the Counter type.
  class Estimate0D : public Estimate,
                     public AnalysisObject {
  public:

    using Ptr = std::shared_ptr<Estimate0D>;
    using BaseT = Estimate;
    using BaseT::operator =;
    using BaseT::operator +=;
    using BaseT::operator -=;
    using AnalysisObject::operator =;

    /// @name Constructors
    //@{

    /// @brief Nullary constructor for unique pointers etc.
    ///
    /// @note The setting of optional path/title is not possible here in order
    /// to avoid overload ambiguity for brace-initialised constructors.
    Estimate0D(const std::string& path = "", const std::string& title = "")
      : BaseT(), AnalysisObject("Estimate0D", path, title) { }


    /// @brief Constructor to set an Estimate0D with a pre-filled state.
    ///
    /// Principally designed for internal persistency use.
    Estimate0D(double v,
             std::map<std::string,std::pair<double,double>>& errors,
             const std::string& path = "", const std::string& title = "")
      : BaseT(v, errors), AnalysisObject("Estimate0D", path, title) { }


    /// @brief Alternative constructor to set an Estimate0D with value and uncertainty.
    Estimate0D(const double v, const std::pair<double,double>& e, const std::string& source = "",
             const std::string& path = "", const std::string& title = "")
      : BaseT(v, e, source), AnalysisObject("Estimate0D", path, title) { }

    /// @brief Copy constructor (needed for clone functions).
    ///
    /// @note Compiler won't generate this constructor automatically.
    Estimate0D(const Estimate0D& other) : Estimate(other),
         AnalysisObject(other.type(), other.path(), other, other.title()) { }

    /// @brief Move constructor
    Estimate0D(Estimate0D&& other) : BaseT(std::move(other)),
         AnalysisObject(other.type(), other.path(), other, other.title()) { }

    /// @brief Copy constructor using base class
    Estimate0D(const BaseT& other, const std::string& path = "", const std::string& title = "")
      : BaseT(other), AnalysisObject("Estimate0D", path, title) { }

    /// @brief Move constructor using base class
    Estimate0D(BaseT&& other, const std::string& path = "", const std::string& title = "")
      : BaseT(std::move(other)), AnalysisObject("Estimate0D", path, title) { }

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

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

    //@}


    /// @name Operators
    //@{

    /// Copy assignment
    ///
    /// Sets all the parameters using the ones provided from an existing Estimate0D.
    Estimate0D& operator=(const Estimate0D& toCopy) noexcept {
      if (this != &toCopy) {
        AnalysisObject::operator = (toCopy);
        BaseT::operator = (toCopy);
      }
      return *this;
    }

    /// Move assignment
    Estimate0D& operator = (Estimate0D&& toMove) noexcept {
      if (this != &toMove) {
        AnalysisObject::operator = (toMove);
        BaseT::operator = (std::move(toMove));
      }
      return *this;
    }

    /// Add two Estimate0Ds
    Estimate0D& add(const Estimate0D& toAdd, const std::string& pat_uncorr="^stat|^uncor" ) {
      if (hasAnnotation("ScaledBy")) rmAnnotation("ScaledBy");
      BaseT::add(toAdd, pat_uncorr);
      return *this;
    }
    //
    Estimate0D& operator+=(const Estimate0D& toAdd) {
      return add(toAdd);
    }

    /// Add two (rvalue) Estimate0Ds
    Estimate0D& add(Estimate0D&& toAdd, const std::string& pat_uncorr="^stat|^uncor" ) {
      if (hasAnnotation("ScaledBy")) rmAnnotation("ScaledBy");
      BaseT::add(std::move(toAdd), pat_uncorr);
      return *this;
    }
    //
    Estimate0D& operator+=(Estimate0D&& toAdd) {
      return add(std::move(toAdd));
    }

    /// Subtract two Estimate0Ds
    Estimate0D& subtract(const Estimate0D& toSubtract, const std::string& pat_uncorr="^stat|^uncor" ) {
      if (hasAnnotation("ScaledBy")) rmAnnotation("ScaledBy");
      BaseT::subtract(toSubtract, pat_uncorr);
      return *this;
    }
    //
    Estimate0D& operator-=(const Estimate0D& toSubtract) {
      return subtract(toSubtract);
    }

    /// Subtract two (rvalue) Estimate0Ds
    Estimate0D& subtract(Estimate0D&& toSubtract, const std::string& pat_uncorr="^stat|^uncor" ) {
      if (hasAnnotation("ScaledBy")) rmAnnotation("ScaledBy");
      BaseT::subtract(std::move(toSubtract), pat_uncorr);
      return *this;
    }
    Estimate0D& operator-=(Estimate0D&& toSubtract) {
      return subtract(std::move(toSubtract));
    }

    //@}

    /// @name Dimensions
    /// @{

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

    //@}

    /// @name Modifiers
    //@{

    /// Reset the internal values
    void reset() noexcept {
      BaseT::reset();
    }

    //@}

    // @brief Render information about this AO
    void _renderYODA(std::ostream& os, const int width = 13) const noexcept {

      // Render error sources
      const std::vector<std::string> labels = this->sources();
      if (labels.size()) {
        os << "ErrorLabels: [";
        for (size_t i = 0; i < labels.size(); ++i) {
          const std::string& src = labels[i];
          if (i)  os << ", ";
          os << std::quoted(src);
        }
        os << "]\n";
      }

      // column header: content types
      os << std::setw(width) << std::left << "# value" << "\t";
      const int errwidth = std::max(int(std::to_string(labels.size()).size()+7), width); // "errDn(" + src + ")"
      for (size_t i = 0; i < labels.size(); ++i) {
        const std::string& src = labels[i];
        if (src.empty()) {
          os << std::setw(errwidth) << std::left << "totalDn" << "\t";
          os << std::setw(errwidth) << std::left << "totalUp" << "\t";
        }
        else {
          os << std::setw(errwidth) << std::left << ("errDn(" + std::to_string(i+1) + ")") << "\t";
          os << std::setw(errwidth) << std::left << ("errUp(" + std::to_string(i+1) + ")") << "\t";
        }
      }
      os << "\n";

      os << std::setw(width) << std::left << val() << "\t"; // render value
      // render systs if available
      for (const std::string& src : labels) {
        if (!hasSource(src)) {
          os << std::setw(errwidth) << std::left << "---" << "\t"
             << std::setw(errwidth) << std::left << "---" << "\t";
          continue;
        }
        const auto& errs = err(src);
        os << std::setw(errwidth) << std::left << errs.first << "\t"
           << std::setw(errwidth) << std::left << errs.second << "\t";
      }
      os << "\n";
    }

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

    #ifdef HAVE_HDF5

    // @brief Extract error labels of this AO
    void _extractLabels(std::vector<std::string>& labels,
                        std::vector<size_t>& labelsizes) const noexcept {
      // append new set of keys
      std::vector<std::string> keys = sources();
      labelsizes.emplace_back(keys.size()+1); //< +1 for length info itself
      labels.insert(std::end(labels),
                    std::make_move_iterator(std::begin(keys)),
                    std::make_move_iterator(std::end(keys)));
      // sort and remove duplicates
      std::sort(labels.begin(), labels.end());
      labels.erase( std::unique(labels.begin(), labels.end()), labels.end() );
    }

    // @brief Extract error breakdown of this AO into the map of @a edges
    void _extractEdges(std::map<std::string, EdgeHandlerBasePtr>& edges,
                       const std::vector<std::string>& labels) const noexcept {

      using lenT = EdgeHandler<size_t>;
      using lenPtr = EdgeHandlerPtr<size_t>;
      const std::string lengthID("sizeinfo");
      lenPtr nedges = std::static_pointer_cast<lenT>(edges.find(lengthID)->second);

      std::vector<std::string> keys = sources();
      nedges->extend({ keys.size() });
      const auto& itr = labels.cbegin();
      const auto& itrEnd = labels.cend();
      for (const string& k : keys) {
        size_t dist = std::find(itr, itrEnd, k) - itr;
        nedges->extend({ dist });
      }

    }

    #endif

    /// @}

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

    size_t lengthContent(bool fixed_length = false) const noexcept {
      return BaseT::_lengthContent(fixed_length);
    }

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

    void deserializeContent(const std::vector<double>& data) {
      BaseT::_deserializeContent(data, data.size() == 4);
    }

    /// @}

    /// @name Type reductions
    //@{

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

      // Add the PointND
      const double tot = fabs(totalErrPos(pat_match)); // use positive error component
      rtn.addPoint( Point1D(val(), {tot, tot}) );

      return rtn;
    }

    /// @brief Method returns clone of the estimate with streamlined error source
    AnalysisObject* mkInert(const std::string& path = "",
                            const std::string& source = "") const noexcept {
      Estimate0D* rtn = newclone();
      rtn->setPath(path);
      if (rtn->numErrs() == 1) {
        try {
          rtn->renameSource("", source);
        }
        catch (YODA::UserError& e) { }
      }
      return rtn;
    }

    //@}

  };

  /// @name Generalised transformations
  /// @{

  inline void transform(Estimate0D& est, const Trf<1>& fn) {
    est.transform(fn);
  }

  template <typename FN>
  inline void transform(Estimate0D& est, const FN& fn) {
    transform(est, Trf<1>(fn));
  }

  /// @}


  /// @name Global operators for Estimate0D objects
  //@{

  /// @brief Add two Estimate0D objects
  inline Estimate0D operator + (Estimate0D lhs, const Estimate0D& rhs) {
    lhs += rhs;
    return lhs;
  }

  /// @brief Add two Estimate0D objects
  inline Estimate0D operator + (Estimate0D lhs, Estimate0D&& rhs) {
    lhs += std::move(rhs);
    return lhs;
  }

  /// @brief Subtract two Estimate0D objects
  inline Estimate0D operator - (Estimate0D lhs, const Estimate0D& rhs) {
    lhs -= rhs;
    return lhs;
  }

  /// @brief Subtract two Estimate0D objects
  inline Estimate0D operator - (Estimate0D lhs, Estimate0D&& rhs) {
    lhs -= std::move(rhs);
    return lhs;
  }

  /// @brief Divide two Estimate0D objects
  inline Estimate0D divide(const Estimate0D& numer, const Estimate0D& denom,
                           const std::string& pat_uncorr="^stat|^uncor" ) {
    Estimate0D rtn = divide(static_cast<const Estimate&>(numer),
                            static_cast<const Estimate&>(denom), pat_uncorr);
    if (rtn.hasAnnotation("ScaledBy")) rtn.rmAnnotation("ScaledBy");
    if (numer.path() == denom.path())  rtn.setPath(numer.path());
    return rtn;
  }

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

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

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

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

  /// @brief Divide two Estimate0D objects using binomial statistics
  inline Estimate0D efficiency(const Estimate0D& accepted, const Estimate0D& total,
                               const std::string& pat_uncorr="^stat|^uncor" ) {
    Estimate0D rtn = efficiency(static_cast<const Estimate&>(accepted),
                                static_cast<const Estimate&>(total), pat_uncorr);
    if (rtn.hasAnnotation("ScaledBy")) rtn.rmAnnotation("ScaledBy");
    if (accepted.path() == total.path())  rtn.setPath(total.path());
    return rtn;
  }

  //@}

}

#endif
