//===-- rosa/agent/FunctionAbstractions.hpp ---------------------*- C++ -*-===//
//
//                                 The RoSA Framework
//
// Distributed under the terms and conditions of the Boost Software License 1.0.
// See accompanying file LICENSE.
//
// If you did not receive a copy of the license file, see
// http://www.boost.org/LICENSE_1_0.txt.
//
//===----------------------------------------------------------------------===//
///
/// \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/Abstraction.hpp"
#include "rosa/agent/Functionality.h"

#include "rosa/support/debug.hpp"

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

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 R Intercept;
  /// The Coefficient of the linear function
  const R Coefficient;

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

  /// Creates an instance given the two points on a linear function.
  ///
  /// \param x1 The x-value of the first point
  /// \param y1 The x-value of the first point
  /// \param x2 The y-value of the second point
  /// \param y2 The y-value of the second point
  LinearFunction(D x1, R y1, D x2, R y2) noexcept
      : Abstraction<D, R>(y1 - x1 * (y1 - y2) / (x1 - x2),
                          (y1 - y2) / (x1 - x2)) {}

  /// Creates an instance given the two points on a linear function.
  ///
  /// \param p1 The coordinates of the first point
  /// \param p2 The coordinates of the second point
  LinearFunction(std::pair<D, R> p1, std::pair<D, R> p2) noexcept
      : LinearFunction<D, R>(p1.first, p1.second, p2.first, p2.second) {}

  /// 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;
  }

  /// Getter for member variable Intercept
  ///
  /// \return Intercept
  D getIntercept() const { return Intercept; }

  /// Setter for member variable Intercept
  ///
  /// \param Intercept the new Intercept
  void setIntercept(const D &Intercept) { this->Intercept = Intercept; }

  /// Getter for member variable Coefficient
  ///
  /// \return Coefficient
  D getCoefficient() const { return Coefficient; }

  /// Setter for member variable Coefficient
  ///
  /// \param Coefficient the new Intercept
  void setCoefficient(const D &Coefficient) { this->Coefficient = Coefficient; }

  /// Set Intercept and Coefficient from two points on the linear function
  ///
  /// \param x1 The x-value of the first point
  /// \param y1 The x-value of the first point
  /// \param x2 The y-value of the second point
  /// \param y2 The y-value of the second point
  void setFromPoints(D x1, R y1, D x2, R y2) {
    Coefficient = (y1 - y2) / (x1 - x2);
    Intercept = y1 - Coefficient * x1;
  }

  /// Set Intercept and Coefficient from two points on the linear function
  ///
  /// \param p1 The coordinates of the first point
  /// \param p2 The coordinates of the second point
  inline void setFromPoints(std::pair<D, R> p1, std::pair<D, R> p2) {
    setFromPoints(p1.first, p1.second, p2.first, p2.second);
  }

  /// 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;
  }
};

enum StepDirection { StepUp, StepDown };

/// Implements \c rosa::agent::PartialFunction as a step function from 0 to 1
/// with a ramp in between
///
/// \tparam D type of the functions domain
/// \tparam R type of the functions range
template <typename D, typename R>
class StepFunction : 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:
  D Coefficient;
  D RightLimit;
  StepDirection Direction;

public:
  /// Creates an instance by Initializing the underlying \c Abstraction.
  ///
  /// \param Coefficient Coefficient of the ramp
  /// \param Direction wether to step up or down
  ///
  /// \pre Coefficient > 0
  StepFunction(D Coefficient, StepDirection Direction = StepUp)
      : Abstraction<D, R>(0), Coefficient(Coefficient),
        RightLimit(1.0f / Coefficient), Direction(Direction) {
    ASSERT(Coefficient > 0);
  }

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

  /// Setter for Coefficient
  ///
  /// \param Coefficient the new Coefficient
  void setCoefficient(const D &Coefficient) {
    ASSERT(Coefficient > 0);
    this->Coefficient = Coefficient;
    this->RightLimit = 1 / Coefficient;
  }

  /// Setter for RightLimit
  ///
  /// \param RightLimit the new RightLimit
  void setRightLimit(const D &RightLimit) {
    ASSERT(RightLimit > 0);
    this->RightLimit = RightLimit;
    this->Coefficient = 1 / RightLimit;
  }

  /// 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 if the is negative, true otherwise
  bool isDefaultAt(const D &V) const noexcept override { return V > 0; }

  /// Executes the Abstraction
  ///
  /// \param V value to abstract
  ///
  /// \return the abstracted value
  R operator()(const D &V) const noexcept override {
    R ret = 0;
    if (V <= 0)
      ret = 0;
    else if (V >= RightLimit)
      ret = 1;
    else
      ret = V * Coefficient;
    return Direction == StepDirection::StepUp ? ret : 1 - ret;
  }
};

/// 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 if the value falls into a defined range and the Abstraction
  /// defined for that range does not fall back to it's default value.
  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
