#include "ConfigLoader.h"

#include <string>
#include <unordered_set>
#include <vector>

#include "TomlHelpers.h"

namespace
{

// Validates unlocks.toml against the rest of the config (REQ-LOCK-EXPLICIT,
// REQ-LOCK-PREREQ): each granted id resolves to the right kind of definition
// (recipe grants must name assembler recipes), each grantable id is granted by
// at most one group, group ids are unique, every group grants at least one
// item, and every `requires` id names a defined group.
void validateUnlocks(const GameConfig& cfg)
{
    const std::string file = "unlocks.toml";

    std::unordered_set<std::string> shipIds;
    for (const ShipDef& def : cfg.ships.ships) { shipIds.insert(def.id); }
    std::unordered_set<std::string> moduleIds;
    for (const ModuleDef& def : cfg.modules.modules) { moduleIds.insert(def.id); }
    std::unordered_set<std::string> buildingIds;
    for (const BuildingDef& def : cfg.buildings.buildings) { buildingIds.insert(def.id); }
    std::unordered_set<std::string> assemblerRecipeIds;
    for (const RecipeDef& def : cfg.recipes.recipes)
    {
        if (def.building == BuildingType::Assembler) { assemblerRecipeIds.insert(def.id); }
    }

    std::unordered_set<std::string> groupIds;
    std::unordered_set<std::string> grantedShipIds;
    std::unordered_set<std::string> grantedModuleIds;
    std::unordered_set<std::string> grantedBuildingIds;
    std::unordered_set<std::string> grantedRecipeIds;

    const auto checkGrants = [&](const std::vector<std::string>& ids,
                                 const std::unordered_set<std::string>& valid,
                                 std::unordered_set<std::string>& granted,
                                 const std::string& kind,
                                 const std::string& gPath)
    {
        for (const std::string& id : ids)
        {
            if (valid.count(id) == 0)
            {
                throw utility::makeError(file, gPath, "grants unknown " + kind + " '" + id + "'");
            }
            if (!granted.insert(id).second)
            {
                throw utility::makeError(file, gPath,
                    "grants " + kind + " '" + id + "' which is already granted by another unlock group");
            }
        }
    };

    for (const UnlockGroupDef& group : cfg.unlocks.groups)
    {
        const std::string gPath = "unlock '" + group.id + "'";
        if (!groupIds.insert(group.id).second)
        {
            throw utility::makeError(file, gPath, "duplicate unlock group id");
        }

        if (group.ships.empty() && group.modules.empty()
            && group.buildings.empty() && group.recipes.empty())
        {
            throw utility::makeError(file, gPath, "grants no items (must grant at least one)");
        }

        checkGrants(group.ships,     shipIds,            grantedShipIds,     "ship",             gPath);
        checkGrants(group.modules,   moduleIds,          grantedModuleIds,   "module",           gPath);
        checkGrants(group.buildings, buildingIds,        grantedBuildingIds, "building",         gPath);
        checkGrants(group.recipes,   assemblerRecipeIds, grantedRecipeIds,   "assembler recipe", gPath);
    }

    // requires must reference defined group ids (checked once all group ids known).
    for (const UnlockGroupDef& group : cfg.unlocks.groups)
    {
        for (const std::string& req : group.requiredGroupIds)
        {
            if (groupIds.count(req) == 0)
            {
                throw utility::makeError(file, "unlock '" + group.id + "'.requires",
                    "references unknown unlock group '" + req + "'");
            }
        }
    }
}

} // namespace

GameConfig ConfigLoader::loadFromDirectory(const std::string& configDir)
{
    GameConfig cfg;
    cfg.world     = loadWorld(configDir + "/world.toml");
    cfg.buildings = loadBuildings(configDir + "/buildings.toml");
    cfg.recipes   = loadRecipes(configDir + "/recipes.toml");
    cfg.ships     = loadShips(configDir + "/ships.toml");
    cfg.stations  = loadStations(configDir + "/stations.toml");
    cfg.modules   = loadModules(configDir + "/modules.toml");
    cfg.unlocks   = loadUnlocks(configDir + "/unlocks.toml");
    validateUnlocks(cfg);
    cfg.threatCosts = computeThreatCostTable(cfg);
    return cfg;
}
