#include "BuildingSystem.h"

#include <algorithm>
#include <cassert>
#include <set>

#include "FactoryQueries.h"
#include "PlacementRules.h"
#include "PortGeometry.h"
#include "SurfaceMask.h"


BuildingSystem::BuildingSystem(const GameConfig& config)
    : m_config(config)
{
}

// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------




// ---------------------------------------------------------------------------
// Placement
// ---------------------------------------------------------------------------

std::optional<BuildingId> BuildingSystem::place(FactoryState& state, BuildingType type, QPoint anchor,
                                Rotation rotation, Tick currentTick)
{
    const BuildingDef* def = m_config.buildings.findBuildingDef(type);
    assert(def != nullptr);
    const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);

    // Reject placements that fall outside the world (REQ-BLD-PLACE-VALID).
    if (!bodyCellsWithinWorldBounds(state, m_config, mask.bodyCells, anchor))
    {
        return std::nullopt;
    }

    const BuildingId id = allocateBuildingId(state);

    // Record tile occupancy for body cells.
    for (const QPoint& cell : mask.bodyCells)
    {
        const QPoint absCell = anchor + cell;
        state.grid.occupy(absCell, id);
    }

    // Build construction site.
    ConstructionSite site;
    site.id       = id;
    site.anchor   = anchor;
    site.footprint = mask.footprint;
    site.rotation = rotation;
    site.type     = type;
    for (const QPoint& cell : mask.bodyCells)
    {
        site.bodyCells.push_back(anchor + cell);
    }

    if (state.constructionQueue.empty())
    {
        site.completesAt = currentTick + secondsToTicks(def->constructionTimeSeconds);
    }
    // else: completesAt remains 0 (queued, not yet started).

    state.constructionQueue.push_back(std::move(site));
    return id;
}

// ---------------------------------------------------------------------------
// Deconstruct
// ---------------------------------------------------------------------------

void BuildingSystem::deconstruct(FactoryState& state, BeltSystem& belts, BuildingId id,
                                 Tick currentTick)
{
    // Construction site? Removed instantly with the full refund; never queued
    // for deconstruction (REQ-BLD-DECONSTRUCT).
    for (std::deque<ConstructionSite>::iterator it = state.constructionQueue.begin();
         it != state.constructionQueue.end();
         ++it)
    {
        if (it->id == id)
        {
            const BuildingDef* def = m_config.buildings.findBuildingDef(it->type);
            state.grid.release(it->bodyCells);
            state.constructionQueue.erase(it);
            if (def)
            {
                state.buildingBlocksStock += def->cost;
            }
            return;
        }
    }

    // Operational building? Append it to the deconstruction queue rather than
    // removing it now; the partial refund is credited on completion in
    // tickDeconstruction (REQ-BLD-DECON-QUEUE).
    for (Building& building : state.buildings)
    {
        if (building.id != id) { continue; }
        if (building.queuedForDeconstruction) { return; }  // already queued

        building.queuedForDeconstruction = true;

        DeconstructionEntry entry;
        entry.id = id;

        // A queued belt/tunnel/splitter stops transporting at once: capture a
        // splitter's filters (so an un-queue can restore them), then unregister
        // its tile, discarding items on it and re-pairing tunnels as if it were
        // gone (REQ-BLD-TUNNEL-PAIR).
        if (building.type == BuildingType::Splitter)
        {
            if (const std::optional<BeltSystem::SplitterInfo> info =
                    belts.getSplitterInfo(building.anchor))
            {
                entry.splitterFilterA = info->filterA;
                entry.splitterFilterB = info->filterB;
            }
        }
        if (isBeltSubsystemType(building.type))
        {
            belts.removeTile(building.anchor);
        }

        const bool wasEmpty = state.deconstructionQueue.empty();
        state.deconstructionQueue.push_back(std::move(entry));
        if (wasEmpty)
        {
            startFrontDeconstruction(state, m_config, currentTick);
        }
        return;
    }
}

// ---------------------------------------------------------------------------
// Set recipe
// ---------------------------------------------------------------------------

