#include "BeltSystem.h"

#include <algorithm>

#include "StateChecksum.h"
#include "Tick.h"
#include "TunnelCompletion.h"
#include "tracing.h"

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

std::pair<int, int> BeltSystem::key(QPoint tile)
{
    return {tile.x(), tile.y()};
}

QPoint BeltSystem::adjacentTile(QPoint tile, Rotation dir)
{
    switch (dir)
    {
        case Rotation::North: return {tile.x(),     tile.y() - 1};
        case Rotation::East:  return {tile.x() + 1, tile.y()    };
        case Rotation::South: return {tile.x(),     tile.y() + 1};
        case Rotation::West:  return {tile.x() - 1, tile.y()    };
    }
    return tile;
}

Rotation BeltSystem::oppositeRotation(Rotation dir)
{
    switch (dir)
    {
        case Rotation::North: return Rotation::South;
        case Rotation::East:  return Rotation::West;
        case Rotation::South: return Rotation::North;
        case Rotation::West:  return Rotation::East;
    }
    return dir;
}

bool BeltSystem::entersThroughOutputEdge(QPoint tile, Rotation travelDir) const
{
    // An item travelling in travelDir crosses into the tile through the edge
    // opposite that direction.  If that entry edge is one of the tile's output
    // edges, the tile must refuse the item (REQ-MAT-ACCEPT-DIR).
    const Rotation entryEdge = oppositeRotation(travelDir);

    const std::map<std::pair<int, int>, BeltTile>::const_iterator beltIt =
        m_belts.find(key(tile));
    if (beltIt != m_belts.end())
    {
        return entryEdge == beltIt->second.direction;
    }

    const std::map<std::pair<int, int>, SplitterTile>::const_iterator splIt =
        m_splitters.find(key(tile));
    if (splIt != m_splitters.end())
    {
        return entryEdge == splIt->second.outputA || entryEdge == splIt->second.outputB;
    }

    const std::map<std::pair<int, int>, TunnelEntryTile>::const_iterator teIt =
        m_tunnelEntries.find(key(tile));
    if (teIt != m_tunnelEntries.end())
    {
        return entryEdge == teIt->second.direction;
    }

    const std::map<std::pair<int, int>, TunnelExitTile>::const_iterator txIt =
        m_tunnelExits.find(key(tile));
    if (txIt != m_tunnelExits.end())
    {
        return entryEdge == txIt->second.direction;
    }

    return false;
}

// ---------------------------------------------------------------------------
// Construction / placement
// ---------------------------------------------------------------------------

BeltSystem::BeltSystem(double beltSpeed_tps)
    : m_progressPerTick_tpt(beltSpeed_tps * kTickDurationSeconds)
{
}

void BeltSystem::placeBelt(QPoint tile, Rotation direction)
{
    m_splitters.erase(key(tile));
    BeltTile bt;
    bt.direction = direction;
    m_belts[key(tile)] = bt;
}

void BeltSystem::placeSplitter(QPoint tile, Rotation outputA, Rotation outputB)
{
    m_belts.erase(key(tile));
    SplitterTile st;
    st.outputA       = outputA;
    st.outputB       = outputB;
    st.nextOutputIsA = true;
    m_splitters[key(tile)] = st;
}

void BeltSystem::placeTunnelEntry(QPoint tile, Rotation direction, int maxDistance)
{
    m_belts.erase(key(tile));
    m_splitters.erase(key(tile));
    m_tunnelExits.erase(key(tile));
    TunnelEntryTile te;
    te.direction   = direction;
    te.maxDistance  = maxDistance;
    m_tunnelEntries[key(tile)] = te;
    reevaluateTunnelPairing();
}

void BeltSystem::placeTunnelExit(QPoint tile, Rotation direction)
{
    m_belts.erase(key(tile));
    m_splitters.erase(key(tile));
    m_tunnelEntries.erase(key(tile));
    TunnelExitTile tx;
    tx.direction = direction;
    m_tunnelExits[key(tile)] = tx;
    reevaluateTunnelPairing();
}

void BeltSystem::removeTile(QPoint tile)
{
    const bool wasTunnel = (m_tunnelEntries.erase(key(tile)) > 0)
                         | (m_tunnelExits.erase(key(tile)) > 0);
    m_belts.erase(key(tile));
    m_splitters.erase(key(tile));
    if (wasTunnel)
    {
        reevaluateTunnelPairing();
    }
}

