//===-- rosa/deluxe/DeluxeContext.hpp ---------------------------*- C++ -*-===//
//
//                                 The RoSA Framework
//
//===----------------------------------------------------------------------===//
///
/// \file rosa/deluxe/DeluxeContext.hpp
///
/// \author David Juhasz (david.juhasz@tuwien.ac.at)
///
/// \date 2017-2019
///
/// \brief Public interface for the *deluxe interface* for working with agent
///        systems.
///
//===----------------------------------------------------------------------===//

#ifndef ROSA_DELUXE_DELUXECONTEXT_HPP
#define ROSA_DELUXE_DELUXECONTEXT_HPP

#include "rosa/deluxe/DeluxeSystem.hpp"

#include "rosa/support/types.hpp"

#include <iterator>
#include <memory>
#include <set>

/// Local helper macro to log and return a
/// \c rosa::deluxe::DeluxeContext::ErrorCode value.
///
/// Creates a debug message with the stringified value and returns the value.
///
/// \param Err \c rosa::deluxe::DeluxeContext::ErrorCode value to log and
/// return
#define DCRETERROR(Err)                                                        \
  {                                                                            \
    LOG_DEBUG(#Err);                                                           \
    return Err;                                                                \
  }

namespace rosa {
namespace deluxe {

/// Defines the *deluxe interface*.
class DeluxeContext {

  /// A system owned by \p this object.
  ///
  /// \note The reference is kept in a \c std::shared_ptr because of the member
  /// function \c rosa::deluxe::DeluxeContext::getSystem.
  std::shared_ptr<DeluxeSystem> System;

  /// References to all *sensors* and *agents* created by \p this object.
  std::set<AgentHandle> DeluxeUnits;

public:

  /// Errors that may be resulted by some of the member functions of the class.
  enum struct ErrorCode {
    NoError,
    TypeMismatch,
    NotSensor,
    NotAgent,
    WrongPosition,
    AlreadyHasSlave,
    AlreadyHasMaster,
    AlreadyHasValueStream
  };

  /// Returns a new instance of \c rosa::deluxe::DeluxeContext.
  ///
  /// \param Name name of the underlying \c rosa::DeluxeSystem
  ///
  /// \return \c std::unique_ptr for the new instance of
  /// \c rosa::deluxe::DeluxeContext with a new, empty \c rosa::DeluxeSystem
  static std::unique_ptr<DeluxeContext>
  create(const std::string &Name) noexcept;

private:
  /// Creates a new instance.
  ///
  /// \note Private constructor restricts instantiation to member functions of
  /// the class.
  ///
  /// \param Name name of the underlying \c rosa::MessagingSystem
  DeluxeContext(const std::string &Name) noexcept;

public:
  /// Destroys \p this object.
  ~DeluxeContext(void) noexcept;

  /// Returns a reference for the underlying \c rosa::MessagingSystem.
  ///
  /// \note One cannot do much with a \c rosa::MessagingSystem currently, this
  /// is for future use.
  ///
  /// \return reference for the underlying \c rosa::MessagingSystem.
  std::weak_ptr<MessagingSystem> getSystem(void) const noexcept;

  /// Creates a new *sensor* in the context of \p this object.
  ///
  /// \tparam T type of data the new *sensor* operates on
  ///
  /// \param Name name of the new *sensor*
  /// \param F function for the new *sensor* to generate the next value with
  /// during normal operation
  ///
  /// \note \p F is not used during simulation, in which case
  /// \c rosa::deluxe::DeluxeContext::registerSensorValues is used to register
  /// an alternative simulation data source with
  /// \c rosa::deluxe::DeluxeSensor::registerSimulationDataSource. One may
  /// safely keep relying on the default value of \p F as long as only
  /// simulation of the system is to be done.
  ///
  /// \return \c rosa::AgentHandle for the new *sensor*
  template <typename T>
  AgentHandle createSensor(const std::string &Name,
                           DeluxeSensor::D<T> &&F = [](void) {
                             return T();
                           }) noexcept;

  /// Creates a new *agent* in the context of \p this object.
  ///
  /// \tparam T type of data the new *agent* outputs
  /// \tparam As types of inputs the new *agent* takes
  ///
  /// \param Name name of the new *agent*
  /// \param F function for the new *agent* to process input values and
  /// generate output with
  ///
  /// \return \c rosa::AgentHandle for the new *agent*
  template <typename T, typename... As>
  AgentHandle createAgent(const std::string &Name,
                          DeluxeAgent::D<T, As...> &&F) noexcept;

  /// Connectes a *sensor* to an *agent* in the context of \p this object.
  ///
  /// \param Agent the *agent* to connect to
  /// \param Pos the index of slot of \p Agent to connect \p Sensor to
  /// \param Sensor the *sensor* to connect
  /// \param Description optional textual description of the connection
  ///
  /// \return how successfull connecting \p Sensor to \p Agent at slot index
  /// \p Pos was
  ///
  /// \note The function may return the following
  /// \c rosa::deluxe::DeluxeContext::ErrorCode values:
  /// `ErrorCode`        | Comment
  /// -----------        | -------
  /// `NoError`          | Success
  /// `NotAgent`         | Referred \p Agent is not \c rosa::deluxe::DeluxeAgent
  /// `NotSensor`        | Referred \p Sensor is not \c rosa::deluxe::DeluxeSensor
  /// `WrongPosition`    | \p Pos is not a valid input position of \p Agent
  /// `TypeMismatch`     | Expected input type at position \p Pos of \p Agent is other than the output type of \p Sensor
  /// `AlreadyHasSlave`  | \p Agent at position \p Pos already has a *slave* registered
  /// `AlreadyHasMaster` | \p Sensor already has a *master* registered
  ErrorCode connectSensor(AgentHandle Agent, const size_t Pos,
                          AgentHandle Sensor,
                          const std::string &Description = "") noexcept;

  /// Connectes two *agents* in the context of \p this object.
  ///
  /// \param Master the *agent* to connect to
  /// \param Pos the index of slot of \p Master to connect \p Slave to
  /// \param Slave the *agent* to connect
  /// \param Description optional textual description of the connection
  ///
  /// \return how succesfull connecting \p Slave to \p Master at slot index
  /// \p Pos was
  ///
  /// \note The function may return the following
  /// \c rosa::deluxe::DeluxeContext::ErrorCode values:
  /// `ErrorCode`        | Comment
  /// -----------        | -------
  /// `NoError`          | Success
  /// `NotAgent`         | Referred \p Master or \p Slave is not \c rosa::deluxe::DeluxeAgent
  /// `WrongPosition`    | \p Pos is not a valid input position of \p Master
  /// `TypeMismatch`     | Expected input type at position \p Pos of \p Master is other than the output type of \p Slave
  /// `AlreadyHasSlave`  | \p Master at position \p Pos already has a *slave* registered
  /// `AlreadyHasMaster` | \p Slave already has a *master* registered
  ErrorCode connectAgents(AgentHandle Master, const size_t Pos,
                          AgentHandle Slave,
                          const std::string &Description = "") noexcept;

  /// Initializes \c this object and others managed by \p this object for
  /// setting up and performing simulation.
  ///
  /// \see \c rosa::deluxe::DeluxeContext::registerSensorValues,
  /// \c rosa::deluxe::DeluxeContext::simulate
  ///
  /// Need to clear simulation data sources from all the *sensors*.
  void initializeSimulation(void) noexcept;

  /// Registers a stream providing values for a *sensor* during simulation.
  ///
  /// \tparam Iterator type of iterator providing values for \p Sensor
  /// \tparam T type of values \p Sensor is operating on, always use default!
  ///
  /// \param Sensor the *sensor* to register values for
  /// \param Start provides values for \p Sensor
  /// \param End denotes the end of stream of values
  /// \param Default value to be used when input stream is depleted during
  /// simulation
  ///
  /// \return how successful registering \p Source for \p Sensor
  ///
  /// \note The function may return the following
  /// \c rosa::deluxe::DeluxeContext::ErrorCode values:
  /// `ErrorCode`             | Comment
  /// -----------             | -------
  /// `NoError`               | Success
  /// `TypeMismatch`          | \p Sensor generates values of a type other than \p T
  /// `NotSensor`             | Referred \p Sensor is not \c rosa::deluxe::DeluxeSensor
  /// `AlreadyHasValueStream` | \p Sensor already has simulation data source set
  template <typename Iterator, typename T = typename Iterator::value_type>
  ErrorCode registerSensorValues(AgentHandle Sensor, Iterator &&Start,
                                 const Iterator &End, T Default = {}) noexcept;

  /// Performs the system contained by \p this object.
  ///
  /// The function performs \p NumCycles cycle of simulation. In each cycle,
  /// all the *agents* and *sensors* registered in
  /// \c rosa::deluxe::DeluxeContext::DeluxeUnits are trigged for execution.
  ///
  /// \param NumCycles number of cycles to perform
  ///
  /// \pre All the *sensors* in the system contained by \p this object generate
  /// their output from simulation data sources.
  void simulate(const size_t NumCycles) const noexcept;
};

template <typename T>
AgentHandle DeluxeContext::createSensor(const std::string &Name,
                                        DeluxeSensor::D<T> &&F) noexcept {
  AgentHandle H = System->createSensor<T>(Name, std::move(F));
  DeluxeUnits.emplace(H);
  return H;
}

template <typename T, typename... As>
AgentHandle DeluxeContext::createAgent(const std::string &Name,
                                       DeluxeAgent::D<T, As...> &&F) noexcept {
  AgentHandle H = System->createAgent(Name, std::move(F));
  DeluxeUnits.emplace(H);
  return H;
}

template <typename Iterator, typename T>
DeluxeContext::ErrorCode
DeluxeContext::registerSensorValues(AgentHandle Sensor, Iterator &&Start,
                                    const Iterator &End, T Default) noexcept {
  // Get the type of values provided by \p Iterator.
  STATIC_ASSERT((std::is_same<T, typename Iterator::value_type>::value),
                "type mismatch");

  // Make sure preconditions are met.
  if (!System->isDeluxeSensor(Sensor)) {
    DCRETERROR(ErrorCode::NotSensor);
  }
  auto S = System->getDeluxeSensor(Sensor);
  ASSERT(S); // Sanity check.
  if (S->OutputType != TypeNumberOf<T>::Value) {
    DCRETERROR(ErrorCode::TypeMismatch);
  } else if (S->simulationDataSourceIsSet()) {
    DCRETERROR(ErrorCode::AlreadyHasValueStream);
  }

  // Register input stream.
  // \note Need to capture parameters by value so having local copies.
  S->registerSimulationDataSource(
      DeluxeSensor::D<T>([=](void) mutable noexcept {
        if (Start != End) {
          LOG_TRACE_STREAM << "Reading next value for sensor '" << S->FullName
                           << "': " << *Start << '\n';
          return *Start++;
        } else {
          LOG_TRACE_STREAM << "Providing default value for sensor '"
                           << S->FullName << "': " << Default << '\n';
          return Default;
        }
      }));
  return ErrorCode::NoError;
}

} // End namespace deluxe
} // End namespace rosa

// Undef local macro if not used in the corresponding implementation.
#ifndef ROSA_LIB_DELUXE_DELUXECONTEXT_CPP
#undef DCRETERROR
#endif

#endif // ROSA_DELUXE_DELUXECONTEXT_HPP
