"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.flattenUnions = flattenUnions;
const collection_utils_1 = require("collection-utils");
const TypeAttributes_js_1 = require("../attributes/TypeAttributes.js");
const Messages_js_1 = require("../Messages.js");
const Support_js_1 = require("../support/Support.js");
const Type_js_1 = require("../Type/Type.js");
const TypeRef_js_1 = require("../Type/TypeRef.js");
const TypeUtils_js_1 = require("../Type/TypeUtils.js");
const UnifyClasses_js_1 = require("../UnifyClasses.js");
function flattenUnions(graph, stringTypeMapping, conflateNumbers, makeObjectTypes, debugPrintReconstitution) {
    let needsRepeat = false;
    // During flattening, recursive unions can appear again while their replacement
    // is still being built.  For example, flattening `A | M` can later ask for
    // `A | (A | M)` through an array item or object property.  Those are the same
    // union by associativity/idempotence, so remember each in-progress flattened
    // union by a normalized key of its non-union member refs.  Looking up that key
    // lets recursive occurrences reuse the replacement's forwarding ref instead
    // of allocating another finite unrolling of the same cycle.
    const unionKeyToRef = new Map();
    function addUnionKeyAtoms(t, atoms, seen) {
        const index = t.index;
        if (seen.has(index))
            return;
        seen.add(index);
        if (t instanceof Type_js_1.UnionType) {
            for (const m of t.members) {
                addUnionKeyAtoms(m, atoms, seen);
            }
        }
        else {
            atoms.add((0, TypeRef_js_1.typeRefIndex)(t.typeRef));
        }
    }
    function unionKeyForTypes(types) {
        const atoms = new Set();
        const seen = new Set();
        for (const t of types) {
            addUnionKeyAtoms(t, atoms, seen);
        }
        return Array.from(atoms)
            .sort((a, b) => a - b)
            .join(",");
    }
    function replace(types, builder, forwardingRef) {
        unionKeyToRef.set(unionKeyForTypes(types), forwardingRef);
        let unionBuilder;
        const unifyTypeRefs = (trefs) => {
            (0, Support_js_1.assert)(trefs.length > 0, "Must have at least one type to build union");
            const maybeReconstituted = builder.lookupTypeRefs(trefs, undefined, false);
            if (maybeReconstituted !== undefined) {
                return maybeReconstituted;
            }
            const typesToUnify = new Set(trefs.map((tref) => (0, TypeRef_js_1.derefTypeRef)(tref, graph)));
            if ((0, collection_utils_1.iterableSome)(typesToUnify, (t) => t instanceof Type_js_1.IntersectionType)) {
                trefs = trefs.map((tref) => builder.reconstituteType((0, TypeRef_js_1.derefTypeRef)(tref, graph)));
                if (trefs.length === 1) {
                    return trefs[0];
                }
                needsRepeat = true;
                return builder.getUnionType(TypeAttributes_js_1.emptyTypeAttributes, new Set(trefs));
            }
            const key = unionKeyForTypes(typesToUnify);
            const maybeRef = unionKeyToRef.get(key);
            if (maybeRef !== undefined) {
                return maybeRef;
            }
            return builder.withForwardingRef(undefined, (nestedForwardingRef) => {
                unionKeyToRef.set(key, nestedForwardingRef);
                return (0, UnifyClasses_js_1.unifyTypes)(typesToUnify, TypeAttributes_js_1.emptyTypeAttributes, builder, unionBuilder, conflateNumbers, nestedForwardingRef);
            });
        };
        unionBuilder = new UnifyClasses_js_1.UnifyUnionBuilder(builder, makeObjectTypes, true, unifyTypeRefs);
        return (0, UnifyClasses_js_1.unifyTypes)(types, TypeAttributes_js_1.emptyTypeAttributes, builder, unionBuilder, conflateNumbers, forwardingRef);
    }
    const allUnions = (0, collection_utils_1.setFilter)(graph.allTypesUnordered(), (t) => t instanceof Type_js_1.UnionType);
    const nonCanonicalUnions = (0, collection_utils_1.setFilter)(allUnions, (u) => !u.isCanonical);
    let foundIntersection = false;
    const groups = (0, TypeUtils_js_1.makeGroupsToFlatten)(nonCanonicalUnions, (members) => {
        (0, Messages_js_1.messageAssert)(members.size > 0, "IRNoEmptyUnions", {});
        if (!(0, collection_utils_1.iterableSome)(members, (m) => m instanceof Type_js_1.IntersectionType))
            return true;
        // FIXME: This is stupid.  `flattenUnions` returns true when no more union
        // flattening is necessary, but `resolveIntersections` can introduce new
        // unions that might require flattening, so now `flattenUnions` needs to take
        // that into account.  Either change `resolveIntersections` such that it
        // doesn't introduce non-canonical unions (by using `unifyTypes`), or have
        // some other way to tell whether more work is needed that doesn't require
        // the two passes to know about each other.
        foundIntersection = true;
        return false;
    });
    graph = graph.rewrite("flatten unions", stringTypeMapping, false, groups, debugPrintReconstitution, replace);
    // console.log(`flattened ${nonCanonicalUnions.size} of ${unions.size} unions`);
    return [graph, !needsRepeat && !foundIntersection];
}