void BeltSystem::setSplitterFilters(QPoint tile,
                                    const std::vector<ItemType>& filterA,
                                    const std::vector<ItemType>& filterB)
{
    const std::map<std::pair<int, int>, SplitterTile>::iterator it = m_splitters.find(key(tile));
    if (it == m_splitters.end())
    {
        return;
    }
    it->second.filterA = filterA;
    it->second.filterB = filterB;
}

std::optional<BeltSystem::SplitterInfo> BeltSystem::getSplitterInfo(QPoint tile) const
{
    const std::map<std::pair<int, int>, SplitterTile>::const_iterator it =
        m_splitters.find(key(tile));
    if (it == m_splitters.end())
    {
        return std::nullopt;
    }
    return SplitterInfo{
        it->second.outputA,
        it->second.outputB,
        it->second.filterA,
        it->second.filterB
    };
}

// ---------------------------------------------------------------------------
// Tunnel pairing
// ---------------------------------------------------------------------------

void BeltSystem::reevaluateTunnelPairing()
{
    std::vector<TunnelLink> oldLinks;
    std::swap(oldLinks, m_tunnelLinks);

    // Tunnel index over this system's own (completed) tunnel tiles, shared with the
    // scan primitive so the pairing rule lives in one place (REQ-BLD-TUNNEL-PAIR).
    const TunnelLookup lookup = [this](QPoint tile) -> std::optional<TunnelTileInfo>
    {
        const std::map<std::pair<int, int>, TunnelEntryTile>::const_iterator teIt =
            m_tunnelEntries.find(key(tile));
        if (teIt != m_tunnelEntries.end())
        {
            return TunnelTileInfo{BuildingType::TunnelEntry, teIt->second.direction};
        }
        const std::map<std::pair<int, int>, TunnelExitTile>::const_iterator txIt =
            m_tunnelExits.find(key(tile));
        if (txIt != m_tunnelExits.end())
        {
            return TunnelTileInfo{BuildingType::TunnelExit, txIt->second.direction};
        }
        return std::nullopt;
    };

    for (const std::pair<const std::pair<int, int>, TunnelEntryTile>& entry : m_tunnelEntries)
    {
        const QPoint entryPos(entry.first.first, entry.first.second);
        const Rotation dir = entry.second.direction;
        const int maxDist  = entry.second.maxDistance;

        // The first same-direction tunnel ahead forms a pair only when it is an exit;
        // a same-direction entry blocks (firstTunnelFacing stops at it either way).
        const std::optional<QPoint> target =
            firstTunnelFacing(lookup, entryPos, dir, dir, maxDist);
        if (!target.has_value() || m_tunnelExits.find(key(*target)) == m_tunnelExits.end())
        {
            continue;
        }

        TunnelLink link;
        link.entryTile = entryPos;
        link.exitTile  = *target;
        // The exit is colinear with the entry along `dir`, so the tile-coordinate
        // distance is the Manhattan distance.
        link.length = static_cast<double>((*target - entryPos).manhattanLength());

        for (const TunnelLink& old : oldLinks)
        {
            if (old.entryTile == entryPos && old.exitTile == *target)
            {
                link.items = old.items;
                break;
            }
        }

        m_tunnelLinks.push_back(std::move(link));
    }
}

// ---------------------------------------------------------------------------
// Port interface
// ---------------------------------------------------------------------------

bool BeltSystem::tryPutItem(QPoint tile, Item item, Rotation fromDir)
{
    // Refuse items that would enter through the tile's output edge (REQ-MAT-ACCEPT-DIR).
    if (entersThroughOutputEdge(tile, fromDir))
    {
        return false;
    }

    const std::map<std::pair<int, int>, BeltTile>::iterator bIt = m_belts.find(key(tile));
    if (bIt != m_belts.end())
    {
        return tryPlaceOnBelt(tile, item);
    }

    const std::map<std::pair<int, int>, SplitterTile>::iterator splIt =
        m_splitters.find(key(tile));
    if (splIt != m_splitters.end())
    {
        if (splIt->second.back.size() < 2)
        {
            splIt->second.back.push_back(BeltItemSlot{item, 0.0});
            splIt->second.backDir.push_back(fromDir);
            return true;
        }
        return false;
    }

    const std::map<std::pair<int, int>, TunnelEntryTile>::iterator teIt =
        m_tunnelEntries.find(key(tile));
    if (teIt != m_tunnelEntries.end())
    {
        if (teIt->second.itemSlots.size() < 4)
        {
            teIt->second.itemSlots.push_back(BeltItemSlot{item, 0.0});
            return true;
        }
        return false;
    }

    return false;
}

