#include "ConfigLoader.h"

#include <cstdint>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "toml.hpp"

#include "TomlHelpers.h"

namespace
{

std::vector<RecipeOutput> parseRecipeOutputs(const toml::array& arr,
                                             const std::string& file,
                                             const std::string& path)
{
    std::vector<RecipeOutput> result;
    result.reserve(arr.size());
    for (std::size_t i = 0; i < arr.size(); ++i)
    {
        const std::string elemPath = path + "[" + std::to_string(i) + "]";
        const toml::table* t = arr[i].as_table();
        if (t == nullptr)
        {
            throw utility::makeError(file, elemPath, "not a table");
        }
        toml::table& mt = const_cast<toml::table&>(*t);

        RecipeOutput out;
        out.item = utility::requireString(mt["item"], file, elemPath + ".item");
        out.amount = static_cast<int>(utility::requireInt(mt["amount"], file, elemPath + ".amount"));
        result.push_back(std::move(out));
    }
    return result;
}

// The several-group form: one [[recipe.output_group]] entry per possible result, each
// carrying its weight and the items it yields together (REQ-MAT-OUTPUT-GROUP).
std::vector<RecipeOutputGroup> parseOutputGroups(const toml::array& arr,
                                                 const std::string& file,
                                                 const std::string& path)
{
    std::vector<RecipeOutputGroup> result;
    result.reserve(arr.size());
    for (std::size_t i = 0; i < arr.size(); ++i)
    {
        const std::string elemPath = path + "[" + std::to_string(i) + "]";
        const toml::table* t = arr[i].as_table();
        if (t == nullptr)
        {
            throw utility::makeError(file, elemPath, "not a table");
        }
        toml::table& mt = const_cast<toml::table&>(*t);

        RecipeOutputGroup group;
        const toml::array& items =
            utility::requireArray(mt["items"], file, elemPath + ".items");
        group.items = parseRecipeOutputs(items, file, elemPath + ".items");
        if (group.items.empty())
        {
            throw utility::makeError(file, elemPath + ".items", "produces nothing");
        }
        if (const std::optional<double> p = mt["probability"].value<double>())
        {
            group.probability = *p;
        }
        else if (const std::optional<int64_t> p = mt["probability"].value<int64_t>())
        {
            group.probability = static_cast<double>(*p);
        }
        result.push_back(std::move(group));
    }
    return result;
}

}  // namespace

RecipesConfig ConfigLoader::loadRecipes(const std::string& path)
{
    const std::string file = "recipes.toml";
    toml::table tbl = utility::parseFile(path, file);

    RecipesConfig cfg;
    const toml::array& arr = utility::requireArray(tbl["recipe"], file, "recipe");

    for (std::size_t i = 0; i < arr.size(); ++i)
    {
        const std::string elemPath = "recipe[" + std::to_string(i) + "]";
        const toml::table* rt = arr[i].as_table();
        if (rt == nullptr)
        {
            throw utility::makeError(file, elemPath, "not a table");
        }
        toml::table& mt = const_cast<toml::table&>(*rt);

        RecipeDef def;
        def.id              = utility::requireString(mt["id"],              file, elemPath + ".id");
        def.durationSeconds = utility::requireDouble(mt["duration_seconds"], file, elemPath + ".duration_seconds");

        const std::string buildingId = utility::requireString(mt["building"], file, elemPath + ".building");
        const std::optional<BuildingType> parsedType = parseBuildingType(buildingId);
        if (!parsedType)
        {
            throw utility::makeError(file, elemPath + ".building",
                            "unknown building id '" + buildingId + "'");
        }
        def.building = *parsedType;

        if (def.building == BuildingType::Assembler && mt.contains("unlocked_at_start"))
        {
            def.unlockedAtStart = utility::requireBool(mt["unlocked_at_start"], file,
                elemPath + ".unlocked_at_start");
        }

        // inputs may be omitted (e.g. miner recipes). An empty array is fine.
        if (mt.contains("inputs"))
        {
            const toml::array& inputs = utility::requireArray(mt["inputs"], file, elemPath + ".inputs");
            def.inputs = utility::parseIngredients(inputs, file, elemPath + ".inputs");
        }

        // Either form, never both (REQ-MAT-OUTPUT-GROUP): `outputs` is the single-group
        // shorthand that all but the reprocessing recipes use, `output_group` the several-
        // group form. The shorthand carries no weight -- with one group nothing is picked.
        const bool hasOutputs = mt.contains("outputs");
        const bool hasGroups  = mt.contains("output_group");
        if (hasOutputs && hasGroups)
        {
            throw utility::makeError(file, elemPath,
                "has both 'outputs' and 'output_group'; use one or the other");
        }
        if (hasGroups)
        {
            const toml::array& groups =
                utility::requireArray(mt["output_group"], file, elemPath + ".output_group");
            def.outputGroups = parseOutputGroups(groups, file, elemPath + ".output_group");
            if (def.outputGroups.empty())
            {
                throw utility::makeError(file, elemPath + ".output_group", "is empty");
            }
        }
        else
        {
            const toml::array& outputs =
                utility::requireArray(mt["outputs"], file, elemPath + ".outputs");
            RecipeOutputGroup group;
            group.items = parseRecipeOutputs(outputs, file, elemPath + ".outputs");
            def.outputGroups.push_back(std::move(group));
        }

        cfg.recipes.push_back(std::move(def));
    }

    return cfg;
}
