const PTS_COST_NAME = "pts";
const FACTION_CATEGORY = /^Faction:\s*(.+)$/;
const POINTS_LIMIT = /(\d[\d,]*)\s*Point/i;
const ENHANCEMENT_GROUP_PREFIX = "Enhancements";
const CHARACTER_CATEGORIES = new Set(["Character", "Epic Hero"]);
const WEAPON_CATEGORY_SUFFIX = " Weapon"; // "Ranged Weapon", "Melee Weapon", "Psychic Weapon"
const NEWRECRUIT_XMLNS = "http://www.battlescribe.net/schema/rosterSchema";
const NEWRECRUIT_HOST_PREFIX = "https://newrecruit";
function asArray(value) {
    return Array.isArray(value) ? value : [];
}
function asString(value) {
    return typeof value === "string" ? value : null;
}
function selectionName(sel) {
    return asString(sel.name) ?? "";
}
function selectionType(sel) {
    return asString(sel.type) ?? "";
}
/** A selection's multiplicity (`number`), defaulting to 1. */
function selectionCount(sel) {
    return typeof sel.number === "number" && sel.number > 0 ? sel.number : 1;
}
/** Point value from a selection's cost block, or null when absent. */
function pointsOf(sel) {
    for (const raw of asArray(sel.costs)) {
        const cost = raw;
        if (asString(cost.name) === PTS_COST_NAME && typeof cost.value === "number") {
            return cost.value;
        }
    }
    return null;
}
function categoryNames(sel) {
    return asArray(sel.categories)
        .map((c) => asString(c.name))
        .filter((n) => n !== null);
}
function childSelections(sel) {
    return asArray(sel.selections);
}
/** Depth-first visit of a selection and everything beneath it. */
function walk(sel, visit) {
    visit(sel);
    for (const child of childSelections(sel))
        walk(child, visit);
}
function isUnitSelection(sel) {
    const type = selectionType(sel);
    return type === "model" || type === "unit";
}
function isCharacter(sel) {
    return categoryNames(sel).some((n) => CHARACTER_CATEGORIES.has(n));
}
function isWeaponSelection(sel) {
    return categoryNames(sel).some((n) => n.endsWith(WEAPON_CATEGORY_SUFFIX));
}
function isEnhancementSelection(sel) {
    const group = asString(sel.group);
    return group !== null && group.startsWith(ENHANCEMENT_GROUP_PREFIX);
}
/** Sum the model count of a unit from its nested model selections. */
function modelCount(unit) {
    let total = 0;
    walk(unit, (s) => {
        if (selectionType(s) === "model")
            total += selectionCount(s);
    });
    return total > 0 ? total : selectionCount(unit);
}
/** Build a parsed unit from a top-level unit selection. */
function parseUnit(unit) {
    const wargear = [];
    let enhancement_raw_name = null;
    let enhancement_points = null;
    let is_warlord = false;
    for (const node of childSelections(unit)) {
        walk(node, (s) => {
            if (isEnhancementSelection(s)) {
                if (enhancement_raw_name === null) {
                    enhancement_raw_name = selectionName(s);
                    enhancement_points = pointsOf(s);
                }
                return;
            }
            if (selectionName(s) === "Warlord") {
                is_warlord = true;
                return;
            }
            if (isWeaponSelection(s)) {
                wargear.push({ raw_name: selectionName(s), count: selectionCount(s) });
            }
        });
    }
    return {
        raw_name: selectionName(unit),
        is_character: isCharacter(unit),
        model_count: modelCount(unit),
        points: pointsOf(unit),
        is_warlord,
        enhancement_raw_name,
        enhancement_points,
        wargear,
    };
}
/** Value carried as the first child of a named configuration selection. */
function configValue(selections, configName) {
    const node = selections.find((s) => selectionName(s) === configName);
    if (!node)
        return null;
    const child = childSelections(node)[0];
    return child ? selectionName(child) : null;
}
function parseLimit(label) {
    if (!label)
        return null;
    const match = POINTS_LIMIT.exec(label);
    if (!match)
        return null;
    return Number.parseInt(match[1].replace(/,/g, ""), 10);
}
/** First `"Faction: X"` category found anywhere; reports all distinct names. */
function collectFactions(forces) {
    const seen = new Set();
    for (const force of forces) {
        for (const sel of childSelections(force)) {
            walk(sel, (s) => {
                for (const name of categoryNames(s)) {
                    const match = FACTION_CATEGORY.exec(name);
                    if (match)
                        seen.add(match[1].trim());
                }
            });
        }
    }
    return [...seen];
}
function rosterOf(decoded) {
    if (!decoded || typeof decoded !== "object")
        return null;
    const roster = decoded.roster;
    if (!roster || typeof roster !== "object")
        return null;
    if (!Array.isArray(roster.forces))
        return null;
    return roster;
}
/** Detect a NewRecruit-flavoured BattleScribe payload. ListForge's matcher
 * excludes these so the greedy first-match dispatcher routes them to the
 * NewRecruit adapter without falling through to here. */
function hasNewRecruitSignature(decoded, roster) {
    if (asString(roster.xmlns) === NEWRECRUIT_XMLNS)
        return true;
    const genBy = asString(decoded.generatedBy) ?? asString(roster.generatedBy);
    return genBy !== null && genBy.toLowerCase().startsWith(NEWRECRUIT_HOST_PREFIX);
}
export const listForgeAdapter = {
    id: "listforge",
    matches(decoded) {
        const roster = rosterOf(decoded);
        if (!roster)
            return false;
        return !hasNewRecruitSignature(decoded, roster);
    },
    parse(decoded) {
        const payload = decoded;
        const roster = rosterOf(decoded);
        if (!roster) {
            throw new Error("listforge: payload has no roster.forces array");
        }
        const forces = asArray(roster.forces);
        // Configuration lives among each force's top-level selections.
        let detachment_raw_name = null;
        let battle_size_raw = null;
        const units = [];
        for (const force of forces) {
            const top = childSelections(force);
            detachment_raw_name ??= configValue(top, "Detachment");
            battle_size_raw ??= configValue(top, "Battle Size");
            for (const sel of top) {
                if (isUnitSelection(sel))
                    units.push(parseUnit(sel));
            }
        }
        const factions = collectFactions(forces);
        const total_reported = pointsOf(roster);
        // Honest computed total: sum every cost line in the tree. A unit's own cost
        // and its nested enhancement's cost are distinct lines that together make up
        // the unit's army contribution, so a full walk reproduces the army total.
        let total_computed = 0;
        for (const force of forces) {
            for (const sel of childSelections(force)) {
                walk(sel, (s) => {
                    const pts = pointsOf(s);
                    if (pts)
                        total_computed += pts;
                });
            }
        }
        return {
            name: asString(payload.name) ?? asString(roster.name) ?? "Imported roster",
            generated_by: asString(payload.generatedBy),
            faction_raw_name: factions[0] ?? null,
            detachment_raw_name,
            battle_size_raw,
            declared_limit: parseLimit(battle_size_raw),
            total_reported,
            total_computed,
            units,
            multi_force: factions.length > 1,
        };
    },
};
//# sourceMappingURL=listforge.js.map