/**
 * A queryable, iterable view over one entity collection.
 *
 * Indexes (by id, by normalized name, by faction) are built once at construction.
 * Records are deduplicated by {@link CollectionConfig.dedupeKeyOf} (default: id,
 * first occurrence wins). Some records are intentionally shared: the same unit
 * id (e.g. `ministorum-priest`) appears under several factions, so units dedupe
 * on `(faction_id, id)` to keep each faction's copy; identical core abilities
 * (e.g. `leader`) copied into many faction files dedupe away on `ability_id`.
 *
 * `get(id)`/`find` return the first match when an id is shared across factions;
 * use {@link Collection.byFaction} or {@link Collection.findAll} to disambiguate.
 *
 * @packageDocumentation
 */
import { normalizeName } from "./normalize.js";
/**
 * A collection of one entity type, exposing id/name/faction lookups.
 *
 * Iterable: `for (const unit of units) { … }`.
 *
 * @typeParam T - the raw (generated) record type
 * @typeParam V - the linked view type returned to callers
 */
export class Collection {
    items = [];
    byId = new Map();
    byNorm = new Map();
    byFactionId = new Map();
    idOf;
    nameOf;
    wrapFn;
    constructor(cfg) {
        this.idOf = cfg.idOf;
        this.nameOf = cfg.nameOf;
        this.wrapFn = cfg.wrap;
        const dedupeKeyOf = cfg.dedupeKeyOf ?? cfg.idOf;
        const seen = new Set();
        for (const item of cfg.items) {
            const dedupeKey = dedupeKeyOf(item);
            if (seen.has(dedupeKey))
                continue; // first-wins dedup
            seen.add(dedupeKey);
            this.items.push(item);
            const id = cfg.idOf(item);
            if (!this.byId.has(id))
                this.byId.set(id, item); // first-wins for shared ids
            const name = cfg.nameOf?.(item);
            if (name)
                push(this.byNorm, normalizeName(name), item);
            const faction = cfg.factionOf?.(item);
            if (faction)
                push(this.byFactionId, faction, item);
        }
    }
    /** Every record, deduplicated by id, in first-seen order. */
    get all() {
        return this.items.map((item) => this.wrapFn(item));
    }
    /** Number of distinct records. */
    get size() {
        return this.items.length;
    }
    /** Look up by exact id. */
    get(id) {
        const item = this.byId.get(id);
        return item ? this.wrapFn(item) : undefined;
    }
    /**
     * Look up by exact id *within a faction*. Use this when an id is shared
     * across factions (e.g. `chaos-land-raider` lives under five Chaos factions)
     * and a faction context is known — {@link get} would return whichever copy
     * was registered first, which may belong to the wrong faction. Returns
     * `undefined` when no record with that id belongs to `factionId`.
     */
    getInFaction(id, factionId) {
        const list = this.byFactionId.get(factionId);
        const item = list?.find((i) => this.idOf(i) === id);
        return item ? this.wrapFn(item) : undefined;
    }
    /** Whether a record with this exact id exists. */
    has(id) {
        return this.byId.has(id);
    }
    /**
     * Find one record by id or name. Name matching is diacritic- and
     * punctuation-insensitive (see {@link normalizeName}), trying, in order:
     * exact id → exact normalized name → normalized-name substring. Returns the
     * first match; names can repeat across factions, so use {@link findAll} or
     * {@link byFaction} when a query may be ambiguous.
     *
     * @example
     * units.find("Kharn"); // resolves "Khârn the Betrayer"
     */
    find(query) {
        return this.findAll(query)[0];
    }
    /**
     * All records matching a query, by the same rules as {@link find}. An exact id
     * match returns just that record; otherwise every normalized-name-exact match
     * is returned, falling back to every normalized-name-substring match. Useful
     * to surface (rather than silently collapse) names shared across factions.
     */
    findAll(query) {
        const byId = this.byId.get(query);
        if (byId)
            return [this.wrapFn(byId)];
        const key = normalizeName(query);
        const exact = this.byNorm.get(key);
        if (exact && exact.length > 0)
            return exact.map((i) => this.wrapFn(i));
        if (!this.nameOf || key === "")
            return [];
        return this.items
            .filter((item) => normalizeName(this.nameOf(item) ?? "").includes(key))
            .map((item) => this.wrapFn(item));
    }
    /** All records belonging to a faction id (empty if the type has no faction). */
    byFaction(factionId) {
        return (this.byFactionId.get(factionId) ?? []).map((i) => this.wrapFn(i));
    }
    [Symbol.iterator]() {
        return this.items.map((item) => this.wrapFn(item))[Symbol.iterator]();
    }
}
function push(map, key, value) {
    const existing = map.get(key);
    if (existing)
        existing.push(value);
    else
        map.set(key, [value]);
}
//# sourceMappingURL=collection.js.map