/**
 * @file Processor.cpp
 * @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/>.
 */

#include "ItemStock.hpp"
#include "Processor.hpp"
#include "Stock.hpp"

namespace artis::factory {

void Processor::dint(const Time &t) {
  for (auto it = _jobs.cbegin(); it != _jobs.cend();) {
    if (it->second._next_time == t and it->second._phase == Phase::READY) {
      it = _jobs.erase(it);
    } else {
      ++it;
    }
  }
  for (auto &j: _jobs) {
    auto &job = j.second;

    if (job._next_time == t) {
      switch (job._phase) {
        case Phase::DEMAND_SUPPLY:
          job._phase = Phase::WAIT_SUPPLY;
          job._next_time = artis::common::DoubleTime::infinity;
          break;
        case Phase::WAIT_SUPPLY:
        case Phase::WAIT:
          assert(false);
          break;
        case Phase::LOADING: {
          job._phase = Phase::PROCESSING;
          job._next_time = t + durations(job._po->get_productID()).processing_time;
          break;
        }
        case Phase::PROCESSING: {
          job._phase = Phase::UNLOADING;
          job._next_time = t + durations(job._po->get_productID()).unload_time;
          break;
        }
        case Phase::UNLOADING: {
          auto it = _parameters.in_out.find(job._po->get_productID());

          if (it != _parameters.in_out.cend() and not it->second._outs.empty()) {
            job._phase = Phase::SEND_STOCK_AVAILABLE;
            job._next_time = t;
          } else {
            job._phase = Phase::READY;
            job._next_time = t;
          }

#ifdef WITH_TRACE
          Trace::trace()
            << TraceElement(get_name(), t,
                            artis::common::FormalismType::PDEVS,
                            artis::common::FunctionType::DELTA_INT,
                            artis::common::LevelType::USER)
            << "FINISH po = " << job._po->to_string();
          Trace::trace().flush();
#endif

          break;
        }
        case Phase::SEND_STOCK_AVAILABLE: {
          job._phase = Phase::WAIT_STOCK_AVAILABLE;
          job._next_time = artis::common::DoubleTime::infinity;
          break;
        }
        default:
          assert(false);
      }
    }
  }
}

void Processor::dext(const Time &t, const Time & /* e */, const Bag &bag) {
  std::for_each(bag.begin(), bag.end(), [this, t](const ExternalEvent &event) {
    if (event.on_port(inputs::IN)) {
      uint8_t *data = nullptr;

      assert(_jobs.size() < _parameters.capacity);

      event.data()(data);

      auto po = std::make_unique<ProductionOrder>(data, event.data().size());
      auto po_id = po->getID();
      auto product_id = po->get_productID();
      auto it = _parameters.in_out.find(po->get_productID());

      if (it == _parameters.in_out.end() or it->second._ins.empty()) {
        _jobs[po_id] = Job{Phase::LOADING, std::move(po), 0, t + durations(product_id).load_time};
      } else {
        _jobs[po_id] = Job{Phase::DEMAND_SUPPLY, std::move(po), (unsigned int) it->second._ins.size(), t};
      }

#ifdef WITH_TRACE
      Trace::trace()
        << TraceElement(get_name(), t,
                        artis::common::FormalismType::PDEVS,
                        artis::common::FunctionType::DELTA_EXT,
                        artis::common::LevelType::USER)
        << "START po = " << _jobs[po_id]._po->to_string();
      Trace::trace().flush();
#endif

    } else if (event.on_port(inputs::IN_ITEM)) {
      ItemResponse response;

      event.data()(response);

      assert(_jobs.find(response.po_id) != _jobs.end());

      auto &job = _jobs[response.po_id];
      job._demand_number--;
      if (job._demand_number == 0) {
        job._phase = Phase::LOADING;
        job._next_time = t + durations(job._po->get_productID()).load_time;
      }
    } else if (event.on_port(inputs::IN_STOCK)) {
      ProductResponse response;

      event.data()(response);

      assert(_jobs.find(response.po_id) != _jobs.end());

      auto &job = _jobs[response.po_id];

      job._demand_number--;
      if (job._demand_number == 0) {
        job._phase = Phase::LOADING;
        job._next_time = t + durations(job._po->get_productID()).load_time;
      }
    } else if (event.port_index() >= inputs::IN_STOCK_AVAILABLE) {
      StockAvailableResponse response;

      event.data()(response);

      assert(_jobs.find(response.po_id) != _jobs.end());

      auto &job = _jobs[response.po_id];

      switch (response.response) {
        case StockAvailableResponse::AVAILABLE: {
          job._phase = Phase::READY;
          job._next_time = t;
          break;
        }
        case StockAvailableResponse::NOT_AVAILABLE: {
          job._phase = Phase::WAIT_STOCK_AVAILABLE;
          job._next_time = artis::common::DoubleTime::infinity;
          break;
        }
        case StockAvailableResponse::NEW_AVAILABLE: {
          job._phase = Phase::SEND_STOCK_AVAILABLE;
          job._next_time = t;
          break;
        }
      }
    }
  });
}

void Processor::start(const Time & /* t */) {
}

Time Processor::ta(const Time &t) const {
  if (not _jobs.empty()) {
    return std::min_element(_jobs.cbegin(), _jobs.cend(), [](const auto &lhs, const auto &rhs) {
      return (lhs.second._next_time != artis::common::DoubleTime::infinity and
              lhs.second._next_time < rhs.second._next_time) or
             rhs.second._next_time == artis::common::DoubleTime::infinity;
    })->second._next_time - t;
  }
  return artis::common::DoubleTime::infinity;
}

Bag Processor::lambda(const Time &t) const {
  Bag bag;

  for (const auto &e: _jobs) {
    const auto &job = e.second;

    if (job._next_time == t) {
      if (job._phase == Phase::READY) {
        bag.push_back(ExternalEvent(outputs::OUT, common::event::Value(job._po->_buffer, job._po->_size)));

        auto it = _parameters.in_out.find(job._po->get_productID());

        if (it != _parameters.in_out.cend() and not it->second._outs.empty()) {
          const auto &p = it->second._outs.front()._destination;

          bag.push_back(ExternalEvent(outputs::OUT_STOCK + int(p._pool_id * 100 + p._stock_id),
                                      StockEntry(e.first, job._po->operation_index(),
                                                 job._po->get_productID(), 1)));
        }
      } else if (job._phase == Phase::DEMAND_SUPPLY) {
        auto it = _parameters.in_out.find(job._po->get_productID());

        for (const auto &item: it->second._ins) {
          if (item._source._pool_id == -1) {
            bag.push_back(ExternalEvent(outputs::OUT_ITEM_DEMAND + (int) item._source._stock_id,
                                        ItemDemand(item._id, e.first, item._quantity)));
          } else {
            bag.push_back(
              ExternalEvent(outputs::OUT_STOCK_DEMAND + (int) (item._source._pool_id * 100 + item._source._stock_id),
                            ProductDemand(e.first, job._po->operation_index() - (job._po->get_operation(
                                            job._po->operation_index() - 1)->get_type() == CHANGE_POOL_TYPE ? 2 : 1),
                                          job._po->get_productID(), 1)));
          }
        }
      } else if (job._phase == Phase::SEND_STOCK_AVAILABLE) {
        auto it = _parameters.in_out.find(job._po->get_productID());

        if (it != _parameters.in_out.cend() and not it->second._outs.empty()) {
          const auto &p = it->second._outs.front()._destination;

          bag.push_back(ExternalEvent(outputs::OUT_STOCK_AVAILABLE + int(p._pool_id * 100 + p._stock_id), e.first));
        }
      }
    }
  }
  return bag;
}

artis::common::event::Value Processor::observe(const Time & /* t */, unsigned int /* index */) const {
  return {};
}

} // namespace artis::factory