#include "BuildingBuffers.h"

#include <algorithm>
#include <cassert>
#include <map>

#include "BuildingType.h"
#include "ItemType.h"
#include "ModulesConfig.h"
#include "ShipsConfig.h"

namespace
{

// Folds the output capacities one recipe implies into `caps`: twice each produced item's
// per-cycle amount (REQ-MAT-OUTPUT-BUFFER). A cycle yields exactly one output group
// (REQ-MAT-OUTPUT-GROUP), so an item's per-cycle amount is the largest total any single
// group produces of it -- summed within a group, whose items come together, and taken at
// its maximum across groups, of which only one ever happens.
//
// Where a cap is already present the larger wins, which is how a cap unions across the
// recipes it could be sized over -- the same rule the input caps follow.
void addOutputCaps(std::map<ItemType, int>& caps, const RecipeDef& recipe)
{
    std::map<ItemType, int> perCycle;
    for (const RecipeOutputGroup& group : recipe.outputGroups)
    {
        std::map<ItemType, int> inGroup;
        for (const RecipeOutput& out : group.items)
        {
            inGroup[ItemType{out.item}] += out.amount;
        }
        for (const std::pair<const ItemType, int>& entry : inGroup)
        {
            perCycle[entry.first] = std::max(perCycle[entry.first], entry.second);
        }
    }

    for (const std::pair<const ItemType, int>& entry : perCycle)
    {
        caps[entry.first] = std::max(caps[entry.first], 2 * entry.second);
    }
}

}  // namespace

void initBuffers(Building& b, const RecipeDef& recipe)
{
    b.inputBuffer.counts.clear();
    b.inputBuffer.caps.clear();
    for (const RecipeIngredient& ing : recipe.inputs)
    {
        const ItemType type{ing.item};
        b.inputBuffer.counts[type] = 0;
        b.inputBuffer.caps[type]   = 2 * ing.amount;
    }

    b.outputBuffer.items.clear();
    b.outputBuffer.caps.clear();
    addOutputCaps(b.outputBuffer.caps, recipe);
}

void initShipyardBuffers(const GameConfig& config, Building& b)
{
    b.inputBuffer.counts.clear();
    b.inputBuffer.caps.clear();
    // A shipyard spawns a ship rather than producing items, so it holds no output
    // buffer at all (REQ-MAT-OUTPUT-BUFFER, REQ-BLD-SHIPYARD).
    b.outputBuffer.items.clear();
    b.outputBuffer.caps.clear();
    const ShipDef* def = config.ships.findShipDef(b.recipeId);
    if (!def)
    {
        return;
    }
    for (const RecipeIngredient& ing : def->schematic.materials)
    {
        const ItemType type{ing.item};
        b.inputBuffer.counts[type] = 0;
        b.inputBuffer.caps[type]   = 2 * ing.amount;
    }
    if (b.shipLayout.has_value())
    {
        for (const PlacedModule& pm : b.shipLayout->placedModules)
        {
            const ModuleDef* modDef = config.modules.findModuleDef(pm.moduleId);
            if (!modDef)
            {
                continue;
            }
            for (const RecipeIngredient& ing : modDef->materials)
            {
                const ItemType type{ing.item};
                b.inputBuffer.counts.try_emplace(type, 0);
                b.inputBuffer.caps[type] += 2 * ing.amount;
            }
        }
    }
}

void initSalvageBayBuffer(const GameConfig& config, Building& b)
{
    // Salvage Bay has no recipe-driven buffer; scrap is the only thing it ever holds,
    // and that single buffer's holding size for ship drop-off is config-defined
    // (REQ-BLD-SALVAGE-BAY).
    b.outputBuffer.items.clear();
    b.outputBuffer.caps.clear();
    const BuildingDef* def = config.buildings.findBuildingDef(BuildingType::SalvageBay);
    b.outputBuffer.caps[ItemType{"scrap"}] =
        (def && def->outputBufferCapacity) ? *def->outputBufferCapacity : 0;
}


void reregisterBeltTile(BeltSystem& belts, const GameConfig& config,
                        const Building& building,
                                        const std::vector<ItemType>& splitterFilterA,
                                        const std::vector<ItemType>& splitterFilterB)
{
    switch (building.type)
    {
        case BuildingType::Belt:
            belts.placeBelt(building.anchor, building.rotation);
            break;
        case BuildingType::Splitter:
            assert(building.outputPorts.size() >= 2);
            belts.placeSplitter(building.anchor,
                                  building.outputPorts[0].direction,
                                  building.outputPorts[1].direction);
            belts.setSplitterFilters(building.anchor, splitterFilterA, splitterFilterB);
            break;
        case BuildingType::TunnelEntry:
            belts.placeTunnelEntry(building.anchor, building.rotation,
                                     config.world.tunnelMaxDistance_tiles);
            break;
        case BuildingType::TunnelExit:
            belts.placeTunnelExit(building.anchor, building.rotation);
            break;
        default:
            break;
    }
}

