export class PlaceTypeRegistry {
    static #providers = new Map();

    static register(provider = {}) {
        const type = String(provider.type || "").trim();
        if (!type) throw new Error("A place type requires an id.");
        if (typeof provider.listNodes !== "function") throw new Error(`Place type "${type}" requires listNodes().`);
        this.#providers.set(type, Object.freeze({ ...provider, type }));
        return this.#providers.get(type);
    }

    static get(type) {
        return this.#providers.get(String(type || "")) || null;
    }

    static list() {
        return [...this.#providers.values()];
    }

    static getNodes(options = {}) {
        const nodes = this.list().flatMap(provider => provider.listNodes(options) || [])
            .filter(node => node?.key && node?.type && node?.objectId);
        const ownerBySceneKey = this.getProjectionAliases(nodes);

        const projected = nodes.map(node => {
            const projectedParentKey = ownerBySceneKey.get(node.parentKey) || null;
            return projectedParentKey && projectedParentKey !== node.key
                ? { ...node, parentKey: projectedParentKey }
                : node;
        });
        if (game.user?.isGM) return projected;

        const byKey = new Map(projected.map(node => [node.key, node]));
        const blockedParents = new Set(projected.filter(node => node.blocksPlayerDescendants).map(node => node.key));
        return projected.filter(node => {
            const visited = new Set();
            let parentKey = String(node.parentKey || "");
            while (parentKey && !visited.has(parentKey)) {
                if (blockedParents.has(parentKey)) return false;
                visited.add(parentKey);
                parentKey = String(byKey.get(parentKey)?.parentKey || "");
            }
            return true;
        });
    }

    static getProjectionAliases(nodes = []) {
        return new Map((nodes || [])
            .filter(node => node.type === "location" && node.primarySceneId)
            .map(node => [`scene:${node.primarySceneId}`, node.key]));
    }

    static resolveProjectionKey(key = null, nodes = []) {
        const normalized = String(key || "").trim() || null;
        return normalized ? this.getProjectionAliases(nodes).get(normalized) || normalized : null;
    }

    static async open(node) {
        const provider = this.get(node?.type);
        return provider?.open ? provider.open(node) : null;
    }

    static async deleteNode(node, options = {}) {
        const provider = this.get(node?.type);
        if (!provider?.deleteNode) throw new Error(`Place type "${node?.type || "unknown"}" cannot be deleted.`);
        return provider.deleteNode(node, options);
    }
}
