#include "ProductionRules.h"

#include <algorithm>
#include <map>

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

const RecipeDef* getSelectedRecipe(const GameConfig& config, const Building& b)
{
    if (b.recipeId.empty())
    {
        return nullptr;
    }
    return config.recipes.findRecipeDef(b.recipeId, b.type);
}

const RecipeDef* findAutoRecipeFor(const GameConfig& config, BuildingType type,
                                   const ItemType& item)
{
    if (!isAutoRecipeBuildingType(type))
    {
        return nullptr;
    }

    // Config order decides where a material feeds more than one recipe of the type, so
    // the same offer always picks the same recipe (REQ-BLD-AUTO-RECIPE).
    for (const RecipeDef& recipe : config.recipes.recipes)
    {
        if (recipe.building != type) { continue; }
        for (const RecipeIngredient& ing : recipe.inputs)
        {
            if (ItemType{ing.item} == item)
            {
                return &recipe;
            }
        }
    }
    return nullptr;
}
bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe)
{
    for (const RecipeIngredient& ing : recipe.inputs)
    {
        const std::map<ItemType, int>::const_iterator it =
            b.inputBuffer.counts.find(ItemType{ing.item});
        const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0;
        if (have < ing.amount)
        {
            return false;
        }
    }
    return true;
}
std::map<std::string, int>
computeShipyardRequiredMaterials(const GameConfig& config, const Building& b)
{
    return computeShipyardRequiredMaterials(config, b.recipeId, b.shipLayout);
}

std::map<std::string, int>
computeShipyardRequiredMaterials(const GameConfig& config,
                                 const std::string& recipeId,
                                 const std::optional<ShipLayoutConfig>& shipLayout)
{
    std::map<std::string, int> requiredMaterials;
    const ShipDef* shipDef = config.ships.findShipDef(recipeId);
    if (!shipDef)
    {
        return requiredMaterials;
    }
    for (const RecipeIngredient& ing : shipDef->schematic.materials)
    {
        requiredMaterials[ing.item] += ing.amount;
    }
    if (shipLayout.has_value())
    {
        for (const PlacedModule& pm : shipLayout->placedModules)
        {
            const ModuleDef* modDef = config.modules.findModuleDef(pm.moduleId);
            if (!modDef)
            {
                continue;
            }
            for (const RecipeIngredient& ing : modDef->materials)
            {
                requiredMaterials[ing.item] += ing.amount;
            }
        }
    }
    return requiredMaterials;
}

double computeShipyardProductionTimeSeconds(
    const GameConfig& config, const std::string& recipeId,
    const std::optional<ShipLayoutConfig>& shipLayout)
{
    const ShipDef* shipDef = config.ships.findShipDef(recipeId);
    if (!shipDef)
    {
        return 0.0;
    }

    double seconds = shipDef->schematic.productionTimeSeconds;
    if (shipLayout.has_value())
    {
        for (const PlacedModule& pm : shipLayout->placedModules)
        {
            const ModuleDef* modDef = config.modules.findModuleDef(pm.moduleId);
            if (!modDef)
            {
                continue;
            }
            seconds += modDef->productionTimeSeconds;
        }
    }
    return seconds;
}

bool hasInputsToStart(const GameConfig& config, const Building& b)
{
    if (b.type == BuildingType::Shipyard)
    {
        const std::map<std::string, int> required =
            computeShipyardRequiredMaterials(config, b);
        for (const std::pair<const std::string, int>& req : required)
        {
            const std::map<ItemType, int>::const_iterator it =
                b.inputBuffer.counts.find(ItemType{req.first});
            const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0;
            if (have < req.second)
            {
                return false;
            }
        }
        return true;
    }

    // Recipe buildings: startable if the selected recipe's inputs are satisfied. A Miner
    // recipe has no inputs, so an idle Miner is always startable here and its only idle
    // reason is an output buffer without room for the next cycle.
    const RecipeDef* recipe = getSelectedRecipe(config, b);
    return recipe != nullptr && recipeInputsAvailable(b, *recipe);
}

bool outputBufferHasRoom(const Building& b, const ItemType& type, int itemCount)
{
    const std::map<ItemType, int>::const_iterator capIt = b.outputBuffer.caps.find(type);
    const int cap = (capIt != b.outputBuffer.caps.end()) ? capIt->second : 0;
    return b.getOutputItemCount(type) + itemCount <= cap;
}

bool recipeOutputsFit(const Building& b, const RecipeDef& recipe)
{
    for (const RecipeOutputGroup& group : recipe.outputGroups)
    {
        // A group's items come together, so an item listed twice in one is produced in the
        // sum of those amounts and judged once, as a sum.
        std::map<ItemType, int> perCycle;
        for (const RecipeOutput& out : group.items)
        {
            perCycle[ItemType{out.item}] += out.amount;
        }
        for (const std::pair<const ItemType, int>& entry : perCycle)
        {
            if (!outputBufferHasRoom(b, entry.first, entry.second))
            {
                return false;
            }
        }
    }
    return true;
}

bool canStartCycle(const GameConfig& config, const Building& b)
{
    // A shipyard's completed cycle spawns a ship instead of filling an output buffer
    // (REQ-BLD-SHIPYARD), so holding the materials is the whole condition.
    if (b.type == BuildingType::Shipyard)
    {
        return hasInputsToStart(config, b);
    }

    const RecipeDef* recipe = getSelectedRecipe(config, b);
    return recipe != nullptr && recipeInputsAvailable(b, *recipe)
           && recipeOutputsFit(b, *recipe);
}

std::optional<ProductionStatus>
getProductionStatus(const GameConfig& config, const Building& building)
{
    // Salvage Bay has no recipe or production cycle (REQ-BLD-SALVAGE-BAY): it is
    // "producing" while it holds scrap to push out, and starved when empty.
    if (building.type == BuildingType::SalvageBay)
    {
        return building.getOutputItemCount() >= 1 ? ProductionStatus::Producing
                                                  : ProductionStatus::Starved;
    }

    // Only the five recipe/cycle production types show a status light besides the
    // Salvage Bay; belts, splitters, tunnels, HQ, and stations show none.
    if (!isProductionBuildingType(building.type))
    {
        return std::nullopt;
    }

    // Every production building can be unconfigured, an auto-recipe building included:
    // it holds no recipe until one is offered to it, and the player can hand it back to
    // automatic selection (REQ-BLD-AUTO-RECIPE, REQ-UI-STATUS-LIGHT).
    if (building.recipeId.empty())
    {
        return ProductionStatus::Unconfigured;
    }

    if (building.production.has_value())
    {
        return ProductionStatus::Producing;
    }

    // Idle, but blocked by neither condition: the building is only between cycles and
    // the simulation starts the next one on a following tick. A building running back
    // to back sits here for exactly one tick per cycle, since tickProduction never
    // starts a cycle in the tick one completed, so this must read as producing rather
    // than blink (REQ-UI-STATUS-LIGHT, REQ-MAT-CYCLE).
    if (canStartCycle(config, building))
    {
        return ProductionStatus::Producing;
    }

    // Idle for a reason: a missing input (red) takes precedence over an output buffer
    // with no room for the next cycle's output (yellow).
    return hasInputsToStart(config, building) ? ProductionStatus::Blocked
                                              : ProductionStatus::Starved;
}