std::optional<Item> BeltSystem::tryTakeItem(Port port)
{
    const std::map<std::pair<int, int>, BeltTile>::iterator beltIt = m_belts.find(key(port.tile));
    if (beltIt != m_belts.end())
    {
        if (beltIt->second.direction != port.direction)
        {
            return std::nullopt;
        }
        BeltTile& bt = beltIt->second;
        if (!bt.itemSlots.empty() && bt.itemSlots.front().progress >= 1.0)
        {
            const Item taken = bt.itemSlots.front().item;
            bt.itemSlots.erase(bt.itemSlots.begin());
            return taken;
        }
        return std::nullopt;
    }

    const std::map<std::pair<int, int>, SplitterTile>::iterator splIt =
        m_splitters.find(key(port.tile));
    if (splIt != m_splitters.end())
    {
        SplitterTile& st = splIt->second;
        if (port.direction == st.outputA && st.frontA && st.frontA->progress >= 1.0)
        {
            const Item taken = st.frontA->item;
            st.frontA = std::nullopt;
            return taken;
        }
        if (port.direction == st.outputB && st.frontB && st.frontB->progress >= 1.0)
        {
            const Item taken = st.frontB->item;
            st.frontB = std::nullopt;
            return taken;
        }
    }

    const std::map<std::pair<int, int>, TunnelExitTile>::iterator txIt =
        m_tunnelExits.find(key(port.tile));
    if (txIt != m_tunnelExits.end())
    {
        TunnelExitTile& tx = txIt->second;
        if (tx.direction == port.direction && !tx.itemSlots.empty()
            && tx.itemSlots.front().progress >= 1.0)
        {
            const Item taken = tx.itemSlots.front().item;
            tx.itemSlots.erase(tx.itemSlots.begin());
            return taken;
        }
    }

    return std::nullopt;
}

std::optional<ItemType> BeltSystem::peekItem(Port port) const
{
    const std::map<std::pair<int, int>, BeltTile>::const_iterator beltIt =
        m_belts.find(key(port.tile));
    if (beltIt != m_belts.end())
    {
        if (beltIt->second.direction != port.direction)
        {
            return std::nullopt;
        }
        const BeltTile& bt = beltIt->second;
        if (!bt.itemSlots.empty() && bt.itemSlots.front().progress >= 1.0)
        {
            return bt.itemSlots.front().item.type;
        }
        return std::nullopt;
    }

    const std::map<std::pair<int, int>, SplitterTile>::const_iterator splIt =
        m_splitters.find(key(port.tile));
    if (splIt != m_splitters.end())
    {
        const SplitterTile& st = splIt->second;
        if (port.direction == st.outputA && st.frontA && st.frontA->progress >= 1.0)
        {
            return st.frontA->item.type;
        }
        if (port.direction == st.outputB && st.frontB && st.frontB->progress >= 1.0)
        {
            return st.frontB->item.type;
        }
    }

    const std::map<std::pair<int, int>, TunnelExitTile>::const_iterator txIt =
        m_tunnelExits.find(key(port.tile));
    if (txIt != m_tunnelExits.end())
    {
        const TunnelExitTile& tx = txIt->second;
        if (tx.direction == port.direction && !tx.itemSlots.empty()
            && tx.itemSlots.front().progress >= 1.0)
        {
            return tx.itemSlots.front().item.type;
        }
    }

    return std::nullopt;
}

// ---------------------------------------------------------------------------
// Maintenance
// ---------------------------------------------------------------------------

void BeltSystem::clearTiles(const std::vector<QPoint>& tiles)
{
    for (const QPoint& tile : tiles)
    {
        const std::map<std::pair<int, int>, BeltTile>::iterator bIt = m_belts.find(key(tile));
        if (bIt != m_belts.end())
        {
            bIt->second.itemSlots.clear();
        }

        const std::map<std::pair<int, int>, SplitterTile>::iterator sIt = m_splitters.find(key(tile));
        if (sIt != m_splitters.end())
        {
            sIt->second.back.clear();
            sIt->second.backDir.clear();
            sIt->second.frontA = std::nullopt;
            sIt->second.frontB = std::nullopt;
        }

        const std::map<std::pair<int, int>, TunnelEntryTile>::iterator teIt =
            m_tunnelEntries.find(key(tile));
        if (teIt != m_tunnelEntries.end())
        {
            // Only what sits on the entry itself: the items already inside the tunnel
            // are counted on its exit (REQ-UI-BELT-ITEMS) and are cleared from there, so
            // a clear never removes more than the panel showed for the tile it acted on
            // (REQ-BLD-TUNNEL-TRANSIT). Emptying a tunnel is done from its exit.
            teIt->second.itemSlots.clear();
        }

        const std::map<std::pair<int, int>, TunnelExitTile>::iterator txIt =
            m_tunnelExits.find(key(tile));
        if (txIt != m_tunnelExits.end())
        {
            txIt->second.itemSlots.clear();
            for (TunnelLink& link : m_tunnelLinks)
            {
                if (link.exitTile == tile)
                {
                    link.items.clear();
                }
            }
        }
    }
}

