#include <artis-star/common/RootCoordinator.hpp>
#include <artis-star/kernel/pdevs/Coordinator.hpp>
#include <artis-star/kernel/pdevs/GraphManager.hpp>

#include <chrono>
#include <iostream>

#define BOOST_TEST_MODULE Factory_Simple_Tests

#include <boost/test/unit_test.hpp>
#include <artis-star/kernel/pdevs/Simulator.hpp>

#include "Processor.hpp"
#include "PoolRouter.hpp"
#include "ProductionOrderGenerator.hpp"
#include "Router.hpp"

using namespace std::chrono;

class OnlyOnePoolGraphManager
  : public artis::pdevs::GraphManager<artis::common::DoubleTime, artis::common::NoParameters, artis::common::NoParameters> {
public:
  enum sub_models {
    PROCESSOR_1, PROCESSOR_2, PROCESSOR_3, POOL_ROUTER, ROUTER, GENERATOR
  };

  OnlyOnePoolGraphManager(artis::common::Coordinator<artis::common::DoubleTime> *coordinator,
                          const artis::common::NoParameters &parameters,
                          const artis::common::NoParameters &graph_parameters
  ) : artis::pdevs::GraphManager<artis::common::DoubleTime, artis::common::NoParameters, artis::common::NoParameters>(
    coordinator, parameters, graph_parameters),
      _generator("G",
                 {
                   {
                     {0, {artis::factory::PoolMachineSequence(0, {0, 1, 2})}},
                     {1, {artis::factory::PoolMachineSequence(0, {1, 2, 0})}},
                     {2, {artis::factory::PoolMachineSequence(0, {2, 0, 1})}}
                   },
                   62642, 15, 30
                 }),
      _router("R", {1}),
      _pool_router("P_R", {0, 3}),
      _processor_1("M1", {{0, 0, 0}, 1, 10, 1}),
      _processor_2("M2", {{1, 0, 0}, 1, 8, 1}),
      _processor_3("M3", {{2, 0, 0}, 1, 15, 1}) {
    this->add_child(PROCESSOR_1, &_processor_1);
    this->add_child(PROCESSOR_2, &_processor_2);
    this->add_child(PROCESSOR_3, &_processor_3);
    this->add_child(ROUTER, &_router);
    this->add_child(POOL_ROUTER, &_pool_router);
    this->add_child(GENERATOR, &_generator);

    out({&_generator, artis::factory::ProductionOrderGenerator::outputs::OUT})
      >> in({&_router, artis::factory::Router::inputs::IN});

    out({&_router, artis::factory::Router::outputs::OUT_P + 0})
      >> in({&_pool_router, artis::factory::PoolRouter::inputs::IN});

    out({&_pool_router, artis::factory::PoolRouter::outputs::OUT})
      >> in({&_router, artis::factory::Router::inputs::IN_P + 0});

    out({&_pool_router, artis::factory::PoolRouter::outputs::OUT_M + 0})
      >> in({&_processor_1, artis::factory::Processor::inputs::IN});

    out({&_pool_router, artis::factory::PoolRouter::outputs::OUT_M + 1})
      >> in({&_processor_2, artis::factory::Processor::inputs::IN});

    out({&_pool_router, artis::factory::PoolRouter::outputs::OUT_M + 2})
      >> in({&_processor_3, artis::factory::Processor::inputs::IN});

    out({&_processor_1, artis::factory::Processor::outputs::OUT})
      >> in({&_pool_router, artis::factory::PoolRouter::inputs::IN_M + 0});

    out({&_processor_2, artis::factory::Processor::outputs::OUT})
      >> in({&_pool_router, artis::factory::PoolRouter::inputs::IN_M + 1});

    out({&_processor_3, artis::factory::Processor::outputs::OUT})
      >> in({&_pool_router, artis::factory::PoolRouter::inputs::IN_M + 2});
  }

  ~OnlyOnePoolGraphManager() override = default;

private:
  artis::pdevs::Simulator<artis::common::DoubleTime, artis::factory::ProductionOrderGenerator, artis::factory::ProductionOrderGeneratorParameters> _generator;
  artis::pdevs::Simulator<artis::common::DoubleTime, artis::factory::Router, artis::factory::RouterParameters> _router;
  artis::pdevs::Simulator<artis::common::DoubleTime, artis::factory::PoolRouter, artis::factory::PoolRouterParameters> _pool_router;
  artis::pdevs::Simulator<artis::common::DoubleTime, artis::factory::Processor, artis::factory::ProcessorParameters> _processor_1;
  artis::pdevs::Simulator<artis::common::DoubleTime, artis::factory::Processor, artis::factory::ProcessorParameters> _processor_2;
  artis::pdevs::Simulator<artis::common::DoubleTime, artis::factory::Processor, artis::factory::ProcessorParameters> _processor_3;
};

class PoolRouterView : public artis::factory::View {
public:
  PoolRouterView() {
    selector("PoolRouter:waiting_po_number",
             {OnlyOnePoolGraphManager::POOL_ROUTER, artis::factory::PoolRouter::vars::WAITING_PO_NUMBER});
    selector("Generator:total_po_number",
             {OnlyOnePoolGraphManager::GENERATOR, artis::factory::ProductionOrderGenerator::vars::TOTAL_PO_NUMBER});
  }
};

BOOST_AUTO_TEST_CASE(TestCase_OnePool)
{
  artis::common::context::Context<artis::common::DoubleTime> context(0, 8 * 3600); // 8h
  artis::common::RootCoordinator<
    artis::common::DoubleTime, artis::pdevs::Coordinator<
      artis::common::DoubleTime, OnlyOnePoolGraphManager, artis::common::NoParameters>
  > rc(context, "root", artis::common::NoParameters(), artis::common::NoParameters());

  rc.attachView("PoolRouter", new PoolRouterView());
  rc.switch_to_timed_observer(1);

  steady_clock::time_point t1 = steady_clock::now();

  rc.run(context);

  steady_clock::time_point t2 = steady_clock::now();

  duration<double> time_span = duration_cast<duration<double> >(t2 - t1);

  std::cout << "Duration: " << time_span.count() << std::endl;

  artis::factory::Output output(rc.observer());

  output(context.begin(), context.end(), {context.begin(), 1});

  std::cout << artis::factory::Trace::trace().elements().filter_level_type(artis::common::LevelType::USER).to_string()
            << std::endl;

  BOOST_CHECK(true);
}