/**
 * @file FactoryGraphManager.hpp
 * @author The ARTIS Development Team
 * See the AUTHORS or Authors.txt file
 */

/*
 * ARTIS - the multimodeling and simulation environment
 * This file is a part of the ARTIS environment
 *
 * Copyright (C) 2013-2023 ULCO http://www.univ-littoral.fr
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#ifndef ARTIS_FACTORY_FACTORY_GRAPH_MANAGER_HPP
#define ARTIS_FACTORY_FACTORY_GRAPH_MANAGER_HPP

#include "Base.hpp"
#include "FactoryGraphManagerParameters.hpp"
#include "ItemStock.hpp"
#include "JsonReader.hpp"
#include "PoolRouter.hpp"
#include "Router.hpp"
#include "Stock.hpp"

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

namespace artis::factory {

class FactoryGraphManager
  : public artis::pdevs::GraphManager<artis::common::DoubleTime, artis::common::NoParameters, FactoryGraphManagerParameters> {
public:
  enum sub_models {
    GENERATOR, ROUTER, POOL_ROUTER, MACHINE = 10000, ITEM_STOCK = 20000, STOCK = 30000
  };

  FactoryGraphManager(Coordinator *coordinator, const artis::common::NoParameters &parameters,
                      const FactoryGraphManagerParameters &graph_parameters) :
    artis::pdevs::GraphManager<artis::common::DoubleTime, artis::common::NoParameters, FactoryGraphManagerParameters>(
      coordinator, parameters, graph_parameters) {
    unsigned int machine_index = 0;

    ProductionOrderGeneratorParameters generator_parameters{graph_parameters.get_programs(),
                                                            graph_parameters.factory._generator._random_seed,
                                                            graph_parameters.factory._generator._min_send_speed_rate,
                                                            graph_parameters.factory._generator._max_send_speed_rate,
                                                            graph_parameters.factory._generator._po_number,
                                                            graph_parameters.factory._generator._po_order};

    _generator = new GeneratorSimulator("G", generator_parameters);
    _router = new RouterSimulator("R", {graph_parameters.factory._pools.size()});

    this->add_child(ROUTER, _router);
    this->add_child(GENERATOR, _generator);

    // item stocks
    for (const auto &p: graph_parameters.factory._stocks) {
      _item_stocks.push_back(
        new ItemStockSimulator("I_S_" + std::to_string(p._id),
                               ItemStockParameters{graph_parameters.get_item_stock(p._id),
                                                   graph_parameters.get_machines_to_item_stock(p._id)}));
      this->add_child(ITEM_STOCK + p._id, _item_stocks.back());
    }

    // intermediate stocks
    for (const auto &p: graph_parameters.factory._pools) {
      unsigned int pool_id = std::get<0>(p);
      for (const auto &s: std::get<3>(p)) {
        _stocks[std::make_pair(pool_id, s._id)] = new StockSimulator(
          "S_" + std::to_string(pool_id) + "_" + std::to_string(s._id),
          StockParameters{graph_parameters.get_machines_to_in_stock(pool_id, s._id),
                          graph_parameters.get_machines_to_out_stock(pool_id, s._id),
                          s._capacity});
        this->add_child(STOCK + pool_id * 100 + s._id, _stocks[std::make_pair(pool_id, s._id)]);
      }
    }

    // pool + machines
    for (const auto &p: graph_parameters.factory._pools) {
      unsigned int pool_id = std::get<0>(p);
      std::map<unsigned int, unsigned int> capacities;

      for (const auto &m: std::get<2>(p)) {
        capacities[m->machine_id] = m->capacity;
      }
      _pool_routers.push_back(
        new PoolRouterSimulator("P_R_" + std::to_string(pool_id), PoolRouterParameters{std::get<1>(p), capacities}));
      this->add_child(POOL_ROUTER + std::get<0>(p), _pool_routers.back());
      out({_router, artis::factory::Router::outputs::OUT_P + pool_id})
        >> in({_pool_routers.back(), artis::factory::PoolRouter::inputs::IN});
      out({_pool_routers.back(), artis::factory::PoolRouter::outputs::OUT})
        >> in({_router, artis::factory::Router::inputs::IN_P + pool_id});

      // machines
      for (const auto &m: std::get<2>(p)) {
        switch (m->machine_type) {
          case PROCESSOR: {
            auto &processor_parameters = (ProcessorParameters &) (*m);
            auto new_processor = new ProcessorSimulator(
              "M_" + std::to_string(m->pool_id) + "_" + std::to_string(m->machine_id), processor_parameters);

            _processors.push_back(new_processor);
            this->add_child(MACHINE + machine_index, new_processor);

            // connection processor <-> pool router
            out({_pool_routers.back(), artis::factory::PoolRouter::outputs::OUT_M + m->machine_id})
              >> in({new_processor, artis::factory::Processor::inputs::IN});
            out({new_processor, artis::factory::Processor::outputs::OUT})
              >> in({_pool_routers.back(), artis::factory::PoolRouter::inputs::IN_M + m->machine_id});

            // connection processor <-> item stock
            std::vector<unsigned int> item_stocks;

            for (const auto &q: processor_parameters.in_out) {
              for (const auto &r: q.second._ins) {
                if (r._source._pool_id == -1) {
                  item_stocks.push_back(r._source._stock_id);
                }
              }
            }
            for (const auto &q: item_stocks) {
              out({new_processor, artis::factory::Processor::outputs::OUT_ITEM_DEMAND + q})
                >> in({_item_stocks.back(), artis::factory::ItemStock::inputs::IN + m->machine_id});
              out({_item_stocks.back(), artis::factory::ItemStock::outputs::OUT + m->machine_id})
                >> in({new_processor, artis::factory::Processor::inputs::IN_ITEM});
            }
            // connection processor <-> intermediate stock
            std::vector<std::pair<unsigned int, unsigned int>> stocks;

            for (const auto &q: processor_parameters.in_out) {
              for (const auto &r: q.second._outs) {
                auto u = std::make_pair((unsigned int) r._destination._pool_id, r._destination._stock_id);

                if (r._destination._pool_id != -1 and std::find(stocks.cbegin(), stocks.cend(), u) == stocks.cend()) {
                  stocks.push_back(u);
                }
              }
            }
            for (const auto &q: stocks) {
              out({new_processor, artis::factory::Processor::outputs::OUT_STOCK + (q.first * 100 + q.second)})
                >> in({_stocks[q], artis::factory::Stock::inputs::IN});
              out({new_processor, artis::factory::Processor::outputs::OUT_STOCK_AVAILABLE + (q.first * 100 + q.second)})
                >> in({_stocks[q], artis::factory::Stock::inputs::IN_STOCK_AVAILABLE + m->machine_id});
              out({_stocks[q], artis::factory::Stock::outputs::OUT_STOCK_AVAILABLE + m->machine_id})
                >> in(
                  {new_processor, artis::factory::Processor::inputs::IN_STOCK_AVAILABLE + (q.first * 100 + q.second)});
            }
            stocks.clear();
            for (const auto &q: processor_parameters.in_out) {
              for (const auto &r: q.second._ins) {
                auto u = std::make_pair((unsigned int) r._source._pool_id, r._source._stock_id);

                if (r._source._pool_id != -1 and std::find(stocks.cbegin(), stocks.cend(), u) == stocks.cend()) {
                  stocks.push_back(u);
                }
              }
            }
            for (const auto &q: stocks) {
              out({new_processor, artis::factory::Processor::outputs::OUT_STOCK_DEMAND + (q.first * 100 + q.second)})
                >> in({_stocks[q], artis::factory::Stock::inputs::IN_DEMAND + m->machine_id});
              out({_stocks[q], artis::factory::Stock::outputs::OUT + m->machine_id})
                >> in({new_processor, artis::factory::Processor::inputs::IN_STOCK});
            }
            ++machine_index;
            break;
          }
          case SINK: {
            auto new_sink = new SinkSimulator(
              "M_" + std::to_string(m->pool_id) + "_" + std::to_string(m->machine_id), (SinkParameters &) (*m));

            _sinks.push_back(new_sink);
            this->add_child(MACHINE + machine_index, new_sink);

            // connection sink <-> pool router
            out({_pool_routers.back(), artis::factory::PoolRouter::outputs::OUT_M + m->machine_id})
              >> in({new_sink, artis::factory::Sink::inputs::IN});
            out({new_sink, artis::factory::Sink::outputs::OUT})
              >> in({_pool_routers.back(), artis::factory::PoolRouter::inputs::FINISH});
            ++machine_index;
            break;
          }
          case SEPARATOR: {
            // TODO
            break;
          }
          case COMBINER : {
            // TODO
            break;
          }
          case CONVEYOR : {
            // TODO
            break;
          }
          default: {
          }
        }
      }
    }
    out({_generator, artis::factory::ProductionOrderGenerator::outputs::OUT})
      >> in({_router, artis::factory::Router::inputs::IN});
  }

private:
  typedef artis::pdevs::Simulator<artis::common::DoubleTime, artis::factory::ProductionOrderGenerator, artis::factory::ProductionOrderGeneratorParameters> GeneratorSimulator;
  typedef artis::pdevs::Simulator<artis::common::DoubleTime, artis::factory::Router, artis::factory::RouterParameters> RouterSimulator;
  typedef artis::pdevs::Simulator<artis::common::DoubleTime, artis::factory::PoolRouter, artis::factory::PoolRouterParameters> PoolRouterSimulator;
  typedef artis::pdevs::Simulator<artis::common::DoubleTime, artis::factory::Processor, artis::factory::ProcessorParameters> ProcessorSimulator;
  typedef artis::pdevs::Simulator<artis::common::DoubleTime, artis::factory::Sink, artis::factory::SinkParameters> SinkSimulator;
  typedef artis::pdevs::Simulator<artis::common::DoubleTime, artis::factory::ItemStock, artis::factory::ItemStockParameters> ItemStockSimulator;
  typedef artis::pdevs::Simulator<artis::common::DoubleTime, artis::factory::Stock, artis::factory::StockParameters> StockSimulator;

  GeneratorSimulator *_generator;
  RouterSimulator *_router;
  std::vector<PoolRouterSimulator *> _pool_routers;
  std::vector<ProcessorSimulator *> _processors;
  std::vector<SinkSimulator *> _sinks;
  std::vector<ItemStockSimulator *> _item_stocks;
  std::map<std::pair<unsigned int, unsigned int>, StockSimulator *> _stocks;
};

}

#endif //ARTIS_FACTORY_FACTORY_GRAPH_MANAGER_HPP
