//===-- examples/deluxe-interface/deluxe-interface.cpp ----------*- C++ -*-===//
//
//                                 The RoSA Framework
//
//===----------------------------------------------------------------------===//
///
/// \file examples/deluxe-interface/deluxe-interface.cpp
///
/// \author David Juhasz (david.juhasz@tuwien.ac.at)
///
/// \date 2017-2019
///
/// \brief A simple example on the \c rosa::deluxe::DeluxeContext and related
/// classes.
//===----------------------------------------------------------------------===//

#include "rosa/config/version.h"

#include "rosa/deluxe/DeluxeContext.hpp"

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

using namespace rosa;
using namespace rosa::deluxe;
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
                  << "deluxe-interface example" << Color::Default << '\n';

  std::unique_ptr<DeluxeContext> C = DeluxeContext::create("Deluxe");

  //
  // Create deluxe 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 = C->createSensor<bool>("BoolSensor");
  AgentHandle IntSensor = C->createSensor<int32_t>("IntSensor");

  // This sensor receives master-input and dumps it to \c LOG_INFO_STREAM.
  const std::string FloatSensorName = "FloatSensor";
  AgentHandle FloatSensor = C->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: " << I.first << "\n******\n";
      });

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

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

  //
  // Create low-level deluxe 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 = C->createAgent(
      BoolAgentName,
      BoolHandler([&BoolAgentName](std::pair<bool, bool> I) -> BoolResult {
        LOG_INFO_STREAM << "\n******\n"
                        << BoolAgentName << " "
                        << (I.second ? "<New>" : "<Old>")
                        << " value: " << 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 = C->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: " << 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: " << 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 = C->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: " << 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: " << I.first << "\n******\n";
        return {{I.first}, {}};
      }));

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

  C->setExecutionPolicy(IntAgent, DeluxeExecutionPolicy::awaitAll({0}));
  C->setExecutionPolicy(FloatAgent, DeluxeExecutionPolicy::awaitAll({0}));

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

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

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

  // 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` and `2`.
  using SumResult = std::tuple<Optional<double>, Optional<unit_t>,
                               Optional<uint32_t>, Optional<uint32_t>>;
  using SumHandler = std::function<SumResult(
      std::pair<bool, bool>, std::pair<int32_t, bool>, std::pair<float, bool>)>;
  uint32_t SumAgentState = 0;
  AgentHandle SumAgent = C->createAgent(
      "Sum Agent",
      SumHandler([&SumAgentState](std::pair<bool, bool> I0,
                                  std::pair<int32_t, bool> I1,
                                  std::pair<float, bool> I2) -> SumResult {
        LOG_INFO_STREAM << "\n*******\nSum Agent triggered with values:\n"
                        << (I0.second ? "<New>" : "<Old>")
                        << " bool value: " << I0.first << "\n"
                        << (I1.second ? "<New>" : "<Old>")
                        << " int value: " << I1.first << "\n"
                        << (I2.second ? "<New>" : "<Old>")
                        << " float value: " << I2.first << "\n******\n";
        if (I0.second && I0.first) {
          ++SumAgentState;
        }
        const auto MasterOutput = I0.second && I0.first
                                      ? Optional<uint32_t>(SumAgentState)
                                      : Optional<uint32_t>();
        const auto Output = I1.first + I2.first;
        return {{Output}, {}, {MasterOutput}, {MasterOutput}};
      }));

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

  C->connectAgents(SumAgent, 0, BoolAgent, "Bool Agent Channel");
  C->connectAgents(SumAgent, 1, IntAgent, "Int Agent Channel");
  C->connectAgents(SumAgent, 2, FloatAgent, "Float 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 =
      C->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: " << Sum.first << "\n";
                           }
                           return {};
                         }));

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

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

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

  //
  // Initialize deluxe context for simulation.
  //

  C->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; });
  C->registerSensorValues(BoolSensor, BoolValues.begin(), BoolValues.end());

  std::vector<int32_t> IntValues(NumberOfSimulationCycles);
  std::generate(IntValues.begin(), IntValues.end(),
                [i = 0](void) mutable { return ++i; });
  C->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;
                });
  C->registerSensorValues(FloatSensor, FloatValues.begin(), FloatValues.end());

  //
  // Simulate.
  //

  C->simulate(NumberOfSimulationCycles);

  return 0;
}
