#include "BufferedBuildingContent.h"

#include <map>
#include <set>

#include <QVBoxLayout>

#include "Building.h"
#include "BuildingTarget.h"
#include "DisplayName.h"
#include "FactoryQueries.h"
#include "ProductionSection.h"
#include "RecipeLineRow.h"
#include "SectionBox.h"
#include "SelectionNames.h"
#include "Simulation.h"

namespace
{

std::vector<RecipeLineRow::Amount> toAmounts(const std::map<std::string, int>& map)
{
    std::vector<RecipeLineRow::Amount> amounts;
    amounts.reserve(map.size());
    for (const std::pair<const std::string, int>& entry : map)
    {
        amounts.push_back(RecipeLineRow::Amount{ entry.first, entry.second });
    }
    return amounts;
}

// Every item one side of the card should list: what the buffer holds and what a cycle
// moves. Selecting a recipe clears the buffers and sizes them for that recipe alone
// (REQ-MAT-INPUT-BUFFER, REQ-MAT-OUTPUT-BUFFER), so the two agree wherever a recipe is
// in force. They are unioned for the sake of the Salvage Bay, which has no recipe to
// name its scrap and so is named by its buffer alone (REQ-BLD-SALVAGE-BAY).
std::set<std::string> collectItemIds(const std::map<std::string, int>& buffered,
                                     const std::map<std::string, int>& perCycle)
{
    std::set<std::string> itemIds;
    for (const std::pair<const std::string, int>& entry : buffered)
    {
        itemIds.insert(entry.first);
    }
    for (const std::pair<const std::string, int>& entry : perCycle)
    {
        itemIds.insert(entry.first);
    }
    return itemIds;
}

int lookUp(const std::map<std::string, int>& map, const std::string& key)
{
    const std::map<std::string, int>::const_iterator it = map.find(key);
    return (it != map.end()) ? it->second : 0;
}

}  // namespace


BufferedBuildingContent::BufferedBuildingContent(const SelectionContext& context,
                                                 BuildingId id, QWidget* parent)
    : SelectionContent(context, asConstructionSite(context, id), parent)
    , m_id(id)
{
    // The summary is configuration -- what the building will do -- so it sits with the
    // selection control and is shown for a construction site too (REQ-UI-RECIPE-SUMMARY).
    m_recipeSummary = new RecipeLineRow(context.itemIcons, context.buildingIcons, this);
    // Its icons name items, and nothing in the panel is clicked, so each explains itself
    // on hover or click -- which is how the player follows an input back to what makes it
    // without that item being in a buffer (REQ-UI-ITEM-VALUE-TOOLTIP).
    m_recipeSummary->setItemTooltips(context.getItemTooltipContext(),
                                     TooltipTrigger::Trigger::HoverAndClick);
    getConfigurationLayout()->addWidget(m_recipeSummary);

    m_inputSection = new SectionBox(tr("Input buffers"), this);
    m_inputChips   = new ItemChipRow(context, m_inputSection);
    m_inputSection->getContentLayout()->addWidget(m_inputChips);

    m_production = new ProductionSection(this);

    m_outputSection = new SectionBox(tr("Output buffers"), this);
    m_outputChips   = new ItemChipRow(context, m_outputSection);
    m_outputSection->getContentLayout()->addWidget(m_outputChips);

    // In the direction the materials flow: what goes in, what is being made of it, what
    // has come out (REQ-UI-SINGLE-SELECTION, REQ-UI-PRODUCTION-PROGRESS).
    getRuntimeLayout()->addWidget(m_inputSection);
    getRuntimeLayout()->addWidget(m_production);
    getRuntimeLayout()->addWidget(m_outputSection);
}

void BufferedBuildingContent::refreshConfiguration()
{
    const BuildingTarget target = resolveBuildingTarget(getContext(), m_id);
    if (!target.isValid())
    {
        // Gone under the card. SelectionPanel rebuilds on the same refresh; this only
        // has to avoid reading it.
        return;
    }

    setBuildingIdentity(target.type, getBuildingTypeName(target.type));

    // The card already names the building in its header, so the summary states the cycle
    // alone -- no building chip, no recipe name (REQ-UI-RECIPE-SUMMARY).
    const CycleInfo cycle = getCycleInfo(target);
    RecipeLineRow::Spec summary;
    summary.inputs          = toAmounts(cycle.perCycleInputs);
    summary.outputGroups    = cycle.perCycleOutputGroups;
    summary.durationSeconds = cycle.durationSeconds;
    m_recipeSummary->setLine(summary);

    refreshControls(target);
}

