#pragma once

#include <algorithm>
#include <functional>
#include <optional>
#include <string>
#include <vector>

#include "BuildingType.h"

// One entry in [[recipe]].inputs — amount units of a named item consumed per
// production cycle (REQ-MAT-CYCLE).
struct RecipeIngredient
{
    std::string item;
    int amount;
};

// One item produced by an output group -- amount units of a named item
// (REQ-MAT-OUTPUT-GROUP).
struct RecipeOutput
{
    std::string item;
    int amount;
};

// One possible result of a production cycle: the items it yields, produced together, and
// the weight this group is picked with among the recipe's groups (REQ-MAT-OUTPUT-GROUP).
// A recipe with a single group always produces it, so the weight is meaningful only where
// there are several -- which is the only difference between what used to be called a
// deterministic and a probabilistic recipe.
struct RecipeOutputGroup
{
    std::vector<RecipeOutput> items;
    std::optional<double> probability;
};

struct RecipeDef
{
    std::string id;                // Unique recipe id; used by UI for selection.
    BuildingType building;         // Which BuildingType can run this recipe.
    std::vector<RecipeIngredient> inputs;
    // Never empty: one group is the ordinary recipe (REQ-MAT-OUTPUT-GROUP).
    std::vector<RecipeOutputGroup> outputGroups;
    double durationSeconds;
    // Assembler only. When true, this recipe is available from game start
    // regardless of the implicit item graph — used for base recipes that no
    // schematic's materials reach (e.g. building blocks). See REQ-LOCK-IMPLICIT.
    // Otherwise an assembler recipe is either explicitly gated (granted by an
    // unlock group, REQ-LOCK-EXPLICIT) or implicitly gated via the item graph.
    bool unlockedAtStart = false;
};

// Every distinct item any group of this recipe can produce, in config order. Most callers
// only want to know what a recipe can make at all -- which items it has buffers for, which
// recipes produce an item -- and not which group yields what.
inline std::vector<std::string> getProducibleItems(const RecipeDef& recipe)
{
    std::vector<std::string> items;
    for (const RecipeOutputGroup& group : recipe.outputGroups)
    {
        for (const RecipeOutput& out : group.items)
        {
            if (std::find(items.begin(), items.end(), out.item) == items.end())
            {
                items.push_back(out.item);
            }
        }
    }
    return items;
}

// True when some group of this recipe yields the given item.
inline bool producesItem(const RecipeDef& recipe, const std::string& itemId)
{
    for (const RecipeOutputGroup& group : recipe.outputGroups)
    {
        for (const RecipeOutput& out : group.items)
        {
            if (out.item == itemId) { return true; }
        }
    }
    return false;
}

// True when every item this group yields is unlocked. A group is judged whole -- its
// items are produced together, so one locked item disqualifies the group entirely, since
// taking it would hand the player a locked item (REQ-LOCK-OUTPUT-POOL).
//
// This says whether a group is eligible, not whether eligibility is asked about at all:
// that is the caller's, because the callers differ. A cycle picking its result does not
// ask where there is only one group to pick (see ProductionSystem::rollOutputGroup).
inline bool isOutputGroupUnlocked(
    const RecipeOutputGroup& group,
    const std::function<bool(const std::string&)>& isItemUnlocked)
{
    for (const RecipeOutput& out : group.items)
    {
        if (!isItemUnlocked(out.item)) { return false; }
    }
    return true;
}

struct RecipesConfig
{
    std::vector<RecipeDef> recipes;

    // Returns the definition for the given recipe id, or nullptr if the id has
    // no entry in recipes.toml.
    const RecipeDef* findRecipeDef(const std::string& id) const
    {
        for (const RecipeDef& recipe : recipes)
        {
            if (recipe.id == id)
            {
                return &recipe;
            }
        }
        return nullptr;
    }

    // Same, but additionally requires the recipe to belong to the given building
    // type — recipe ids are only unique per building type.
    const RecipeDef* findRecipeDef(const std::string& id, BuildingType building) const
    {
        for (const RecipeDef& recipe : recipes)
        {
            if (recipe.id == id && recipe.building == building)
            {
                return &recipe;
            }
        }
        return nullptr;
    }
};
