//===-- examples/application-interface/application-interface.cpp *- 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 examples/application-interface/application-interface.cpp
///
/// \author David Juhasz (david.juhasz@tuwien.ac.at)
///
/// \date 2017-2020
///
/// \brief A simple example on the \c rosa::app::Application and related
/// classes.
//===----------------------------------------------------------------------===//

#include "rosa/config/version.h"

#include "rosa/app/Application.hpp"

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

using namespace rosa;
using namespace rosa::app;
using namespace rosa::terminal;

/// How many cycles of simulation to perform.
const size_t NumberOfSimulationCycles = 16;

int main(void) {
  LOG_INFO_STREAM << '\n'
                  << library_string() << " -- " << Color::Red
                  << "application-interface example" << Color::Default << '\n';

  std::unique_ptr<Application> App = Application::create("App");

  //
  // Create application sensors.
  //
  LOG_INFO("Creating sensors.");

  // All sensors are created without defining a normal generator function, but
  // with the default value of the last argument. That, however, requires the
  // data type to be explicitly defined. This is good for simulation only.

  // The first and second sensors do not receive master-input.
  AgentHandle BoolSensor = App->createSensor<bool>("BoolSensor");
  AgentHandle IntSensor = App->createSensor<int32_t>("IntSensor");

  // This sensor receives master-input and dumps it to \c LOG_INFO_STREAM.
  const std::string FloatSensorName = "FloatSensor";
  AgentHandle FloatSensor = App->createSensor<uint32_t, float>(
      FloatSensorName, [&FloatSensorName](std::pair<uint32_t, bool> I) {
        LOG_INFO_STREAM << "\n******\n"
                        << FloatSensorName << " master-input "
                        << (I.second ? "<New>" : "<Old>")
                        << " value: " << PRINTABLE(I.first) << "\n******\n";
      });

  // This sensor do not receive master-input but produces tuples.
  using TupleType = AppTuple<float, float>;
  AgentHandle TupleSensor = App->createSensor<TupleType>("TupleSensor");

  //
  // Check and set execution policy for sensors.
  //
  LOG_INFO("Execution policies for sensors.");

  LOG_INFO(std::to_string(*App->getExecutionPolicy(IntSensor)));
  App->setExecutionPolicy(IntSensor, AppExecutionPolicy::decimation(2));
  App->setExecutionPolicy(FloatSensor, AppExecutionPolicy::decimation(2));
  LOG_INFO(std::to_string(*App->getExecutionPolicy(IntSensor)));

  //
  // Create low-level application agents with \c createLowLevelAgent.
  //
  LOG_INFO("Creating low-level agents.");

  // All agents below dump their received values to \c LOG_INFO_STREAM on each
  // triggering.

  // This agent does not receive master-input and does not produce
  // master-output. It results in the value it received.
  const std::string BoolAgentName = "BoolAgent";
  using BoolResult = Optional<bool>;
  using BoolHandler = std::function<BoolResult(std::pair<bool, bool>)>;
  AgentHandle BoolAgent = App->createAgent(
      BoolAgentName,
      BoolHandler([&BoolAgentName](std::pair<bool, bool> I) -> BoolResult {
        LOG_INFO_STREAM << "\n******\n"
                        << BoolAgentName << " "
                        << (I.second ? "<New>" : "<Old>")
                        << " value: " << PRINTABLE(I.first) << "\n******\n";
        return {I.first};
      }));

  // This agent receives master-input but does not produce master-output. The
  // agent maintains a state in \c IntAgentOffset. The master-input handler
  // updates \c IntAgentOffset according to each received (new) value from its
  // master. The slave-input handler results in the sum of the received value
  // and the actual value of \c IntAgentOffset.
  const std::string IntAgentName = "IntAgent";
  using IntMasterHandler = std::function<void(std::pair<uint32_t, bool>)>;
  using IntResult = Optional<int32_t>;
  using IntHandler = std::function<IntResult(std::pair<int32_t, bool>)>;
  uint32_t IntAgentOffset = 0;
  AgentHandle IntAgent = App->createAgent(
      IntAgentName,
      // Master-input handler.
      IntMasterHandler(
          [&IntAgentName, &IntAgentOffset](std::pair<uint32_t, bool> I) {
            LOG_INFO_STREAM << "\n******\n"
                            << IntAgentName << " master-input "
                            << (I.second ? "<New>" : "<Old>")
                            << " value: " << PRINTABLE(I.first) << "\n******\n";
            if (I.second) {
              IntAgentOffset = I.first;
            }
          }),
      // Slave-input handler.
      IntHandler([&IntAgentName,
                  &IntAgentOffset](std::pair<int32_t, bool> I) -> IntResult {
        LOG_INFO_STREAM << "\n******\n"
                        << IntAgentName << " " << (I.second ? "<New>" : "<Old>")
                        << " value: " << PRINTABLE(I.first) << "\n******\n";
        return {I.first + IntAgentOffset};
      }));

  // This agent receives master-input and produces master-output. The
  // master-input handler propagaates each received (new) value to its slave as
  // master-output. The slave-input handler results in the value it received and
  // produces no actual master-output.
  const std::string FloatAgentName = "FloatAgent";
  using FloatMasterResult = std::tuple<Optional<uint32_t>>;
  using FloatMasterHandler =
      std::function<FloatMasterResult(std::pair<uint32_t, bool>)>;
  using FloatResult = std::tuple<Optional<float>, Optional<uint32_t>>;
  using FloatHandler = std::function<FloatResult(std::pair<float, bool>)>;
  AgentHandle FloatAgent = App->createAgent(
      FloatAgentName,
      // Master-input handler.
      FloatMasterHandler(
          [&FloatAgentName](std::pair<uint32_t, bool> I) -> FloatMasterResult {
            LOG_INFO_STREAM << "\n******\n"
                            << FloatAgentName << " master-input "
                            << (I.second ? "<New>" : "<Old>")
                            << " value: " << PRINTABLE(I.first) << "\n******\n";
            const auto Output =
                I.second ? Optional<uint32_t>(I.first) : Optional<uint32_t>();
            return {Output};
          }),
      // Slave-input handler.
      FloatHandler([&FloatAgentName](std::pair<float, bool> I) -> FloatResult {
        LOG_INFO_STREAM << "\n******\n"
                        << FloatAgentName << " "
                        << (I.second ? "<New>" : "<Old>")
                        << " value: " << PRINTABLE(I.first) << "\n******\n";
        return {{I.first}, {}};
      }));

  // This agent does not receive master-input and does not produce
  // master-output. It results in the sum of the values it receives in a tuple.
  const std::string TupleAgentName = "TupleAgent";
  using TupleSumResult = Optional<AppTuple<double>>;
  using TupleHandler =
      std::function<TupleSumResult(std::pair<TupleType, bool>)>;
  AgentHandle TupleAgent = App->createAgent(
      TupleAgentName,
      TupleHandler(
          [&TupleAgentName](std::pair<TupleType, bool> I) -> TupleSumResult {
            LOG_INFO_STREAM << "\n******\n"
                            << TupleAgentName << " "
                            << (I.second ? "<New>" : "<Old>")
                            << " value: " << PRINTABLE(I.first) << "\n******\n";
            return {std::get<0>(I.first) + std::get<1>(I.first)};
          }));

  //
  // Set execution policies for low-level agents.
  //
  LOG_INFO("Setting Execution policies for low-level agents.");

  App->setExecutionPolicy(IntAgent, AppExecutionPolicy::awaitAll({0}));
  App->setExecutionPolicy(FloatAgent, AppExecutionPolicy::awaitAll({0}));

  //
  // Connect sensors to low-level agents.
  //
  LOG_INFO("Connect sensors to their corresponding low-level agents.");

  App->connectSensor(BoolAgent, 0, BoolSensor, "Bool Sensor Channel");
  App->connectSensor(IntAgent, 0, IntSensor, "Int Sensor Channel");
  App->connectSensor(FloatAgent, 0, FloatSensor, "Float Sensor Channel");
  App->connectSensor(TupleAgent, 0, TupleSensor, "Tuple Sensor Channel");

  //
  // Create a high-level application agent.
  //
  LOG_INFO("Create high-level agent.");

  using SingleDoubleOutputType = Optional<AppTuple<double>>;
  using SingleUInt32OutputType = Optional<AppTuple<uint32_t>>;
  using NoOutputType = Optional<EmptyAppTuple>;

  // This agent does not receive master-input but produces master-output for its
  // slaves at positions `1` and `2` but not for that at position `0`. The agent
  // maintains a state in \c SumAgentState. The handler increments \c
  // SumAgentState upon each received (new) `true` value from its slave at
  // position `0`. Whenever \c SumAgentState has been updated, it is sent to the
  // slaves at positions `1` and `2`. The handler results in the sum of the
  // values received from slaves at positions `1`, `2`, and `3`.
  using SumResult =
      std::tuple<SingleDoubleOutputType, NoOutputType, SingleUInt32OutputType,
                 SingleUInt32OutputType, NoOutputType>;
  using SumHandler = std::function<SumResult(
      std::pair<AppTuple<bool>, bool>, std::pair<AppTuple<int32_t>, bool>,
      std::pair<AppTuple<float>, bool>, std::pair<AppTuple<double>, bool>)>;
  uint32_t SumAgentState = 0;
  AgentHandle SumAgent = App->createAgent(
      "Sum Agent",
      SumHandler(
          [&SumAgentState](std::pair<AppTuple<bool>, bool> I0,
                           std::pair<AppTuple<int32_t>, bool> I1,
                           std::pair<AppTuple<float>, bool> I2,
                           std::pair<AppTuple<double>, bool> I3) -> SumResult {
            const auto V0 = std::get<0>(I0.first);
            const auto V1 = std::get<0>(I1.first);
            const auto V2 = std::get<0>(I2.first);
            const auto V3 = std::get<0>(I3.first);
            LOG_INFO_STREAM << "\n*******\nSum Agent triggered with values:\n"
                            << (I0.second ? "<New>" : "<Old>")
                            << " bool value: " << PRINTABLE(V0) << "\n"
                            << (I1.second ? "<New>" : "<Old>")
                            << " int value: " << PRINTABLE(V1) << "\n"
                            << (I2.second ? "<New>" : "<Old>")
                            << " float value: " << PRINTABLE(V2) << "\n"
                            << (I3.second ? "<New>" : "<Old>")
                            << " double value: " << PRINTABLE(V3)
                            << "\n******\n";
            if (I0.second && V0) {
              ++SumAgentState;
            }
            const SingleUInt32OutputType MasterOutput =
                I0.second && V0
                    ? SingleUInt32OutputType(AppTuple<uint32_t>(SumAgentState))
                    : SingleUInt32OutputType();
            const AppTuple<double> Output = {V1 + V2 + V3};
            return {{Output}, {}, {MasterOutput}, {MasterOutput}, {}};
          }));

  //
  // Connect low-level agents to the high-level agent.
  //
  LOG_INFO("Connect low-level agents to the high-level agent.");

  App->connectAgents(SumAgent, 0, BoolAgent, "Bool Agent Channel");
  App->connectAgents(SumAgent, 1, IntAgent, "Int Agent Channel");
  App->connectAgents(SumAgent, 2, FloatAgent, "Float Agent Channel");
  App->connectAgents(SumAgent, 3, TupleAgent, "Tuple Agent Channel");

  //
  // For simulation output, create a logger agent writing the output of the
  // high-level agent into a log stream.
  //
  LOG_INFO("Create a logger agent.");

  // The agent dumps each received (new) value to \c LOG_INFO_STREAM and
  // produces nothing; does not receive mater-input and does not produce
  // master-output.
  AgentHandle LoggerAgent = App->createAgent(
      "Logger Agent", std::function<Optional<unit_t>(std::pair<double, bool>)>(
                          [](std::pair<double, bool> Sum) -> Optional<unit_t> {
                            if (Sum.second) {
                              LOG_INFO_STREAM
                                  << "Result: " << PRINTABLE(Sum.first) << "\n";
                            }
                            return {};
                          }));

  //
  // Connect the high-level agent to the logger agent.
  //
  LOG_INFO("Connect the high-level agent to the logger agent.");

  App->connectAgents(LoggerAgent, 0, SumAgent, "Sum Agent Channel");

  //
  // Do simulation.
  //
  LOG_INFO("Setting up and performing simulation.");

  //
  // Initialize application for simulation.
  //

  App->initializeSimulation();

  //
  // Create some vectors and register them for their corresponding sensors.
  //

  std::vector<bool> BoolValues(NumberOfSimulationCycles);
  std::generate(BoolValues.begin(), BoolValues.end(),
                [i = 0](void) mutable -> bool { return (++i % 4) == 0; });
  App->registerSensorValues(BoolSensor, BoolValues.begin(), BoolValues.end());

  std::vector<int32_t> IntValues(NumberOfSimulationCycles);
  std::generate(IntValues.begin(), IntValues.end(),
                [i = 0](void) mutable { return ++i; });
  App->registerSensorValues(IntSensor, IntValues.begin(), IntValues.end());

  std::vector<float> FloatValues(NumberOfSimulationCycles);
  std::generate(FloatValues.begin(), FloatValues.end(),
                [f = 0.5f](void) mutable {
                  f += 0.3f;
                  return std::floor(f) + 0.5f;
                });
  App->registerSensorValues(FloatSensor, FloatValues.begin(),
                            FloatValues.end());

  std::vector<TupleType> TupleValues(NumberOfSimulationCycles);
  std::generate(TupleValues.begin(), TupleValues.end(),
                [f1 = 0.f, f2 = 3.14f](void) mutable -> TupleType {
                  f1 += f2;
                  f2 -= f1;
                  return {f1, f2};
                });
  App->registerSensorValues(TupleSensor, TupleValues.begin(),
                            TupleValues.end());

  //
  // Simulate.
  //

  App->simulate(NumberOfSimulationCycles);

  return 0;
}