void BuildingSystem::setRecipe(FactoryState& state, BuildingId id, const std::string& recipeId)
{
    // Construction site: store recipe for when building completes.
    for (ConstructionSite& site : state.constructionQueue)
    {
        if (site.id == id)
        {
            // No-op if the recipe is unchanged, so a redundant selection does
            // not wipe an already-configured ship layout.
            if (site.recipeId == recipeId)
            {
                return;
            }
            site.recipeId = recipeId;
            site.shipLayout = std::nullopt;
            return;
        }
    }

    // Operational building: clear buffers and re-init.
    for (Building& building : state.buildings)
    {
        if (building.id == id)
        {
            // No-op if the recipe is unchanged, so a redundant selection does
            // not wipe an already-configured ship layout or reset buffers.
            if (building.recipeId == recipeId)
            {
                return;
            }
            building.recipeId = recipeId;
            building.shipLayout = std::nullopt;
            building.inputBuffer.counts.clear();
            building.inputBuffer.caps.clear();
            building.outputBuffer.items.clear();
            building.outputBuffer.caps.clear();
            // Emerging items are part of the output buffer, so clearing it on a
            // recipe change discards them too (REQ-MAT-OUTPUT-EMERGE); in-transit
            // input items are discarded and their reservations released
            // (REQ-MAT-INPUT-INTAKE).
            for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
            for (std::vector<BeltItemSlot>& lane : building.incomingItems) { lane.clear(); }
            building.production = std::nullopt;

            if (!recipeId.empty())
            {
                if (building.type == BuildingType::Shipyard)
                {
                    initShipyardBuffers(m_config, building);
                }
                else
                {
                    const RecipeDef* recipe = m_config.recipes.findRecipeDef(recipeId, building.type);
                    if (recipe)
                    {
                        initBuffers(building, *recipe);
                    }
                }
            }
            return;
        }
    }
}

void BuildingSystem::setShipLayout(FactoryState& state, BuildingId id, const ShipLayoutConfig& layout)
{
    for (ConstructionSite& site : state.constructionQueue)
    {
        if (site.id == id)
        {
            site.shipLayout = layout;
            return;
        }
    }

    for (Building& building : state.buildings)
    {
        if (building.id == id)
        {
            // No-op if the layout is unchanged, so re-applying the layout a shipyard
            // already has does not cancel its production cycle or wipe its buffers
            // (REQ-MAT-INPUT-BUFFER, REQ-BLD-SHIPYARD). Confirming the layout dialog
            // without editing anything, and a blueprint configuration transfer onto an
            // already-matching shipyard (REQ-UI-BLUEPRINT-TRANSFER), both land here.
            // An unset layout counts as an empty one: the two are equivalent for
            // buffers, production, and the spawned ship (see the spawn path below),
            // so an empty layout arriving at an unconfigured shipyard changes nothing.
            const bool unchanged = building.shipLayout.has_value()
                                       ? *building.shipLayout == layout
                                       : layout.placedModules.empty();
            if (unchanged)
            {
                return;
            }
            if (building.production.has_value())
            {
                building.production = std::nullopt;
            }
            building.shipLayout = layout;
            building.inputBuffer.counts.clear();
            building.inputBuffer.caps.clear();
            building.outputBuffer.items.clear();
            building.outputBuffer.caps.clear();
            for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
            for (std::vector<BeltItemSlot>& lane : building.incomingItems) { lane.clear(); }
            if (!building.recipeId.empty() && building.type == BuildingType::Shipyard)
            {
                initShipyardBuffers(m_config, building);
            }
            return;
        }
    }
}

void BuildingSystem::setSiteSplitterFilters(FactoryState& state, BuildingId id,
                                            const std::vector<ItemType>& filterA,
                                            const std::vector<ItemType>& filterB)
{
    for (ConstructionSite& site : state.constructionQueue)
    {
        if (site.id == id && site.type == BuildingType::Splitter)
        {
            site.splitterFilterA = filterA;
            site.splitterFilterB = filterB;
            return;
        }
    }
}

// ---------------------------------------------------------------------------
// Tick hooks
// ---------------------------------------------------------------------------

void BuildingSystem::cancelDeconstruction(FactoryState& state, BeltSystem& belts,
                                          BuildingId id)
{
    for (std::deque<DeconstructionEntry>::iterator it = state.deconstructionQueue.begin();
         it != state.deconstructionQueue.end();
         ++it)
    {
        if (it->id != id) { continue; }

        // Resume operation: clear the flag and re-register belt/tunnel/splitter
        // tiles that were unregistered at enqueue (which re-pairs tunnels,
        // REQ-BLD-TUNNEL-PAIR). Deconstruction progress is discarded; no refund.
        if (Building* building = findBuilding(state, id))
        {
            building->queuedForDeconstruction = false;
            reregisterBeltTile(belts, m_config, *building, it->splitterFilterA, it->splitterFilterB);
        }

        state.deconstructionQueue.erase(it);
        // If the running front was removed, the new front (completesAt == 0) has
        // its timer started by the next tickDeconstruction guard.
        return;
    }
}

