// -*- 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_TRANSFORMATION_H
#define YODA_TRANSFORMATION_H

#include "YODA/Exceptions.h"
#include "YODA/Utils/MathUtils.h"
#include "YODA/Utils/ndarray.h"
#include <algorithm>
#include <functional>
#include <map>
#include <utility>


namespace YODA {


  /// Generalised transformation
  ///
  /// The tranformation uses a functor that
  /// acts on a double (possibly with additional
  /// arguments) and returns a double.
  /// The generalisation is in the number of
  /// dimensions in which the trf can be applied.
  template<size_t N, typename... Args>
  class Transformation {
  public:

    /// Convenient aliases
    using Pair = std::pair<double,double>;
    using NdVal = typename Utils::ndarray<double, N>;
    using NdValPair = typename Utils::ndarray<Pair, N>;
    using Breakdown = std::map<std::string,std::pair<double,double>>;


    /// @name Constructors
    /// @{

    /// No nullary constructor since a trf method
    /// should always be supplied
    Transformation() = delete;

    /// @brief Default constructor setting the trf method
    Transformation(const std::function<double(double, Args...)>& fn) : _Fn(fn) { }

    /// @brief Default constructor setting the trf method
    Transformation(std::function<double(double, Args...)>&& fn) : _Fn(std::move(fn)) { }


    /// @brief Operator calling the functor
    double operator()(const double v, Args&&... args) const { return _Fn(v, std::forward<Args>(args)...); }

    /// @}


    /// @name Modifiers
    /// @{

    /// @brief Transform value @a val
    void transform(double& val, Args&&... args) const { val = _Fn(val, std::forward<Args>(args)...); }

    /// @brief Transform values @a vals
    void transform(NdVal& vals, Args&&... args) const {
      for (double& val : vals) {
        transform(val, std::forward<Args>(args)...);
      }
    }

    // @todo allow arbitrary arrays, not just NdVal ...

    /// @brief Transform value @a val and breakdown of signed error pairs @a signed_errs
    void transform(double& val, Breakdown& signed_errs, Args&&... args) const {
      // keep track of original central value
      const double oldval = val;
      // transform central value
      transform(val, std::forward<Args>(args)...);
      // transform errors
      for (auto& item : signed_errs) {
        item.second.first = _Fn(oldval + item.second.first, std::forward<Args>(args)...) - val;
        item.second.second = _Fn(oldval + item.second.second, std::forward<Args>(args)...) - val;
      }
    }

    /// @brief Transform value @a val and error pair @a errs
    void transform(double& val, Pair& errs, Args&&... args) const {
      // transform errors
      const double trf_min = _Fn(val - errs.first, std::forward<Args>(args)...);
      const double trf_max = _Fn(val + errs.second, std::forward<Args>(args)...);
      // transform central value
      transform(val, std::forward<Args>(args)...);
      // Deal with possible inversions of min/max ordering under the transformation
      const double new_min = std::min(trf_min, trf_max);
      const double new_max = std::max(trf_min, trf_max);
      errs = std::make_pair(val - new_min, new_max - val);
    }

    /// @brief Transform values @a vals and error pairs @a errs
    void transform(NdVal& vals, NdValPair& errs, Args&&... args) const {
      for (size_t i = 0; i < N; ++i) {
        transform(vals[i], errs[i], std::forward<Args>(args)...);
      }
    }

    /// @}

  protected:

    /// @name Functor
    /// @{

    std::function<double(double, Args...)> _Fn;

    /// @}

  };



  /// @name Convenient aliases
  /// @{

  template<size_t N>
  using Trf = Transformation<N>;

  /// @}

}

#endif