std::map<ItemType, int> BeltSystem::countItems(const std::vector<QPoint>& tiles) const
{
    std::map<ItemType, int> counts;

    // The same five containers clearTiles walks, in the same order, so that what the
    // panel lists and what the button removes cannot drift apart (REQ-UI-BELT-ITEMS,
    // REQ-UI-BELT-CLEAR).
    for (const QPoint& tile : tiles)
    {
        const std::map<std::pair<int, int>, BeltTile>::const_iterator bIt =
            m_belts.find(key(tile));
        if (bIt != m_belts.end())
        {
            for (const BeltItemSlot& slot : bIt->second.itemSlots)
            {
                counts[slot.item.type]++;
            }
        }

        const std::map<std::pair<int, int>, SplitterTile>::const_iterator sIt =
            m_splitters.find(key(tile));
        if (sIt != m_splitters.end())
        {
            const SplitterTile& splitter = sIt->second;
            for (const BeltItemSlot& slot : splitter.back)
            {
                counts[slot.item.type]++;
            }
            if (splitter.frontA)
            {
                counts[splitter.frontA->item.type]++;
            }
            if (splitter.frontB)
            {
                counts[splitter.frontB->item.type]++;
            }
        }

        const std::map<std::pair<int, int>, TunnelEntryTile>::const_iterator teIt =
            m_tunnelEntries.find(key(tile));
        if (teIt != m_tunnelEntries.end())
        {
            for (const BeltItemSlot& slot : teIt->second.itemSlots)
            {
                counts[slot.item.type]++;
            }
        }

        const std::map<std::pair<int, int>, TunnelExitTile>::const_iterator txIt =
            m_tunnelExits.find(key(tile));
        if (txIt != m_tunnelExits.end())
        {
            for (const BeltItemSlot& slot : txIt->second.itemSlots)
            {
                counts[slot.item.type]++;
            }

            // The tunnel's invisible cargo, counted on the end it is travelling toward.
            // This is the only place the player can see it at all, the tunnel drawing
            // nothing of what is inside it (REQ-BLD-TUNNEL-TRANSIT).
            for (const TunnelLink& link : m_tunnelLinks)
            {
                if (link.exitTile != tile)
                {
                    continue;
                }
                for (const TunnelTransitItem& transit : link.items)
                {
                    counts[transit.item.type]++;
                }
            }
        }
    }

    return counts;
}

// ---------------------------------------------------------------------------
// Tick
// ---------------------------------------------------------------------------

void BeltSystem::tick()
{
	TRACE();
    advanceProgress();
    advanceTunnelProgress();
    moveItemsToNextTile();
    moveTunnelItems();
    routeSplitterItems();
}

void BeltSystem::advanceProgress()
{
    for (std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.begin();
         it != m_belts.end(); ++it)
    {
        advanceBeltSlots(it->second.itemSlots, m_progressPerTick_tpt);
    }

    for (std::map<std::pair<int, int>, SplitterTile>::iterator it = m_splitters.begin();
         it != m_splitters.end(); ++it)
    {
        SplitterTile& st = it->second;

        for (std::size_t i = 0; i < st.back.size(); ++i)
        {
            st.back[i].progress += m_progressPerTick_tpt;
            const double absoluteCap = 0.5 - i * 0.25;
            if (st.back[i].progress > absoluteCap)
            {
                st.back[i].progress = absoluteCap;
            }

            if (i > 0)
            {
                const double gapCap = st.back[i - 1].progress - 0.25;
                if (gapCap < 0.0)
                {
                    st.back[i].progress = 0.0;
                }
                else if (st.back[i].progress > gapCap)
                {
                    st.back[i].progress = gapCap;
                }
            }
        }

        if (st.frontA)
        {
            st.frontA->progress += m_progressPerTick_tpt;
            if (st.frontA->progress > 1.0)
            {
                st.frontA->progress = 1.0;
            }
        }

        if (st.frontB)
        {
            st.frontB->progress += m_progressPerTick_tpt;
            if (st.frontB->progress > 1.0)
            {
                st.frontB->progress = 1.0;
            }
        }
    }
}