void BuildingSystem::rotateInPlace(FactoryState& state, BeltSystem& belts, BuildingId id,
                                   Rotation newRotation)
{
    // Construction site path — just update rotation; no ports to recompute.
    for (ConstructionSite& site : state.constructionQueue)
    {
        if (site.id == id)
        {
            site.rotation = newRotation;
            return;
        }
    }

    // Operational building path.
    for (Building& b : state.buildings)
    {
        if (b.id != id) { continue; }

        b.rotation = newRotation;

        const BuildingDef* def = m_config.buildings.findBuildingDef(b.type);
        if (!def) { return; }
        const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, newRotation);

        b.outputPorts.clear();
        for (const Port& port : mask.outputPorts)
        {
            Port absPort;
            absPort.tile      = b.anchor + port.tile;
            absPort.direction = port.direction;
            b.outputPorts.push_back(absPort);
        }
        // The output ports moved; discard any in-flight emerging items and re-size
        // the lanes to the new port set (REQ-MAT-OUTPUT-EMERGE).
        b.emergingItems.clear();
        b.emergingItems.resize(b.outputPorts.size());
        b.inputPorts = computeInputPorts(b.bodyCells, b.outputPorts);
        // Likewise discard in-transit input items and re-size the input belts to
        // the new port set (REQ-MAT-INPUT-INTAKE).
        b.incomingItems.assign(b.inputPorts.size(), {});

        // Re-register with BeltSystem (items on tile are discarded). A splitter's
        // filters live in BeltSystem and would be lost by removeTile, so capture
        // them first and hand them back to reregisterBeltTile (REQ-BLD-SPLITTER).
        if (isBeltSubsystemType(b.type))
        {
            std::vector<ItemType> splitterFilterA;
            std::vector<ItemType> splitterFilterB;
            if (b.type == BuildingType::Splitter)
            {
                if (const std::optional<BeltSystem::SplitterInfo> info =
                        belts.getSplitterInfo(b.anchor))
                {
                    splitterFilterA = info->filterA;
                    splitterFilterB = info->filterB;
                }
            }

            belts.removeTile(b.anchor);
            reregisterBeltTile(belts, m_config, b, splitterFilterA, splitterFilterB);
        }

        return;
    }
}

BuildingId BuildingSystem::placeImmediate(FactoryState& state, BuildingType type,
                                         const std::vector<std::string>& surfaceMask,
                                         QPoint anchor, Rotation rotation)
{
    const BuildingId id = allocateBuildingId(state);
    const ParsedSurfaceMask mask = parseSurfaceMask(surfaceMask, rotation);

    Building building;
    building.id        = id;
    building.anchor    = anchor;
    building.footprint = mask.footprint;
    building.rotation  = rotation;
    building.type      = type;

    for (const QPoint& cell : mask.bodyCells)
    {
        const QPoint absCell = anchor + cell;
        building.bodyCells.push_back(absCell);
        state.grid.occupy(absCell, id);
    }
    for (const Port& port : mask.outputPorts)
    {
        Port absPort;
        absPort.tile      = anchor + port.tile;
        absPort.direction = port.direction;
        building.outputPorts.push_back(absPort);
    }
    building.emergingItems.resize(building.outputPorts.size());
    building.inputPorts = computeInputPorts(building.bodyCells, building.outputPorts);
    building.incomingItems.assign(building.inputPorts.size(), {});

    if (type == BuildingType::SalvageBay)
    {
        initSalvageBayBuffer(m_config, building);
    }

    state.buildings.push_back(std::move(building));
    return id;
}

void BuildingSystem::forEachBuilding(FactoryState& state, std::function<void(Building&)> fn)
{
    for (Building& b : state.buildings)
    {
        fn(b);
    }
}

void BuildingSystem::registerTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells,
                                            BuildingId ownerPlaceholder)
{
    state.grid.occupy(cells, ownerPlaceholder);
}

void BuildingSystem::unregisterTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells)
{
    state.grid.release(cells);
}
