#pragma once

#include <cstddef>
#include <functional>
#include <optional>
#include <random>
#include <string>
#include <vector>

#include <QVector2D>

#include "Building.h"
#include "BuildingId.h"
#include "FactoryState.h"
#include "GameConfig.h"
#include "Item.h"
#include "ItemType.h"
#include "Port.h"
#include "ShipLayout.h"
#include "Tick.h"

class BeltSystem;

// The building side of material flow, end to end: what a building takes in, what it makes
// of it, and what it puts back out. Its four tick hooks are steps 3 to 5 of the tick order
// (docs/architecture.md), run back to back and in this order --
//
//   tickBeltPull            belt -> building   (REQ-MAT-INPUT-PORTS, REQ-MAT-INPUT-INTAKE)
//   tickProduction          one cycle          (REQ-MAT-CYCLE, REQ-MAT-OUTPUT-GROUP)
//   tickShipyardProduction  one ship           (REQ-BLD-SHIPYARD)
//   tickOutputBelts         building -> belt   (REQ-MAT-OUTPUT-EMERGE, REQ-MAT-OUTPUT-PORT)
//
// -- so that an item arriving this tick is consumable this tick and a produced item starts
// travelling the same tick it appears.
//
// Split out of BuildingSystem, which keeps what buildings *are* -- placement, demolition,
// rotation, recipe and layout configuration. This system keeps what flows through them,
// and with it the three things that flow needs and building topology never did: the RNG an
// output group is drawn with, the unlock test that decides which groups are eligible, and
// the entity model a finished ship is spawned into.
//
// Holds no world data: the factory and the transport layer arrive per tick, as they do for
// ConstructionSystem (FactoryState.h).
class ProductionSystem
{
public:
    // isItemUnlocked decides which output groups a cycle may pick (REQ-LOCK-OUTPUT-POOL);
    // isRecipeUnlocked decides which recipe an auto-recipe building may adopt
    // (REQ-BLD-AUTO-RECIPE). Two questions, asked of the same unlock state.
    ProductionSystem(const GameConfig& config,
                     std::function<void(const std::string&, QVector2D,
                                        const std::optional<ShipLayoutConfig>&)> spawnShip,
                     std::function<bool(const std::string&)> isItemUnlocked,
                     std::function<bool(const std::string&)> isRecipeUnlocked,
                     std::mt19937& rng);

    // Advances every building's virtual input belts, delivers what arrives into the input
    // buffers (into the global block stock for the HQ), and takes what the adjacent real
    // belts offer (REQ-MAT-INPUT-INTAKE, REQ-HQ-BELT-INPUT).
    void tickBeltPull(FactoryState& state, BeltSystem& belts);
    void tickProduction(FactoryState& state, Tick currentTick);
    void tickShipyardProduction(FactoryState& state, Tick currentTick);
    // Advances each building's virtual output belts, hands finished items off onto the
    // adjacent real belt, and feeds new buffered items into them (REQ-MAT-OUTPUT-EMERGE).
    void tickOutputBelts(FactoryState& state, BeltSystem& belts);

private:
    // Selects a recipe for an auto-recipe building that has none, from a material being
    // offered to it at one of its input ports (REQ-BLD-AUTO-RECIPE). No-op for every
    // other building, for one that already holds a recipe, and for a material none of
    // its recipes consumes. Called from both intake paths -- the belt pull and the
    // direct coupling -- since either can be where the first material arrives.
    void                 selectAutoRecipeIfUnset(Building& building,
                                                 const ItemType& offered);
    // True if the consumer would accept `type` at the given input port right now:
    // it is a required input (or a building block for the HQ), the reservation-aware
    // buffer has room, and the input belt entry is free (REQ-MAT-INPUT-INTAKE).
    bool                 canAcceptInput(const Building& consumer,
                                        std::size_t inputPortIndex,
                                        const ItemType& type) const;
    // Places an accepted item onto the consumer's input belt at progress 0.0,
    // reserving a per-material buffer slot (REQ-MAT-INPUT-INTAKE).
    void                 depositToInputBelt(Building& consumer,
                                            std::size_t inputPortIndex,
                                            const Item& item);
    // Attempts to hand an emerging output item straight into a directly adjacent
    // building whose input edge meets the producer's output port (REQ-MAT-DIRECT-COUPLE).
    // Returns true if the item was accepted onto the consumer's input belt.
    bool                 tryDirectCoupleDeposit(FactoryState& state, BuildingId producerId,
                                                const Port& outputPort,
                                                const Item& item);

    // What one cycle of this recipe produces: the items of its one output group
    // (REQ-MAT-OUTPUT-GROUP). Where the recipe has several, one is picked by weight from
    // those currently eligible (REQ-LOCK-OUTPUT-POOL) and the result is empty if none is;
    // where it has one, that group is returned with no draw and no eligibility test.
    std::vector<Item>   rollOutputGroup(const RecipeDef& recipe);

    const GameConfig&                              m_config;
    // Spawning a finished ship reaches into the entity model, and the two unlock
    // questions into the unlock state; neither is factory data, so all three arrive as
    // callbacks rather than living in FactoryState.
    std::function<void(const std::string&, QVector2D,
                       const std::optional<ShipLayoutConfig>&)> m_spawnShip;
    std::function<bool(const std::string&)>        m_isItemUnlocked;
    std::function<bool(const std::string&)>        m_isRecipeUnlocked;
    // The simulation's one RNG, by reference: a draw here shares the stream with every
    // other draw in the run, which is what makes a replay reproducible (docs/replay_design.md).
    std::mt19937&                                  m_rng;
};