void BeltSystem::advanceTunnelProgress()
{
    for (std::map<std::pair<int, int>, TunnelEntryTile>::iterator it = m_tunnelEntries.begin();
         it != m_tunnelEntries.end(); ++it)
    {
        advanceBeltSlots(it->second.itemSlots, m_progressPerTick_tpt);
    }

    for (std::map<std::pair<int, int>, TunnelExitTile>::iterator it = m_tunnelExits.begin();
         it != m_tunnelExits.end(); ++it)
    {
        advanceBeltSlots(it->second.itemSlots, m_progressPerTick_tpt);
    }

    for (TunnelLink& link : m_tunnelLinks)
    {
        for (std::size_t i = 0; i < link.items.size(); ++i)
        {
            TunnelTransitItem& ti = link.items[i];
            ti.progress += m_progressPerTick_tpt;
            if (ti.progress > link.length)
            {
                ti.progress = link.length;
            }
            if (i > 0)
            {
                const double maxProgress = link.items[i - 1].progress - 0.25;
                if (ti.progress > maxProgress)
                {
                    ti.progress = maxProgress;
                    if (ti.progress < 0.0)
                    {
                        ti.progress = 0.0;
                    }
                }
            }
        }
    }
}

void BeltSystem::moveItemsToNextTile()
{
    // Belt items advancing into the next tile.
    for (std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.begin();
         it != m_belts.end(); ++it)
    {
        BeltTile& bt = it->second;
        if (bt.itemSlots.empty() || bt.itemSlots.front().progress < 1.0)
        {
            continue;
        }

        const QPoint here  = QPoint(it->first.first, it->first.second);
        const QPoint next  = adjacentTile(here, bt.direction);

        // Refuse to hand off into a downstream tile's output edge (REQ-MAT-ACCEPT-DIR);
        // the item stays blocked at progress 1.0.
        if (entersThroughOutputEdge(next, bt.direction))
        {
            continue;
        }

        const std::map<std::pair<int, int>, BeltTile>::iterator nextBelt = m_belts.find(key(next));
        const std::map<std::pair<int, int>, SplitterTile>::iterator nextSplitter = m_splitters.find(key(next));

        if (nextBelt != m_belts.end())
        {
            if (tryPlaceOnBelt(next, bt.itemSlots.front().item))
            {
                bt.itemSlots.erase(bt.itemSlots.begin());
            }
            // else: next belt is full — item stays blocked at progress 1.0.
        }
        else if (nextSplitter != m_splitters.end())
        {
            if (nextSplitter->second.back.size() < 2)
            {
                nextSplitter->second.back.push_back(BeltItemSlot{bt.itemSlots.front().item, 0.0});
                nextSplitter->second.backDir.push_back(bt.direction);
                bt.itemSlots.erase(bt.itemSlots.begin());
            }
        }
        else
        {
            const std::map<std::pair<int, int>, TunnelEntryTile>::iterator nextEntry =
                m_tunnelEntries.find(key(next));
            if (nextEntry != m_tunnelEntries.end()
                && nextEntry->second.itemSlots.size() < 4)
            {
                nextEntry->second.itemSlots.push_back(
                    BeltItemSlot{bt.itemSlots.front().item, 0.0});
                bt.itemSlots.erase(bt.itemSlots.begin());
            }
        }
    }

    // Splitter front slots advancing into downstream belt tiles.
    for (std::map<std::pair<int, int>, SplitterTile>::iterator it = m_splitters.begin();
         it != m_splitters.end(); ++it)
    {
        SplitterTile& st = it->second;
        const QPoint here = QPoint(it->first.first, it->first.second);

        if (st.frontA && st.frontA->progress >= 1.0)
        {
            const QPoint dest = adjacentTile(here, st.outputA);
            if (tryPushToTile(dest, st.frontA->item, st.outputA))
            {
                st.frontA = std::nullopt;
            }
        }

        if (st.frontB && st.frontB->progress >= 1.0)
        {
            const QPoint dest = adjacentTile(here, st.outputB);
            if (tryPushToTile(dest, st.frontB->item, st.outputB))
            {
                st.frontB = std::nullopt;
            }
        }
    }

    // Tunnel exit items advancing into downstream tiles.
    for (std::map<std::pair<int, int>, TunnelExitTile>::iterator it = m_tunnelExits.begin();
         it != m_tunnelExits.end(); ++it)
    {
        TunnelExitTile& tx = it->second;
        if (tx.itemSlots.empty() || tx.itemSlots.front().progress < 1.0)
        {
            continue;
        }

        const QPoint here = QPoint(it->first.first, it->first.second);
        const QPoint next = adjacentTile(here, tx.direction);
        if (tryPushToTile(next, tx.itemSlots.front().item, tx.direction))
        {
            tx.itemSlots.erase(tx.itemSlots.begin());
        }
    }
}

