"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolve = resolve;
const fetchEntityResolver_1 = require("./resolvers/fetchEntityResolver");
const fetchLayoutResolver_1 = require("./resolvers/fetchLayoutResolver");
const resourceUrlResolver_1 = require("./resolvers/resourceUrlResolver");
const defaultResolvers = {
    entity: fetchEntityResolver_1.fetchEntityResolver,
    layout: fetchLayoutResolver_1.fetchLayoutResolver,
    resourceUrl: resourceUrlResolver_1.resourceUrlResolver,
};
/**
 * Resolves $ref placeholders in objects so downstream functions can work with concrete data.
 * This keeps objects portable (they can point at remote entities) while consumers get a fully materialized object.
 * Note: This was designed for the Layout system, and the default resolvers reflect that,
 * but the function is generic and can be used to resolve any references in any object,
 * as long as appropriate resolvers are provided for the protocols in use.
 */
async function resolve(obj, context, options) {
    if (obj == null || typeof obj !== "object") {
        throw new Error("resolve: only objects can be resolved");
    }
    const { resolvers = {}, cache = new Map() } = options || {};
    const protocolResolvers = Object.assign(Object.assign({}, defaultResolvers), resolvers);
    // track refs so we only resolve each unique reference once, even if it appears multiple times
    const refMap = new Map();
    const collectRefs = (value) => {
        if (!value || typeof value !== "object") {
            return;
        }
        if (Array.isArray(value)) {
            value.forEach(collectRefs);
            return;
        }
        const record = value;
        const refValue = record.$ref;
        if (typeof refValue === "string") {
            refMap.set(refValue, refValue);
            return;
        }
        Object.values(record).forEach(collectRefs);
    };
    // enforce a strict protocol prefix so resolvers can be selected deterministically
    const parseProtocol = (ref) => {
        const delimiterIndex = ref.indexOf("://");
        if (delimiterIndex === -1) {
            throw new Error(`resolve: invalid ref "${ref}"`);
        }
        return ref.slice(0, delimiterIndex);
    };
    const resolveReferenceMap = async (refs) => {
        // prefer cached values to avoid repeated network calls and keep layouts fast when the same ref recurs
        const refsToResolve = new Map();
        const cachedRefs = new Map();
        refs.forEach((value, key) => {
            if (cache.has(key)) {
                cachedRefs.set(key, cache.get(key));
            }
            else {
                refsToResolve.set(key, value);
            }
        });
        const resolvedEntries = await Promise.all(Array.from(refsToResolve.keys()).map(async (ref) => {
            const protocol = parseProtocol(ref);
            const resolver = protocolResolvers[protocol];
            if (typeof resolver !== "function") {
                throw new Error(`resolve: unsupported protocol "${protocol}" in ref "${ref}"`);
            }
            const value = await resolver(ref, context);
            cache.set(ref, value);
            return [ref, value];
        }));
        return new Map([...resolvedEntries, ...cachedRefs]);
    };
    const replaceRefValues = (value, resolvedRefs) => {
        if (!value || typeof value !== "object") {
            return value;
        }
        if (Array.isArray(value)) {
            const arr = value;
            for (let i = 0; i < arr.length; i++) {
                arr[i] = replaceRefValues(arr[i], resolvedRefs);
            }
            return arr;
        }
        const record = value;
        const refValue = record.$ref;
        if (typeof refValue === "string") {
            return resolvedRefs.get(refValue);
        }
        Object.keys(record).forEach((key) => {
            record[key] = replaceRefValues(record[key], resolvedRefs);
        });
        return record;
    };
    collectRefs(obj);
    if (!refMap.size) {
        return obj;
    }
    // only resolve when we actually discovered references, keeping plain objects untouched
    const resolvedRefs = await resolveReferenceMap(refMap);
    return replaceRefValues(obj, resolvedRefs);
}
//# sourceMappingURL=resolve.js.map