//===-- apps/sa-ews1/sa-ews1.cpp --------------------------------*- C++ -*-===//
//
//                 The RoSA Framework -- Application SA-EWS1
//
// 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 apps/sa-ews1/sa-ews1.cpp
///
/// \author David Juhasz (david.juhasz@tuwien.ac.at)
///         Maximilian Goetzinger (maxgot@utu.fi)
///
/// \date 2017-2020
///
/// \brief The application SA-EWS1 implements the case study from the paper:
/// M. Götzinger, A. Anzanpour, I. Azimi, N. TaheriNejad, and A. M. Rahmani:
/// Enhancing the Self-Aware Early Warning Score System through Fuzzified Data
/// Reliability Assessment. DOI: 10.1007/978-3-319-98551-0_1
//===----------------------------------------------------------------------===//

#include "rosa/agent/Abstraction.hpp"
#include "rosa/agent/CrossCombinator.h"
#include "rosa/agent/ReliabilityConfidenceCombination.h"

#include "rosa/config/version.h"

#include "rosa/app/Application.hpp"

#include "rosa/support/csv/CSVReader.hpp"
#include "rosa/support/csv/CSVWriter.hpp"
#include "rosa/support/iterator/split_tuple_iterator.hpp"

#include "cxxopts/cxxopts.hpp"

#include <fstream>

using namespace rosa;
using namespace rosa::agent;
using namespace rosa::app;
using namespace rosa::terminal;
using namespace rosa::csv;
using namespace rosa::iterator;

const std::string AppName = "SA-EWS1";

/// Representation type of warning levels.
/// \note Make sure it suits all defined enumeration values.
using WarningScoreType = uint8_t;

/// Warning levels for abstraction.
enum WarningScore : WarningScoreType {
  No = 0,
  Low = 1,
  High = 2,
  Emergency = 3
};

/// Vector with all possible warning levels.
std::vector<WarningScore> warningScores = {No, Low, High, Emergency};

/// Type used to represent reliability values.
using ReliabilityType = double;

/// The result type of low-level functions.
using WarningValue = AppTuple<WarningScoreType, ReliabilityType>;

/// Helper function creating an application sensor and setting its execution
/// policy for decimation.
///
/// \note The sensors are created without defining a normal generator function,
/// which is suitable for simulation only.
///
/// \tparam T type of values for the sensor to generate
///
/// \param App the application to create the sensor in
/// \param Name name of the new sensor
/// \param Decimation the decimation parameter
///
/// \return handle for the new sensor
template <typename T>
AgentHandle createSensor(std::unique_ptr<Application> &App,
                         const std::string &Name, const size_t Decimation) {
  AgentHandle Sensor = App->createSensor<T>(Name);
  App->setExecutionPolicy(Sensor, AppExecutionPolicy::decimation(Decimation));
  return Sensor;
}

/// Helper function creating an application agent for pre-processing sensory
/// values and setting its execution policy for decimation.
///
/// Received values are assessed for reliability and abstracted into a \c
/// WarningScore. The result of the processing function is a pair of assessed
/// reliability and abstracted values.
///
/// \note The result, \c WarningScore, is returned as \c WarningScoreType
/// because enumeration types are not integrated into built-in types. Hence, a
/// master to these agents receives its input as \c WarningScoreType values, and
/// may cast them to \c WarningScore explicitly.
///
/// \tparam T type of values to receive from the sensor
///
/// \param App the application to create the agent in
/// \param Name name of the new agent
/// \param Decimation the decimation parameter
/// \param A abstraction to use
/// \param R reliability/confidence combination to use
///
/// \return handle for the new agent
template <typename T>
AgentHandle createLowLevelAgent(
    std::unique_ptr<Application> &App, const std::string &Name,
    const size_t Decimation, const Abstraction<T, WarningScore> &A,
    ReliabilityAndConfidenceCombination<T, WarningScore, ReliabilityType> &R) {
  using result = Optional<WarningValue>;
  using input = AppTuple<T>;
  using handler = std::function<result(std::pair<input, bool>)>;
  AgentHandle Agent = App->createAgent(
      Name, handler([&, Name](std::pair<input, bool> I) -> result {
        LOG_INFO_STREAM << "\n******\n"
                        << Name << " " << (I.second ? "<New>" : "<Old>")
                        << " value: " << std::get<0>(I.first) << "\n******\n";
        const auto SensorValue = std::get<0>(I.first);
        const WarningScoreType Score = A(SensorValue);
        const ReliabilityType InputReliability =
            R.getInputReliability(SensorValue);
        return {WarningValue(Score, InputReliability)};
      }));
  App->setExecutionPolicy(Agent, AppExecutionPolicy::decimation(Decimation));
  return Agent;
}