void BeltSystem::moveTunnelItems()
{
    for (TunnelLink& link : m_tunnelLinks)
    {
        // Entry front → transit
        const std::map<std::pair<int, int>, TunnelEntryTile>::iterator teIt =
            m_tunnelEntries.find(key(link.entryTile));
        if (teIt != m_tunnelEntries.end())
        {
            TunnelEntryTile& te = teIt->second;
            if (!te.itemSlots.empty() && te.itemSlots.front().progress >= 1.0)
            {
                const bool canEnter = link.items.empty()
                    || link.items.back().progress >= 0.25;
                if (canEnter)
                {
                    TunnelTransitItem ti;
                    ti.item     = te.itemSlots.front().item;
                    ti.progress = 0.0;
                    link.items.push_back(ti);
                    te.itemSlots.erase(te.itemSlots.begin());
                }
            }
        }

        // Transit front → exit
        if (!link.items.empty() && link.items.front().progress >= link.length)
        {
            const std::map<std::pair<int, int>, TunnelExitTile>::iterator txIt =
                m_tunnelExits.find(key(link.exitTile));
            if (txIt != m_tunnelExits.end())
            {
                TunnelExitTile& tx = txIt->second;
                if (tx.itemSlots.size() < 4)
                {
                    tx.itemSlots.push_back(BeltItemSlot{link.items.front().item, 0.0});
                    link.items.erase(link.items.begin());
                }
            }
        }
    }
}

void BeltSystem::routeSplitterItems()
{
    for (std::map<std::pair<int, int>, SplitterTile>::iterator it = m_splitters.begin();
         it != m_splitters.end(); ++it)
    {
        SplitterTile& st = it->second;
        if (st.back.empty() || st.back.front().progress < 0.5)
        {
            continue;
        }

        const Item& item = st.back.front().item;

        const bool matchesA = st.filterA.empty() ||
            std::find(st.filterA.begin(), st.filterA.end(), item.type) != st.filterA.end();
        const bool matchesB = st.filterB.empty() ||
            std::find(st.filterB.begin(), st.filterB.end(), item.type) != st.filterB.end();

        bool routed = false;

        // A front slot holds only one item, so an item entering at progress 0.0
        // would have to traverse the whole tile before the next could enter,
        // throttling that output below belt speed and leaving large gaps. Entering
        // near the output edge lets the slot clear roughly every quarter tile, so
        // the output stays packed (fixes the half-blocked / single-output gap bug).
        constexpr double frontEntryProgress = 0.75;

        if (matchesA && !matchesB)
        {
            if (!st.frontA)
            {
                st.frontA = BeltItemSlot{item, frontEntryProgress};
                routed = true;
            }
        }
        else if (matchesB && !matchesA)
        {
            if (!st.frontB)
            {
                st.frontB = BeltItemSlot{item, frontEntryProgress};
                routed = true;
            }
        }
        else if (matchesA && matchesB)
        {
            // Alternation: try preferred output first, fall back to other if preferred full.
            const bool preferA = st.nextOutputIsA;

            if (preferA && !st.frontA)
            {
                st.frontA        = BeltItemSlot{item, frontEntryProgress};
                st.nextOutputIsA = false;
                routed = true;
            }
            else if (!preferA && !st.frontB)
            {
                st.frontB        = BeltItemSlot{item, frontEntryProgress};
                st.nextOutputIsA = true;
                routed = true;
            }
            else if (preferA && !st.frontB)
            {
                // Preferred (A) is full — fall back to B; nextOutputIsA stays.
                st.frontB = BeltItemSlot{item, frontEntryProgress};
                routed = true;
            }
            else if (!preferA && !st.frontA)
            {
                // Preferred (B) is full — fall back to A; nextOutputIsA stays.
                st.frontA = BeltItemSlot{item, frontEntryProgress};
                routed = true;
            }
            // else both fronts occupied — back stays.
        }
        // else (!matchesA && !matchesB): stall — back stays.

        if (routed)
        {
            st.back.erase(st.back.begin());
            st.backDir.erase(st.backDir.begin());
        }
    }
}

