//===-- rosa/agent/FunctionAbstractions.hpp ---------------------*- C++ -*-===//
//
//                                 The RoSA Framework
//
//===----------------------------------------------------------------------===//
///
/// \file rosa/agent/FunctionAbstractions.hpp
///
/// \author Benedikt Tutzer (benedikt.tutzer@tuwien.ac.at)
///
/// \date 2019
///
/// \brief Definition of *FunctionAbstractions* *functionality*.
///
//===----------------------------------------------------------------------===//

#ifndef ROSA_AGENT_FUNCTIONABSTRACTIONS_HPP
#define ROSA_AGENT_FUNCTIONABSTRACTIONS_HPP

#include "rosa/agent/Functionality.h"
#include "rosa/agent/Abstraction.hpp"

#include "rosa/support/debug.hpp"

#include <algorithm>
#include <vector>
#include <cmath>
#include <memory>

namespace rosa {
namespace agent {

/// Implements \c rosa::agent::Abstraction as a linear function,
/// y = Coefficient * X + Intercept.
///
/// \note This implementation is supposed to be used to represent a linear
/// function from an arithmetic domain to an arithmetic range. This is enforced
/// statically.
///
/// \tparam D type of the functions domain
/// \tparam R type of the functions range
template <typename D, typename R> class LinearFunction :
	public Abstraction<D, R>{
  // Make sure the actual type arguments are matching our expectations.
  STATIC_ASSERT((std::is_arithmetic<D>::value),
    "LinearFunction not arithmetic T");
  STATIC_ASSERT((std::is_arithmetic<R>::value),
    "LinearFunction not to arithmetic");
protected:
  /// The Intercept of the linear function
  const D Intercept;
  /// The Coefficient of the linear function
  const D Coefficient;

public:
  /// Creates an instance.
  ///
  /// \param Intercept the intercept of the linear function
  /// \param Coefficient the coefficient of the linear function
  LinearFunction(D Intercept, D Coefficient) noexcept
      : Abstraction<D, R>(Intercept),
        Intercept(Intercept),
        Coefficient(Coefficient) {}

  /// Destroys \p this object.
  ~LinearFunction(void) = default;

  /// Checks wether the Abstraction evaluates to default at the given position
  /// As LinearFunctions can be evaluated everythwere, this is always false
  ///
  /// \param V the value at which to check if the function falls back to it's
  /// default value.
  ///
  /// \return false
  bool isDefaultAt(const D &V) const noexcept override {
    (void)V;
    return false;
  }

  /// Evaluates the linear function
  ///
  /// \param X the value at which to evaluate the function
  ///
  /// \return Coefficient*X + Intercept
  virtual R operator()(const D &X) const noexcept override {
    return Intercept + X*Coefficient;
  }
};

/// Implements \c rosa::agent::Abstraction as a sine function,
/// y = Amplitude * sin(Frequency * X + Phase) + Average.
///
/// \note This implementation is supposed to be used to represent a sine
/// function from an arithmetic domain to an arithmetic range. This is enforced
/// statically.
///
/// \tparam D type of the functions domain
/// \tparam R type of the functions range
template <typename D, typename R> class SineFunction :
	public Abstraction<D, R>{
  // Make sure the actual type arguments are matching our expectations.
  STATIC_ASSERT((std::is_arithmetic<D>::value),
    "SineFunction not arithmetic T");
  STATIC_ASSERT((std::is_arithmetic<R>::value),
    "SineFunction not to arithmetic");
protected:
  /// The frequency of the sine wave
  const D Frequency;
  /// The Ampiltude of the sine wave
  const D Amplitude;
  /// The Phase-shift of the sine wave
  const D Phase;
  /// The y-shift of the sine wave
  const D Average;

public:
  /// Creates an instance.
  ///
  /// \param Frequency the frequency of the sine wave
  /// \param Amplitude the amplitude of the sine wave
  /// \param Phase the phase of the sine wave
  /// \param Average the average of the sine wave
  SineFunction(D Frequency, D Amplitude, D Phase, D Average) noexcept
      : Abstraction<D, R>(Average),
        Frequency(Frequency),
        Amplitude(Amplitude),
        Phase(Phase),
        Average(Average) {}

  /// Destroys \p this object.
  ~SineFunction(void) = default;

  /// Checks wether the Abstraction evaluates to default at the given position
  /// As SineFunctions can be evaluated everythwere, this is always false
  ///
  /// \param V the value at which to check if the function falls back to it's
  /// default value.
  ///
  /// \return false
  bool isDefaultAt(const D &V) const noexcept override {
    (void)V;
    return false;
  }

  /// Evaluates the sine function
  ///
  /// \param X the value at which to evaluate the function
  /// \return the value of the sine-function at X
  virtual R operator()(const D &X) const noexcept override {
    return Amplitude*sin(Frequency * X + Phase) + Average;
  }
};
/// Implements \c rosa::agent::Abstraction as a partial function from a domain
// /to a range.
///
/// \note This implementation is supposed to be used to represent a partial
/// function from an arithmetic domain to an arithmetic range. This is enforced
/// statically.
///
/// A partial function is defined as a list of abstractions, where each
/// abstraction is associated a range in which it is defined. These ranges must
/// be mutually exclusive.
///
/// \tparam D type of the functions domain
/// \tparam R type of the functions range
template <typename D, typename R>
class PartialFunction : public Abstraction<D, R> {
  // Make sure the actual type arguments are matching our expectations.
  STATIC_ASSERT((std::is_arithmetic<D>::value), "abstracting not arithmetic");
  STATIC_ASSERT((std::is_arithmetic<R>::value),
      "abstracting not to arithmetic");

private:
  /// A \c rosa::agent::RangeAbstraction RA is used to represent the association
  /// from ranges to Abstractions.
  /// This returns the Abstraction that is defined for any given value, or
  /// a default Abstraction if no Abstraction is defined for that value.
  RangeAbstraction<D, std::shared_ptr<Abstraction<D, R>>> RA;

public:
  /// Creates an instance by Initializing the underlying \c Abstraction.
  ///
  /// \param Map the mapping to do abstraction according to
  /// \param Default abstraction to abstract to by default
  ///
  /// \pre Each key defines a valid range such that `first <= second` and
  /// there are no overlapping ranges defined by the keys.
  PartialFunction(const std::map<std::pair<D, D>,
      std::shared_ptr<Abstraction<D, R>>> &Map,
    const R Default)
      : Abstraction<D, R>(Default),
        RA(Map, std::shared_ptr<Abstraction<D, R>>
          (new Abstraction<D, R>(Default))) {
  }

  /// Destroys \p this object.
  ~PartialFunction(void) = default;

  /// Checks wether the Abstraction evaluates to default at the given position
  ///
  /// \param V the value at which to check if the function falls back to it's
  /// default value.
  ///
  /// \return false
  bool isDefaultAt(const D &V) const noexcept override {
    return RA.isDefaultAt(V) ? true : RA(V)->isDefaultAt(V);
  }

  /// Searches for an Abstraction for the given value and executes it for that
  /// value, if such an Abstraction is found. The default Abstraction is
  /// evaluated otherwise.
  ///
  /// \param V value to abstract
  ///
  /// \return the abstracted value based on the set mapping
  R operator()(const D &V) const noexcept override {
    return RA(V)->operator()(V);
  }
};
} // End namespace agent
} // End namespace rosa

#endif // ROSA_AGENT_FUNCTIONABSTRACTIONS_HPP
