import { EqualityMap, mapMap } from "collection-utils";
import { combineTypeAttributes, emptyTypeAttributes, } from "./attributes/TypeAttributes.js";
import { assert, indentationString, panic } from "./support/Support.js";
import { TypeBuilder } from "./Type/TypeBuilder.js";
import { assertTypeRefGraph, derefTypeRef, isTypeRef, typeAndAttributesForTypeRef, typeRefIndex, } from "./Type/TypeRef.js";
import { combineTypeAttributesOfTypes } from "./Type/TypeUtils.js";
export class TypeReconstituter {
    constructor(_typeBuilder, _makeClassUnique, _typeAttributes, _forwardingRef, _register) {
        this._typeBuilder = _typeBuilder;
        this._makeClassUnique = _makeClassUnique;
        this._typeAttributes = _typeAttributes;
        this._forwardingRef = _forwardingRef;
        this._register = _register;
        this._wasUsed = false;
        this._typeRef = undefined;
    }
    builderForNewType() {
        assert(!this._wasUsed, "TypeReconstituter used more than once");
        this._wasUsed = true;
        return this._typeBuilder;
    }
    builderForSetting() {
        assert(this._wasUsed && this._typeRef !== undefined, "Can't set type members before constructing a type");
        return this._typeBuilder;
    }
    getResult() {
        if (this._typeRef === undefined) {
            return panic("Type was not reconstituted");
        }
        return this._typeRef;
    }
    // FIXME: Do registration automatically.
    register(tref) {
        assert(this._typeRef === undefined, "Cannot register a type twice");
        this._typeRef = tref;
        this._register(tref);
    }
    registerAndAddAttributes(tref) {
        this._typeBuilder.addAttributes(tref, this._typeAttributes);
        this.register(tref);
    }
    lookup(trefs) {
        assert(!this._wasUsed, "Cannot lookup constituents after building type");
        if (isTypeRef(trefs)) {
            return this._typeBuilder.lookupTypeRefs([trefs], undefined, false);
        }
        const maybeRefs = Array.from(trefs).map((tref) => this._typeBuilder.lookupTypeRefs([tref], undefined, false));
        if (maybeRefs.some((tref) => tref === undefined)) {
            return undefined;
        }
        return maybeRefs;
    }
    lookupMap(trefs) {
        const resultValues = this.lookup(trefs.values());
        if (resultValues === undefined)
            return undefined;
        assert(resultValues.length === trefs.size, "Didn't get back the correct number of types");
        const result = new Map();
        let i = 0;
        for (const k of trefs.keys()) {
            result.set(k, resultValues[i]);
            i += 1;
        }
        return result;
    }
    reconstitute(trefs) {
        assert(this._wasUsed, "Cannot reconstitute constituents before building type");
        if (isTypeRef(trefs)) {
            return this._typeBuilder.reconstituteTypeRef(trefs);
        }
        return Array.from(trefs).map((tref) => this._typeBuilder.reconstituteTypeRef(tref));
    }
    reconstituteMap(trefs) {
        return mapMap(trefs, (tref) => this._typeBuilder.reconstituteTypeRef(tref));
    }
    getPrimitiveType(kind) {
        this.register(this.builderForNewType().getPrimitiveType(kind, this._typeAttributes, this._forwardingRef));
    }
    getEnumType(cases) {
        this.register(this.builderForNewType().getEnumType(this._typeAttributes, cases, this._forwardingRef));
    }
    getUniqueMapType() {
        this.registerAndAddAttributes(this.builderForNewType().getUniqueMapType(this._forwardingRef));
    }
    getMapType(values) {
        this.register(this.builderForNewType().getMapType(this._typeAttributes, values, this._forwardingRef));
    }
    getUniqueArrayType() {
        this.registerAndAddAttributes(this.builderForNewType().getUniqueArrayType(this._forwardingRef));
    }
    getArrayType(items) {
        this.register(this.builderForNewType().getArrayType(this._typeAttributes, items, this._forwardingRef));
    }
    setArrayItems(items) {
        this.builderForSetting().setArrayItems(this.getResult(), items);
    }
    makeClassProperty(tref, isOptional) {
        return this._typeBuilder.makeClassProperty(tref, isOptional);
    }
    getObjectType(properties, additionalProperties) {
        this.register(this.builderForNewType().getUniqueObjectType(this._typeAttributes, properties, additionalProperties, this._forwardingRef));
    }
    getUniqueObjectType(properties, additionalProperties) {
        this.register(this.builderForNewType().getUniqueObjectType(this._typeAttributes, properties, additionalProperties, this._forwardingRef));
    }
    getClassType(properties) {
        if (this._makeClassUnique) {
            this.getUniqueClassType(false, properties);
            return;
        }
        this.register(this.builderForNewType().getClassType(this._typeAttributes, properties, this._forwardingRef));
    }
    getUniqueClassType(isFixed, properties) {
        this.register(this.builderForNewType().getUniqueClassType(this._typeAttributes, isFixed, properties, this._forwardingRef));
    }
    setObjectProperties(properties, additionalProperties) {
        this.builderForSetting().setObjectProperties(this.getResult(), properties, additionalProperties);
    }
    getUnionType(members) {
        this.register(this.builderForNewType().getUnionType(this._typeAttributes, members, this._forwardingRef));
    }
    getUniqueUnionType() {
        this.register(this.builderForNewType().getUniqueUnionType(this._typeAttributes, undefined, this._forwardingRef));
    }
    getIntersectionType(members) {
        this.register(this.builderForNewType().getIntersectionType(this._typeAttributes, members, this._forwardingRef));
    }
    getUniqueIntersectionType(members) {
        this.register(this.builderForNewType().getUniqueIntersectionType(this._typeAttributes, members, this._forwardingRef));
    }
    setSetOperationMembers(members) {
        this.builderForSetting().setSetOperationMembers(this.getResult(), members);
    }
}
export class BaseGraphRewriteBuilder extends TypeBuilder {
    constructor(originalGraph, stringTypeMapping, alphabetizeProperties, graphHasProvenanceAttributes, debugPrint) {
        super(stringTypeMapping, alphabetizeProperties, false, false, graphHasProvenanceAttributes);
        this.originalGraph = originalGraph;
        this.debugPrint = debugPrint;
        this.reconstitutedTypes = new Map();
        this._lostTypeAttributes = false;
        this._printIndent = 0;
    }
    withForwardingRef(maybeForwardingRef, typeCreator) {
        if (maybeForwardingRef !== undefined) {
            return typeCreator(maybeForwardingRef);
        }
        const forwardingRef = this.reserveTypeRef();
        const actualRef = typeCreator(forwardingRef);
        assert(actualRef === forwardingRef, "Type creator didn't return its forwarding ref");
        return actualRef;
    }
    reconstituteType(t, attributes, forwardingRef) {
        return this.reconstituteTypeRef(t.typeRef, attributes, forwardingRef);
    }
    reconstituteTypeRef(originalRef, attributes, maybeForwardingRef) {
        const maybeRef = this.lookupTypeRefs([originalRef], maybeForwardingRef);
        if (maybeRef !== undefined) {
            if (attributes !== undefined) {
                this.addAttributes(maybeRef, attributes);
            }
            return maybeRef;
        }
        return this.forceReconstituteTypeRef(originalRef, attributes, maybeForwardingRef);
    }
    reconstituteTypeAttributes(attributes) {
        return mapMap(attributes, (v, a) => a.reconstitute(this, v));
    }
    assertTypeRefsToReconstitute(typeRefs, forwardingRef) {
        assert(typeRefs.length > 0, "Must have at least one type to reconstitute");
        for (const originalRef of typeRefs) {
            assertTypeRefGraph(originalRef, this.originalGraph);
        }
        if (forwardingRef !== undefined) {
            assertTypeRefGraph(forwardingRef, this.typeGraph);
        }
    }
    changeDebugPrintIndent(delta) {
        this._printIndent += delta;
    }
    get debugPrintIndentation() {
        return indentationString(this._printIndent);
    }
    finish() {
        for (const [name, t] of this.originalGraph.topLevels) {
            this.addTopLevel(name, this.reconstituteType(t));
        }
        return super.finish();
    }
    setLostTypeAttributes() {
        this._lostTypeAttributes = true;
    }
    get lostTypeAttributes() {
        return this._lostTypeAttributes;
    }
}
export class GraphRemapBuilder extends BaseGraphRewriteBuilder {
    constructor(originalGraph, stringTypeMapping, alphabetizeProperties, graphHasProvenanceAttributes, _map, debugPrintRemapping) {
        super(originalGraph, stringTypeMapping, alphabetizeProperties, graphHasProvenanceAttributes, debugPrintRemapping);
        this._map = _map;
        this._attributeSources = new Map();
        for (const [source, target] of _map) {
            let maybeSources = this._attributeSources.get(target);
            if (maybeSources === undefined) {
                maybeSources = [target];
                this._attributeSources.set(target, maybeSources);
            }
            maybeSources.push(source);
        }
    }
    makeIdentity(_maker) {
        return undefined;
    }
    getMapTarget(tref) {
        const maybeType = this._map.get(derefTypeRef(tref, this.originalGraph));
        if (maybeType === undefined)
            return tref;
        assert(this._map.get(maybeType) === undefined, "We have a type that's remapped to a remapped type");
        return maybeType.typeRef;
    }
    addForwardingIntersection(_forwardingRef, _tref) {
        return panic("We can't add forwarding intersections when we're removing forwarding intersections");
    }
    lookupTypeRefs(typeRefs, forwardingRef) {
        assert(forwardingRef === undefined, "We can't have a forwarding ref when we remap");
        this.assertTypeRefsToReconstitute(typeRefs, forwardingRef);
        const first = this.reconstitutedTypes.get(typeRefIndex(this.getMapTarget(typeRefs[0])));
        if (first === undefined)
            return undefined;
        for (let i = 1; i < typeRefs.length; i++) {
            const other = this.reconstitutedTypes.get(typeRefIndex(this.getMapTarget(typeRefs[i])));
            if (first !== other)
                return undefined;
        }
        return first;
    }
    forceReconstituteTypeRef(originalRef, attributes, maybeForwardingRef) {
        originalRef = this.getMapTarget(originalRef);
        const index = typeRefIndex(originalRef);
        assert(this.reconstitutedTypes.get(index) === undefined, "Type has already been reconstituted");
        assert(maybeForwardingRef === undefined, "We can't have a forwarding ref when we remap");
        return this.withForwardingRef(undefined, (forwardingRef) => {
            this.reconstitutedTypes.set(index, forwardingRef);
            if (this.debugPrint) {
                console.log(`${this.debugPrintIndentation}reconstituting ${index} as ${typeRefIndex(forwardingRef)}`);
                this.changeDebugPrintIndent(1);
            }
            const [originalType, originalAttributes] = typeAndAttributesForTypeRef(originalRef, this.originalGraph);
            const attributeSources = this._attributeSources.get(originalType);
            if (attributes === undefined) {
                attributes = emptyTypeAttributes;
            }
            if (attributeSources === undefined) {
                attributes = combineTypeAttributes("union", attributes, this.reconstituteTypeAttributes(originalAttributes));
            }
            else {
                attributes = combineTypeAttributes("union", attributes, this.reconstituteTypeAttributes(combineTypeAttributesOfTypes("union", attributeSources)));
            }
            const newAttributes = attributes;
            const reconstituter = new TypeReconstituter(this, this.canonicalOrder, newAttributes, forwardingRef, (tref) => {
                assert(tref === forwardingRef, "Reconstituted type as a different ref");
                if (this.debugPrint) {
                    this.changeDebugPrintIndent(-1);
                    console.log(`${this.debugPrintIndentation}reconstituted ${index} as ${typeRefIndex(tref)}`);
                }
            });
            originalType.reconstitute(reconstituter, this.canonicalOrder);
            return reconstituter.getResult();
        });
    }
}
export class GraphRewriteBuilder extends BaseGraphRewriteBuilder {
    constructor(originalGraph, stringTypeMapping, alphabetizeProperties, graphHasProvenanceAttributes, setsToReplace, debugPrintReconstitution, _replacer) {
        super(originalGraph, stringTypeMapping, alphabetizeProperties, graphHasProvenanceAttributes, debugPrintReconstitution);
        this._replacer = _replacer;
        this._reconstitutedUnions = new EqualityMap();
        this._setsToReplaceByMember = new Map();
        for (const types of setsToReplace) {
            const set = new Set(types);
            for (const t of set) {
                const index = t.index;
                assert(!this._setsToReplaceByMember.has(index), "A type is member of more than one set to be replaced");
                this._setsToReplaceByMember.set(index, set);
            }
        }
    }
    registerUnion(typeRefs, reconstituted) {
        const set = new Set(typeRefs);
        assert(!this._reconstitutedUnions.has(set), "Cannot register reconstituted set twice");
        this._reconstitutedUnions.set(set, reconstituted);
    }
    replaceSet(typesToReplace, maybeForwardingRef) {
        return this.withForwardingRef(maybeForwardingRef, (forwardingRef) => {
            if (this.debugPrint) {
                console.log(`${this.debugPrintIndentation}replacing set ${Array.from(typesToReplace)
                    .map((t) => t.index.toString())
                    .join(",")} as ${typeRefIndex(forwardingRef)}`);
                this.changeDebugPrintIndent(1);
            }
            for (const t of typesToReplace) {
                const originalRef = t.typeRef;
                const index = typeRefIndex(originalRef);
                this.reconstitutedTypes.set(index, forwardingRef);
                this._setsToReplaceByMember.delete(index);
            }
            const result = this._replacer(typesToReplace, this, forwardingRef);
            assert(result === forwardingRef, "The forwarding ref got lost when replacing");
            if (this.debugPrint) {
                this.changeDebugPrintIndent(-1);
                console.log(`${this.debugPrintIndentation}replaced set ${Array.from(typesToReplace)
                    .map((t) => t.index.toString())
                    .join(",")} as ${typeRefIndex(forwardingRef)}`);
            }
            return result;
        });
    }
    forceReconstituteTypeRef(originalRef, attributes, maybeForwardingRef) {
        const [originalType, originalAttributes] = typeAndAttributesForTypeRef(originalRef, this.originalGraph);
        const index = typeRefIndex(originalRef);
        if (this.debugPrint) {
            console.log(`${this.debugPrintIndentation}reconstituting ${index}`);
            this.changeDebugPrintIndent(1);
        }
        if (attributes === undefined) {
            attributes = this.reconstituteTypeAttributes(originalAttributes);
        }
        else {
            attributes = combineTypeAttributes("union", attributes, this.reconstituteTypeAttributes(originalAttributes));
        }
        const reconstituter = new TypeReconstituter(this, this.canonicalOrder, attributes, maybeForwardingRef, (tref) => {
            if (this.debugPrint) {
                this.changeDebugPrintIndent(-1);
                console.log(`${this.debugPrintIndentation}reconstituted ${index} as ${typeRefIndex(tref)}`);
            }
            if (maybeForwardingRef !== undefined) {
                assert(tref === maybeForwardingRef, "We didn't pass the forwarding ref");
            }
            const alreadyReconstitutedType = this.reconstitutedTypes.get(index);
            if (alreadyReconstitutedType === undefined) {
                this.reconstitutedTypes.set(index, tref);
            }
            else {
                assert(tref === alreadyReconstitutedType, "We reconstituted a type twice differently");
            }
        });
        originalType.reconstitute(reconstituter, this.canonicalOrder);
        return reconstituter.getResult();
    }
    /*
     public reconstituteTypeUnmodified(originalType: Type): TypeRef {
        const reconstituter = new TypeReconstituter(
            this,
            this.alphabetizeProperties,
            emptyTypeAttributes,
            undefined,
            () => {}
        );
        originalType.reconstitute(reconstituter);
        return reconstituter.getResult();
    }
    */
    // If the union of these type refs have been, or are supposed to be, reconstituted to
    // one target type, return it.  Otherwise return undefined.
    lookupTypeRefs(typeRefs, forwardingRef, replaceSet = true) {
        this.assertTypeRefsToReconstitute(typeRefs, forwardingRef);
        // Check whether we have already reconstituted them.  That means ensuring
        // that they all have the same target type.
        let maybeRef = this.reconstitutedTypes.get(typeRefIndex(typeRefs[0]));
        if (maybeRef !== undefined && maybeRef !== forwardingRef) {
            let allEqual = true;
            for (let i = 1; i < typeRefs.length; i++) {
                if (this.reconstitutedTypes.get(typeRefIndex(typeRefs[i])) !==
                    maybeRef) {
                    allEqual = false;
                    break;
                }
            }
            if (allEqual) {
                return this.forwardIfNecessary(forwardingRef, maybeRef);
            }
        }
        // Has this been reconstituted as a set?
        maybeRef = this._reconstitutedUnions.get(new Set(typeRefs));
        if (maybeRef !== undefined && maybeRef !== forwardingRef) {
            return this.forwardIfNecessary(forwardingRef, maybeRef);
        }
        // Is this set requested to be replaced?  If not, we're out of options.
        const maybeSet = this._setsToReplaceByMember.get(typeRefIndex(typeRefs[0]));
        if (maybeSet === undefined) {
            return undefined;
        }
        for (let i = 1; i < typeRefs.length; i++) {
            if (this._setsToReplaceByMember.get(typeRefIndex(typeRefs[i])) !==
                maybeSet) {
                return undefined;
            }
        }
        // Yes, this set is requested to be replaced, so do it.
        if (!replaceSet)
            return undefined;
        return this.replaceSet(maybeSet, forwardingRef);
    }
}