bool BeltSystem::tryPlaceOnBelt(QPoint tile, Item item)
{
    const std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.find(key(tile));
    if (it == m_belts.end())
    {
        return false;
    }

    BeltTile& bt = it->second;
    if (bt.itemSlots.size() < 4)
    {
        bt.itemSlots.push_back(BeltItemSlot{item, 0.0});
        return true;
    }
    return false;  // all slots occupied
}

bool BeltSystem::tryPushToTile(QPoint dest, Item item, Rotation fromDir)
{
    // Refuse items that would enter through the tile's output edge (REQ-MAT-ACCEPT-DIR).
    if (entersThroughOutputEdge(dest, fromDir))
    {
        return false;
    }

    if (tryPlaceOnBelt(dest, item))
    {
        return true;
    }

    const std::map<std::pair<int, int>, SplitterTile>::iterator splIt =
        m_splitters.find(key(dest));
    if (splIt != m_splitters.end())
    {
        if (splIt->second.back.size() < 2)
        {
            splIt->second.back.push_back(BeltItemSlot{item, 0.0});
            splIt->second.backDir.push_back(fromDir);
            return true;
        }
        return false;
    }

    const std::map<std::pair<int, int>, TunnelEntryTile>::iterator teIt =
        m_tunnelEntries.find(key(dest));
    if (teIt != m_tunnelEntries.end())
    {
        if (teIt->second.itemSlots.size() < 4)
        {
            teIt->second.itemSlots.push_back(BeltItemSlot{item, 0.0});
            return true;
        }
        return false;
    }

    const std::map<std::pair<int, int>, TunnelExitTile>::iterator txIt =
        m_tunnelExits.find(key(dest));
    if (txIt != m_tunnelExits.end())
    {
        if (txIt->second.itemSlots.size() < 4)
        {
            txIt->second.itemSlots.push_back(BeltItemSlot{item, 0.0});
            return true;
        }
        return false;
    }

    return false;
}

// ---------------------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------------------

void BeltSystem::forEachVisualItem(QRect viewportTiles,
                                   std::function<void(VisualItem)> visit) const
{
    for (const std::pair<const std::pair<int, int>, BeltTile>& entry : m_belts)
    {
        const QPoint tile(entry.first.first, entry.first.second);
        if (!viewportTiles.contains(tile))
        {
            continue;
        }

        const BeltTile& bt = entry.second;

        // Render least-progressed first (bottom) → most-progressed last (top).
        for (int i = static_cast<int>(bt.itemSlots.size()) - 1; i >= 0; --i)
        {
            VisualItem vi;
            vi.type     = bt.itemSlots[i].item.type;
            vi.worldPos = beltSlotWorldPos(tile, bt.direction, bt.itemSlots[i].progress);
            visit(vi);
        }
    }

    for (const std::pair<const std::pair<int, int>, SplitterTile>& entry : m_splitters)
    {
        const QPoint tile(entry.first.first, entry.first.second);
        if (!viewportTiles.contains(tile))
        {
            continue;
        }

        const SplitterTile& st = entry.second;

        // Unassigned items: least-progressed first (bottom), then higher-progressed (top).
        for (int i = static_cast<int>(st.back.size()) - 1; i >= 0; --i)
        {
            VisualItem vi;
            vi.type     = st.back[i].item.type;
            vi.worldPos = beltSlotWorldPos(tile, st.backDir[i], st.back[i].progress);
            visit(vi);
        }

        // Output-slot items rendered on top of unassigned, in clockwise order from East.
        // East=0, South=1, West=2, North=3 — lower rank rendered first (bottom).
        auto clockwiseRank = [](Rotation r) -> int
        {
            switch (r)
            {
                case Rotation::East:  return 0;
                case Rotation::South: return 1;
                case Rotation::West:  return 2;
                case Rotation::North: return 3;
            }
            return 0;
        };

        const bool aBeforeB = clockwiseRank(st.outputA) <= clockwiseRank(st.outputB);

        auto renderFront = [&](const std::optional<BeltItemSlot>& slot, Rotation dir)
        {
            if (slot)
            {
                VisualItem vi;
                vi.type     = slot->item.type;
                vi.worldPos = beltSlotWorldPos(tile, dir, slot->progress);
                visit(vi);
            }
        };

        if (aBeforeB)
        {
            renderFront(st.frontA, st.outputA);
            renderFront(st.frontB, st.outputB);
        }
        else
        {
            renderFront(st.frontB, st.outputB);
            renderFront(st.frontA, st.outputA);
        }
    }

    for (const std::pair<const std::pair<int, int>, TunnelEntryTile>& entry : m_tunnelEntries)
    {
        const QPoint tile(entry.first.first, entry.first.second);
        if (!viewportTiles.contains(tile))
        {
            continue;
        }

        const TunnelEntryTile& te = entry.second;
        for (int i = static_cast<int>(te.itemSlots.size()) - 1; i >= 0; --i)
        {
            VisualItem vi;
            vi.type     = te.itemSlots[i].item.type;
            vi.worldPos = beltSlotWorldPos(tile, te.direction, te.itemSlots[i].progress);
            visit(vi);
        }
    }

    for (const std::pair<const std::pair<int, int>, TunnelExitTile>& entry : m_tunnelExits)
    {
        const QPoint tile(entry.first.first, entry.first.second);
        if (!viewportTiles.contains(tile))
        {
            continue;
        }

        const TunnelExitTile& tx = entry.second;
        for (int i = static_cast<int>(tx.itemSlots.size()) - 1; i >= 0; --i)
        {
            VisualItem vi;
            vi.type     = tx.itemSlots[i].item.type;
            vi.worldPos = beltSlotWorldPos(tile, tx.direction, tx.itemSlots[i].progress);
            visit(vi);
        }
    }
}