void BufferedBuildingContent::refreshRuntime()
{
    const BuildingTarget target = resolveBuildingTarget(getContext(), m_id);
    if (!target.building)
    {
        return;
    }

    setProductionStatusSlot(*target.building);

    const CycleInfo cycle = getCycleInfo(target);

    const std::vector<ItemChipRow::Entry> inputs =
        buildInputEntries(*target.building, cycle);
    const std::vector<ItemChipRow::Entry> outputs =
        buildOutputEntries(*target.building, cycle);

    m_inputChips->setEntries(inputs);
    m_outputChips->setEntries(outputs);
    m_inputSection->setVisible(!inputs.empty());
    m_outputSection->setVisible(!outputs.empty());

    m_production->setProduction(cycle.runsProduction, *target.building,
                                cycle.durationSeconds,
                                getContext().sim->getCurrentTick());
}

std::vector<ItemChipRow::Entry> BufferedBuildingContent::buildInputEntries(
    const Building& building, const CycleInfo& cycle) const
{
    std::map<std::string, int> buffered;
    for (const std::pair<const ItemType, int>& entry : building.inputBuffer.counts)
    {
        buffered[entry.first.id] = entry.second;
    }

    // One chip per input buffer the building has, which is one per input of the selected
    // recipe: selecting it sized the buffers for that recipe alone (REQ-MAT-INPUT-BUFFER).
    // No unlock test, as on the output side -- whether a buffer exists is a fact about
    // the building, not about the player's progression. A buffer for something they
    // cannot make yet simply reads 0 until they can, which is what it holds.
    std::vector<ItemChipRow::Entry> entries;
    for (const std::string& itemId : collectItemIds(buffered, cycle.perCycleInputs))
    {
        ItemChipRow::Entry chip;
        chip.itemId    = itemId;
        chip.countText = QString::number(lookUp(buffered, itemId));

        const int perCycle = lookUp(cycle.perCycleInputs, itemId);
        if (perCycle > 0)
        {
            chip.subLine = tr("/ %1 per cycle").arg(perCycle);
        }
        else
        {
            chip.subLine = QString::fromStdString(toDisplayName(itemId));
        }
        entries.push_back(chip);
    }
    return entries;
}

std::vector<ItemChipRow::Entry> BufferedBuildingContent::buildOutputEntries(
    const Building& building, const CycleInfo& cycle) const
{
    // The buffered items plus those still emerging onto the output belts: an emerging
    // item still belongs to the output buffer (REQ-MAT-OUTPUT-EMERGE), so leaving it out
    // would make it vanish from the panel while it animates.
    std::map<std::string, int> buffered;
    for (const Item& item : building.outputBuffer.items)
    {
        buffered[item.type.id]++;
    }
    for (const std::vector<BeltItemSlot>& lane : building.emergingItems)
    {
        for (const BeltItemSlot& slot : lane)
        {
            buffered[slot.item.type.id]++;
        }
    }

    // A chip stands for a buffer, and a buffer exists for every item any group can
    // produce (REQ-MAT-OUTPUT-BUFFER), so the groups are flattened here. They are the
    // groups this building can actually be handed, the locked ones having already been
    // dropped from the cycle (REQ-UI-RECIPE-SUMMARY), which is what keeps the chips
    // agreeing with the summary above them.
    std::map<std::string, int> producible;
    for (const std::vector<RecipeLineRow::Amount>& group : cycle.perCycleOutputGroups)
    {
        for (const RecipeLineRow::Amount& amount : group)
        {
            producible[amount.itemId] = std::max(producible[amount.itemId], amount.amount);
        }
    }

    // Every id here is one the player can already have: the producible ones because a
    // locked group never reaches this far, and the buffered ones because a building only
    // ever deposits what a group it was picking from yields. So nothing is filtered out
    // -- and nothing should be. A chip that vanished would not hide a locked item, it
    // would misreport a buffer the building is filling.
    std::vector<ItemChipRow::Entry> entries;
    for (const std::string& itemId : collectItemIds(buffered, producible))
    {
        ItemChipRow::Entry chip;
        chip.itemId = itemId;
        // Counted against this item's own buffer capacity, which is what production
        // stops at (REQ-MAT-OUTPUT-BUFFER, REQ-UI-SINGLE-SELECTION). Every item listed
        // here has one: a recipe sizes a buffer for each item it can produce and
        // selecting it clears whatever the previous recipe left, so no chip stands for
        // an unsized buffer.
        const std::map<ItemType, int>::const_iterator capIt =
            building.outputBuffer.caps.find(ItemType{itemId});
        const int cap =
            (capIt != building.outputBuffer.caps.end()) ? capIt->second : 0;
        chip.countText = tr("%1 / %2").arg(lookUp(buffered, itemId)).arg(cap);
        chip.subLine = QString::fromStdString(toDisplayName(itemId));
        entries.push_back(chip);
    }
    return entries;
}