/// Helper function to print an error message in red color to the terminal and
/// exit from the application.
///
/// \note The function never returns as it calles `exit()`.
///
/// \param Error error message
/// \param ExitCode exit code to return from the application
void logErrorAndExit(const std::string &Error, const int ExitCode) {
    LOG_ERROR_STREAM << Color::Red << Error << Color::Default << std::endl;
    exit(ExitCode);
}

int main(int argc, char *argv[]) {
  LOG_INFO_STREAM << '\n'
                  << library_string() << " -- " << Color::Red << AppName
                  << " app" << Color::Default << '\n';

  /// Paths for the CSV files for simulation.
  ///
  ///@{
  std::string DataCSVPath;
  std::string ScoreCSVPath;
  ///@}

  /// Whether CSV files have header row at the beginning.
  bool CSVHeader = false;

  /// Delimiter character in CSV files.
  char CSVDelimiter = ',';

  /// Decimation of sensors and agents.
  size_t Decimation = 1;

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

  // Handle command-line arguments.
  try {
    cxxopts::Options Options(argv[0], library_string() + " -- " + AppName);
    Options.add_options()("i,input",
                          "Path for the CSV file providing input data",
                          cxxopts::value(DataCSVPath), "file")
                         ("o,output",
                          "Path fr the CSV file to write output scores",
                          cxxopts::value(ScoreCSVPath), "file")
                         ("header", "CSV files have header row",
                          cxxopts::value(CSVHeader)->default_value("false"))
                         ("delimiter", "CSV delimiter character",
                          cxxopts::value(CSVDelimiter)->default_value(","), "char")
                         ("d,decimation", "Decimation of sensors and agents",
                          cxxopts::value(Decimation)->default_value("1"), "int")
                         ("c,cycles", "Number of simulation cycles to perform",
                          cxxopts::value(NumberOfSimulationCycles)->default_value("16"), "int")
                         ("h,help", "Print usage");

    auto Args = Options.parse(argc, argv);

    if (Args.count("help")) {
      LOG_INFO_STREAM << '\n' << Options.help() << std::endl;
      exit(0);
    }

    if (Args.count("input") == 0) {
        throw std::invalid_argument("Argument --input must be defined.");
    }
    if (Args.count("output") == 0) {
        throw std::invalid_argument("Argument --output must be defined.");
    }
  } catch (const cxxopts::OptionException &e) {
    logErrorAndExit(e.what(), 1);
  } catch (const std::invalid_argument &e) {
    logErrorAndExit(e.what(), 1);
  }

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

  //
  // Relevant types and definitions.
  //

  using HRType = int32_t;
  using HRParFun = PartialFunction<HRType, ReliabilityType>;
  using HRLinFun = LinearFunction<HRType, ReliabilityType>;

  using BRType = int32_t;
  using BRParFun = PartialFunction<BRType, ReliabilityType>;
  using BRLinFun = LinearFunction<BRType, ReliabilityType>;

  using SpO2Type = int32_t;
  using SpO2ParFun = PartialFunction<SpO2Type, ReliabilityType>;
  using SpO2LinFun = LinearFunction<SpO2Type, ReliabilityType>;

  using BPSysType = int32_t;
  using BPSysParFun = PartialFunction<BPSysType, ReliabilityType>;
  using BPSysLinFun = LinearFunction<BPSysType, ReliabilityType>;

  using BodyTempType = float;
  using BodyTempParFun = PartialFunction<BodyTempType, ReliabilityType>;
  using BodyTempLinFun = LinearFunction<BodyTempType, ReliabilityType>;

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

  // All sensors are created without defining a normal generator function, but
  // with the default value of the second argument. That, however, requires the
  // data type to be explicitly defined. This is good for simulation only.
  AgentHandle HRSensor = createSensor<HRType>(App, "HR Sensor", Decimation);
  AgentHandle BRSensor = createSensor<BRType>(App, "BR Sensor", Decimation);
  AgentHandle SpO2Sensor =
      createSensor<SpO2Type>(App, "SpO2 Sensor", Decimation);
  AgentHandle BPSysSensor =
      createSensor<BPSysType>(App, "BPSys Sensor", Decimation);
  AgentHandle BodyTempSensor =
      createSensor<BodyTempType>(App, "BodyTemp Sensor", Decimation);

  //
  // Create functionalities.
  //
  LOG_INFO("Creating Functionalities for Agents.");

  //
  // Define abstractions.
  //

  RangeAbstraction<HRType, WarningScore> HRAbstraction(
      {{{0, 40}, Emergency},
       {{40, 51}, High},
       {{51, 60}, Low},
       {{60, 100}, No},
       {{100, 110}, Low},
       {{110, 129}, High},
       {{129, 200}, Emergency}},
      Emergency);

  RangeAbstraction<BRType, WarningScore> BRAbstraction({{{0, 9}, High},
                                                        {{9, 14}, No},
                                                        {{14, 20}, Low},
                                                        {{20, 29}, High},
                                                        {{29, 50}, Emergency}},
                                                       Emergency);

  RangeAbstraction<SpO2Type, WarningScore> SpO2Abstraction(
      {{{1, 85}, Emergency},
       {{85, 90}, High},
       {{90, 95}, Low},
       {{95, 100}, No}},
      Emergency);

  RangeAbstraction<BPSysType, WarningScore> BPSysAbstraction(
      {{{0, 70}, Emergency},
       {{70, 81}, High},
       {{81, 101}, Low},
       {{101, 149}, No},
       {{149, 169}, Low},
       {{169, 179}, High},
       {{179, 200}, Emergency}},
      Emergency);

  RangeAbstraction<BodyTempType, WarningScore> BodyTempAbstraction(
      {{{0.f, 28.f}, Emergency},
       {{28.f, 32.f}, High},
       {{32.f, 35.f}, Low},
       {{35.f, 38.f}, No},
       {{38.f, 39.5f}, High},
       {{39.5f, 100.f}, Emergency}},
      Emergency);

  //
  // Define reliabilities.
  //

  ReliabilityAndConfidenceCombination<HRType, WarningScore, ReliabilityType>
      HRReliability;
  HRReliability.setTimeStep(1);

  std::shared_ptr<Abstraction<HRType, ReliabilityType>> HRAbsRel(
      new HRParFun({{{0, 200}, std::make_shared<HRLinFun>(1, 0)},
                    {{200, 300}, std::make_shared<HRLinFun>(200, 1, 300, 0)}},
                   0));
  HRReliability.setAbsoluteReliabilityFunction(HRAbsRel);

  std::shared_ptr<Abstraction<HRType, ReliabilityType>> HRRelSlope(new HRParFun(
      {{{-200, -100}, std::make_shared<HRLinFun>(-200, 0, -100, 1)},
       {{-100, 100}, std::make_shared<HRLinFun>(1, 0)},
       {{100, 200}, std::make_shared<HRLinFun>(100, 1, 200, 0)}},
      0));
  HRReliability.setReliabilitySlopeFunction(HRRelSlope);

  ReliabilityAndConfidenceCombination<BRType, WarningScore, ReliabilityType>
      BRReliability;
  BRReliability.setTimeStep(1);

  std::shared_ptr<Abstraction<BRType, ReliabilityType>> BRAbsRel(
      new BRParFun({{{0, 40}, std::make_shared<BRLinFun>(1, 0)},
                    {{40, 60}, std::make_shared<BRLinFun>(40, 1, 60, 0)}},
                   0));
  BRReliability.setAbsoluteReliabilityFunction(BRAbsRel);

  std::shared_ptr<Abstraction<BRType, ReliabilityType>> BRRelSlope(
      new BRParFun({{{-30, -20}, std::make_shared<BRLinFun>(-30, 0, -20, 1)},
                    {{-20, 20}, std::make_shared<BRLinFun>(1, 0)},
                    {{20, 30}, std::make_shared<BRLinFun>(20, 1, 30, 0)}},
                   0));
  BRReliability.setReliabilitySlopeFunction(BRRelSlope);

  ReliabilityAndConfidenceCombination<SpO2Type, WarningScore, ReliabilityType>
      SpO2Reliability;
  SpO2Reliability.setTimeStep(1);

  std::shared_ptr<Abstraction<SpO2Type, ReliabilityType>> SpO2AbsRel(
      new SpO2ParFun(
          {
              {{0, 100}, std::make_shared<SpO2LinFun>(1, 0)},
          },
          0));
  SpO2Reliability.setAbsoluteReliabilityFunction(SpO2AbsRel);

  std::shared_ptr<Abstraction<SpO2Type, ReliabilityType>> SpO2RelSlope(
      new SpO2ParFun({{{-8, -5}, std::make_shared<SpO2LinFun>(-8, 0, -5, 1)},
                      {{-5, 5}, std::make_shared<SpO2LinFun>(1, 0)},
                      {{5, 8}, std::make_shared<SpO2LinFun>(5, 1, 8, 0)}},
                     0));
  SpO2Reliability.setReliabilitySlopeFunction(SpO2RelSlope);

  ReliabilityAndConfidenceCombination<BPSysType, WarningScore, ReliabilityType>
      BPSysReliability;
  BPSysReliability.setTimeStep(1);

  std::shared_ptr<Abstraction<BPSysType, ReliabilityType>> BPSysAbsRel(
      new BPSysParFun(
          {{{0, 260}, std::make_shared<BPSysLinFun>(1, 0)},
           {{260, 400}, std::make_shared<BPSysLinFun>(260, 1, 400, 0)}},
          0));
  BPSysReliability.setAbsoluteReliabilityFunction(BPSysAbsRel);

  std::shared_ptr<Abstraction<BPSysType, ReliabilityType>> BPSysRelSlope(
      new BPSysParFun(
          {{{-100, -50}, std::make_shared<BPSysLinFun>(-100, 0, -50, 1)},
           {{-50, 50}, std::make_shared<BPSysLinFun>(1, 0)},
           {{50, 100}, std::make_shared<BPSysLinFun>(50, 1, 100, 0)}},
          0));
  BPSysReliability.setReliabilitySlopeFunction(BPSysRelSlope);

  ReliabilityAndConfidenceCombination<BodyTempType, WarningScore,
                                      ReliabilityType>
      BodyTempReliability;
  BodyTempReliability.setTimeStep(1);

  std::shared_ptr<Abstraction<BodyTempType, ReliabilityType>> BodyTempAbsRel(
      new BodyTempParFun(
          {{{-70.f, -50.f},
            std::make_shared<BodyTempLinFun>(-70.f, 0, -50.f, 1)},
           {{-50.f, 40.f}, std::make_shared<BodyTempLinFun>(1, 0)},
           {{40.f, 60.f}, std::make_shared<BodyTempLinFun>(40.f, 1, 60.f, 0)}},
          0));
  BodyTempReliability.setAbsoluteReliabilityFunction(BodyTempAbsRel);

  std::shared_ptr<Abstraction<BodyTempType, ReliabilityType>> BodyTempRelSlope(
      new BodyTempParFun(
          {{{-0.1f, -0.05f},
            std::make_shared<BodyTempLinFun>(-0.1f, 0, -0.05f, 1)},
           {{-0.05f, 0.05f}, std::make_shared<BodyTempLinFun>(1, 0)},
           {{0.05f, 0.1f},
            std::make_shared<BodyTempLinFun>(0.05f, 1, 0.1f, 0)}},
          0));
  BodyTempReliability.setReliabilitySlopeFunction(BodyTempRelSlope);

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

  AgentHandle HRAgent = createLowLevelAgent(App, "HR Agent", Decimation,
                                            HRAbstraction, HRReliability);
  AgentHandle BRAgent = createLowLevelAgent(App, "BR Agent", Decimation,
                                            BRAbstraction, BRReliability);
  AgentHandle SpO2Agent = createLowLevelAgent(App, "SpO2 Agent", Decimation,
                                              SpO2Abstraction, SpO2Reliability);
  AgentHandle BPSysAgent = createLowLevelAgent(
      App, "BPSys Agent", Decimation, BPSysAbstraction, BPSysReliability);
  AgentHandle BodyTempAgent =
      createLowLevelAgent(App, "BodyTemp Agent", Decimation,
                          BodyTempAbstraction, BodyTempReliability);

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

  App->connectSensor(HRAgent, 0, HRSensor, "HR Sensor Channel");
  App->connectSensor(BRAgent, 0, BRSensor, "BR Sensor Channel");
  App->connectSensor(SpO2Agent, 0, SpO2Sensor, "SpO2 Sensor Channel");
  App->connectSensor(BPSysAgent, 0, BPSysSensor, "BPSys Sensor Channel");
  App->connectSensor(BodyTempAgent, 0, BodyTempSensor,
                     "BodyTemp Sensor Channel");

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

  // Slave positions on BodyAgent.
  enum SlaveIndex : rosa::id_t {
    HRIdx = 0,
    BRIdx = 1,
    SpO2Idx = 2,
    BPSysIdx = 3,
    BodyTempIdx = 4
  };

  CrossCombinator<WarningScoreType, ReliabilityType> BodyCrossCombinator;
  BodyCrossCombinator.setCrossLikelinessParameter(1.5);

  using WarningLikelinessFun =
      LikelinessFunction<WarningScoreType, ReliabilityType>;

  std::shared_ptr<WarningLikelinessFun> BRCrossLikelinessFun(
      new WarningLikelinessFun(0.6));
  BodyCrossCombinator.addCrossLikelinessProfile(HRIdx, BRIdx,
                                                BRCrossLikelinessFun);
  BodyCrossCombinator.addCrossLikelinessProfile(BRIdx, HRIdx,
                                                BRCrossLikelinessFun);
  BodyCrossCombinator.addCrossLikelinessProfile(BRIdx, SpO2Idx,
                                                BRCrossLikelinessFun);
  BodyCrossCombinator.addCrossLikelinessProfile(BRIdx, BPSysIdx,
                                                BRCrossLikelinessFun);
  BodyCrossCombinator.addCrossLikelinessProfile(BRIdx, BodyTempIdx,
                                                BRCrossLikelinessFun);
  BodyCrossCombinator.addCrossLikelinessProfile(SpO2Idx, BRIdx,
                                                BRCrossLikelinessFun);
  BodyCrossCombinator.addCrossLikelinessProfile(BPSysIdx, BRIdx,
                                                BRCrossLikelinessFun);
  BodyCrossCombinator.addCrossLikelinessProfile(BodyTempIdx, BRIdx,
                                                BRCrossLikelinessFun);

  std::shared_ptr<WarningLikelinessFun> HRBPCrossLikelinessFun(
      new WarningLikelinessFun(2.5));
  BodyCrossCombinator.addCrossLikelinessProfile(HRIdx, BPSysIdx,
                                                HRBPCrossLikelinessFun);
  BodyCrossCombinator.addCrossLikelinessProfile(BPSysIdx, HRIdx,
                                                HRBPCrossLikelinessFun);

  // The new agent logs its input values and results in a pair of the sum of
  // received warning scores and their cross-reliability.
  AgentHandle BodyAgent = App->createAgent(
      "Body Agent",
      std::function<Optional<WarningValue>(
          std::pair<WarningValue, bool>, std::pair<WarningValue, bool>,
          std::pair<WarningValue, bool>, std::pair<WarningValue, bool>,
          std::pair<WarningValue, bool>)>(
          [&](std::pair<WarningValue, bool> HR,
              std::pair<WarningValue, bool> BR,
              std::pair<WarningValue, bool> SpO2,
              std::pair<WarningValue, bool> BPSys,
              std::pair<WarningValue, bool> BodyTemp)
              -> Optional<WarningValue> {
            LOG_INFO_STREAM << "\n*******\nBody Agent trigged with values:\n"
                            << (HR.second ? "<New>" : "<Old>")
                            << " HR result: " << HR.first << "\n"
                            << (BR.second ? "<New>" : "<Old>")
                            << " BR result: " << BR.first << "\n"
                            << (SpO2.second ? "<New>" : "<Old>")
                            << " SpO2 result: " << SpO2.first << "\n"
                            << (BPSys.second ? "<New>" : "<Old>")
                            << " BPSys result: " << BPSys.first << "\n"
                            << (BodyTemp.second ? "<New>" : "<Old>")
                            << " BodyTemp result: " << BodyTemp.first
                            << "\n******\n";

            using ValueType =
                std::tuple<rosa::id_t, WarningScoreType, ReliabilityType>;
            const std::vector<ValueType> SlaveValues{
                {HRIdx, std::get<0>(HR.first), std::get<1>(HR.first)},
                {BRIdx, std::get<0>(BR.first), std::get<1>(BR.first)},
                {SpO2Idx, std::get<0>(SpO2.first), std::get<1>(SpO2.first)},
                {BPSysIdx, std::get<0>(BPSys.first), std::get<1>(BPSys.first)},
                {BodyTempIdx, std::get<0>(BodyTemp.first),
                 std::get<1>(BodyTemp.first)}};

            const ReliabilityType crossReliability =
                BodyCrossCombinator.getOutputLikeliness(SlaveValues);

            struct ScoreSum {
              void operator()(const ValueType &V) { ews += std::get<1>(V); }
              WarningScoreType ews{0};
            };
            const ScoreSum scoreSum = std::for_each(
                SlaveValues.cbegin(), SlaveValues.cend(), ScoreSum());

            return {WarningValue(scoreSum.ews, crossReliability)};
          }));
  App->setExecutionPolicy(BodyAgent, AppExecutionPolicy::decimation(Decimation));

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

  App->connectAgents(BodyAgent, HRIdx, HRAgent, "HR Agent Channel");
  App->connectAgents(BodyAgent, BRIdx, BRAgent, "BR Agent Channel");
  App->connectAgents(BodyAgent, SpO2Idx, SpO2Agent, "SpO2 Agent Channel");
  App->connectAgents(BodyAgent, BPSysIdx, BPSysAgent, "BPSys Agent Channel");
  App->connectAgents(BodyAgent, BodyTempIdx, BodyTempAgent,
                     "BodyTemp Agent Channel");

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

  // Create CSV writer.
  std::ofstream ScoreCSV(ScoreCSVPath);
  csv::CSVTupleWriter<WarningScoreType, ReliabilityType>
      ScoreWriter(ScoreCSV, CSVDelimiter);
  if (CSVHeader) {
    ScoreWriter.writeHeader({"EWS", "Reliability"});
  }

  // The agent writes each new input value into a CSV file and produces nothing.
  // \note The execution of the logger is not subject to decimation.
  using logger_result = AppTuple<unit_t>;
  AgentHandle LoggerAgent = App->createAgent(
      "Logger Agent",
      std::function<Optional<logger_result>(std::pair<WarningValue, bool>)>(
          [&ScoreWriter](
              std::pair<WarningValue, bool> Score) -> Optional<logger_result> {
            // The state of \p ScoreWriter is not checked, expecting good.
            ScoreWriter << Score.first;
            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, BodyAgent, "Body Agent Channel");

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

  //
  // Initialize application for simulation.
  //

  App->initializeSimulation();

  //
  // Open CSV files and register them for their corresponding sensors.
  //

  // Type aliases for iterators.
  using CSVDataIterator =
      CSVIterator<HRType, BRType, SpO2Type, BPSysType, BodyTempType>;
  const auto CSVHeaderInfo =
      CSVHeader ? HeaderInformation::HasHeader : HeaderInformation::HasNoHeader;
  std::ifstream DataCSV(DataCSVPath);
  auto [HRRange, BRRange, SpO2Range, BPSysRange, BodyTempRange] =
      splitTupleIterator(
          CSVDataIterator(DataCSV, 0, CSVHeaderInfo, CSVDelimiter),
          CSVDataIterator());

  App->registerSensorValues(HRSensor, std::move(begin(HRRange)), end(HRRange));
  App->registerSensorValues(BRSensor, std::move(begin(BRRange)), end(BRRange));
  App->registerSensorValues(SpO2Sensor, std::move(begin(SpO2Range)),
                            end(SpO2Range));
  App->registerSensorValues(BPSysSensor, std::move(begin(BPSysRange)),
                            end(BPSysRange));
  App->registerSensorValues(BodyTempSensor, std::move(begin(BodyTempRange)),
                            end(BodyTempRange));

  //
  // Simulate.
  //

  App->simulate(NumberOfSimulationCycles);

  return 0;
}