void BeltSystem::appendItemSlots(Hasher& hasher, const std::vector<BeltItemSlot>& slotRun)
{
    hasher.append(slotRun.size());
    for (const BeltItemSlot& slot : slotRun)
    {
        hasher.append(slot.item.type.id);
        hasher.append(slot.progress);
    }
}

void BeltSystem::appendChecksum(Hasher& hasher) const
{
    // std::map iterates in sorted key order, so all tile loops are deterministic.
    hasher.append(m_belts.size());
    for (const std::pair<const std::pair<int, int>, BeltTile>& entry : m_belts)
    {
        hasher.append(entry.first.first);
        hasher.append(entry.first.second);
        hasher.append(entry.second.direction);
        appendItemSlots(hasher, entry.second.itemSlots);
    }

    hasher.append(m_splitters.size());
    for (const std::pair<const std::pair<int, int>, SplitterTile>& entry : m_splitters)
    {
        hasher.append(entry.first.first);
        hasher.append(entry.first.second);
        const SplitterTile& s = entry.second;
        hasher.append(s.outputA);
        hasher.append(s.outputB);
        hasher.append(s.filterA.size());
        for (const ItemType& type : s.filterA) { hasher.append(type.id); }
        hasher.append(s.filterB.size());
        for (const ItemType& type : s.filterB) { hasher.append(type.id); }
        hasher.append(s.nextOutputIsA);
        appendItemSlots(hasher, s.back);
        hasher.append(s.backDir.size());
        for (Rotation dir : s.backDir) { hasher.append(dir); }
        hasher.append(s.frontA.has_value());
        if (s.frontA.has_value())
        {
            hasher.append(s.frontA->item.type.id);
            hasher.append(s.frontA->progress);
        }
        hasher.append(s.frontB.has_value());
        if (s.frontB.has_value())
        {
            hasher.append(s.frontB->item.type.id);
            hasher.append(s.frontB->progress);
        }
    }

    hasher.append(m_tunnelEntries.size());
    for (const std::pair<const std::pair<int, int>, TunnelEntryTile>& entry : m_tunnelEntries)
    {
        hasher.append(entry.first.first);
        hasher.append(entry.first.second);
        hasher.append(entry.second.direction);
        hasher.append(entry.second.maxDistance);
        appendItemSlots(hasher, entry.second.itemSlots);
    }

    hasher.append(m_tunnelExits.size());
    for (const std::pair<const std::pair<int, int>, TunnelExitTile>& entry : m_tunnelExits)
    {
        hasher.append(entry.first.first);
        hasher.append(entry.first.second);
        hasher.append(entry.second.direction);
        appendItemSlots(hasher, entry.second.itemSlots);
    }

    // m_tunnelLinks preserves insertion order, which is itself deterministic.
    hasher.append(m_tunnelLinks.size());
    for (const TunnelLink& link : m_tunnelLinks)
    {
        hasher.append(link.entryTile);
        hasher.append(link.exitTile);
        hasher.append(link.length);
        hasher.append(link.items.size());
        for (const TunnelTransitItem& item : link.items)
        {
            hasher.append(item.item.type.id);
            hasher.append(item.progress);
        }
    }
}


