"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveIntersections = resolveIntersections;
/* eslint-disable @typescript-eslint/brace-style */
const collection_utils_1 = require("collection-utils");
const TypeAttributes_js_1 = require("../attributes/TypeAttributes.js");
const Support_js_1 = require("../support/Support.js");
const index_js_1 = require("../Type/index.js");
const TypeUtils_js_1 = require("../Type/TypeUtils.js");
const UnionBuilder_js_1 = require("../UnionBuilder.js");
function canResolve(t) {
    const members = (0, TypeUtils_js_1.setOperationMembersRecursively)(t, undefined)[0];
    if (members.size <= 1)
        return true;
    return (0, collection_utils_1.iterableEvery)(members, (m) => !(m instanceof index_js_1.UnionType) || m.isCanonical);
}
function attributesForTypes(types) {
    return (0, collection_utils_1.mapMapEntries)(types.entries(), (t) => [t.kind, t.getAttributes()]);
}
class IntersectionAccumulator {
    constructor() {
        this._primitiveAttributes = new Map();
        this._arrayAttributes = TypeAttributes_js_1.emptyTypeAttributes;
        // We start out with all object types allowed, which means
        // _additionalPropertyTypes is empty - no restrictions - and
        // _classProperties is empty - no defined properties so far.
        //
        // If _additionalPropertyTypes is undefined, no additional
        // properties are allowed anymore.  If _classProperties is
        // undefined, no object types are allowed, in which case
        // _additionalPropertyTypes must also be undefined;
        this._objectProperties = new Map();
        this._objectAttributes = TypeAttributes_js_1.emptyTypeAttributes;
        this._additionalPropertyTypes = new Set();
        this._lostTypeAttributes = false;
    }
    updatePrimitiveTypes(members) {
        const types = (0, collection_utils_1.setFilter)(members, (t) => (0, index_js_1.isPrimitiveTypeKind)(t.kind));
        const attributes = attributesForTypes(types);
        (0, collection_utils_1.mapMergeWithInto)(this._primitiveAttributes, (a, b) => (0, TypeAttributes_js_1.combineTypeAttributes)("intersect", a, b), attributes);
        const kinds = (0, collection_utils_1.setMap)(types, (t) => t.kind);
        if (this._primitiveTypes === undefined) {
            this._primitiveTypes = new Set(kinds);
            return;
        }
        const haveNumber = (0, collection_utils_1.iterableFind)(this._primitiveTypes, index_js_1.isNumberTypeKind) !==
            undefined &&
            (0, collection_utils_1.iterableFind)(kinds, index_js_1.isNumberTypeKind) !== undefined;
        this._primitiveTypes = (0, collection_utils_1.setIntersect)(this._primitiveTypes, kinds);
        if (haveNumber &&
            (0, collection_utils_1.iterableFind)(this._primitiveTypes, index_js_1.isNumberTypeKind) === undefined) {
            // One set has integer, the other has double.  The intersection
            // of that is integer.
            this._primitiveTypes = this._primitiveTypes.add("integer");
        }
    }
    updateArrayItemTypes(members) {
        const maybeArray = (0, collection_utils_1.iterableFind)(members, (t) => t instanceof index_js_1.ArrayType);
        if (maybeArray === undefined) {
            this._arrayItemTypes = false;
            return;
        }
        this._arrayAttributes = (0, TypeAttributes_js_1.combineTypeAttributes)("intersect", this._arrayAttributes, maybeArray.getAttributes());
        if (this._arrayItemTypes === undefined) {
            this._arrayItemTypes = new Set();
        }
        else if (this._arrayItemTypes !== false) {
            this._arrayItemTypes.add(maybeArray.items);
        }
    }
    updateObjectProperties(members) {
        const maybeObject = (0, collection_utils_1.iterableFind)(members, (t) => t instanceof index_js_1.ObjectType);
        if (maybeObject === undefined) {
            this._objectProperties = undefined;
            this._additionalPropertyTypes = undefined;
            return;
        }
        this._objectAttributes = (0, TypeAttributes_js_1.combineTypeAttributes)("intersect", this._objectAttributes, maybeObject.getAttributes());
        const objectAdditionalProperties = maybeObject.getAdditionalProperties();
        if (this._objectProperties === undefined) {
            (0, Support_js_1.assert)(this._additionalPropertyTypes === undefined);
            return;
        }
        const allPropertyNames = (0, collection_utils_1.setUnionInto)(new Set(this._objectProperties.keys()), maybeObject.getProperties().keys());
        for (const name of allPropertyNames) {
            const existing = (0, Support_js_1.defined)(this._objectProperties).get(name);
            const newProperty = maybeObject.getProperties().get(name);
            if (existing !== undefined && newProperty !== undefined) {
                const cp = new index_js_1.GenericClassProperty(existing.typeData.add(newProperty.type), existing.isOptional && newProperty.isOptional);
                (0, Support_js_1.defined)(this._objectProperties).set(name, cp);
            }
            else if (existing !== undefined &&
                objectAdditionalProperties !== undefined) {
                const cp = new index_js_1.GenericClassProperty(existing.typeData.add(objectAdditionalProperties), existing.isOptional);
                (0, Support_js_1.defined)(this._objectProperties).set(name, cp);
            }
            else if (existing !== undefined) {
                (0, Support_js_1.defined)(this._objectProperties).delete(name);
            }
            else if (newProperty !== undefined &&
                this._additionalPropertyTypes !== undefined) {
                // FIXME: This is potentially slow
                const types = new Set(this._additionalPropertyTypes).add(newProperty.type);
                (0, Support_js_1.defined)(this._objectProperties).set(name, new index_js_1.GenericClassProperty(types, newProperty.isOptional));
            }
            else if (newProperty !== undefined) {
                (0, Support_js_1.defined)(this._objectProperties).delete(name);
            }
            else {
                (0, Support_js_1.mustNotHappen)();
            }
        }
        if (this._additionalPropertyTypes !== undefined &&
            objectAdditionalProperties !== undefined) {
            this._additionalPropertyTypes.add(objectAdditionalProperties);
        }
        else if (this._additionalPropertyTypes !== undefined ||
            objectAdditionalProperties !== undefined) {
            this._additionalPropertyTypes = undefined;
            this._lostTypeAttributes = true;
        }
    }
    addUnionSet(members) {
        this.updatePrimitiveTypes(members);
        this.updateArrayItemTypes(members);
        this.updateObjectProperties(members);
    }
    addType(t) {
        let attributes = t.getAttributes();
        (0, TypeUtils_js_1.matchTypeExhaustive)(t, (_noneType) => {
            return (0, Support_js_1.panic)("There shouldn't be a none type");
        }, (_anyType) => {
            return (0, Support_js_1.panic)("The any type should have been filtered out in setOperationMembersRecursively");
        }, (nullType) => this.addUnionSet([nullType]), (boolType) => this.addUnionSet([boolType]), (integerType) => this.addUnionSet([integerType]), (doubleType) => this.addUnionSet([doubleType]), (stringType) => this.addUnionSet([stringType]), (arrayType) => this.addUnionSet([arrayType]), (_classType) => (0, Support_js_1.panic)("We should never see class types in intersections"), (_mapType) => (0, Support_js_1.panic)("We should never see map types in intersections"), (objectType) => this.addUnionSet([objectType]), (_enumType) => (0, Support_js_1.panic)("We should never see enum types in intersections"), (unionType) => {
            attributes = (0, TypeAttributes_js_1.combineTypeAttributes)("intersect", [attributes].concat(Array.from(unionType.members).map((m) => m.getAttributes())));
            this.addUnionSet(unionType.members);
        }, (transformedStringType) => this.addUnionSet([transformedStringType]));
        return (0, TypeAttributes_js_1.makeTypeAttributesInferred)(attributes);
    }
    get arrayData() {
        if (this._arrayItemTypes === undefined ||
            this._arrayItemTypes === false) {
            return (0, Support_js_1.panic)("This should not be called if the type can't be an array");
        }
        return this._arrayItemTypes;
    }
    get objectData() {
        if (this._objectProperties === undefined) {
            (0, Support_js_1.assert)(this._additionalPropertyTypes === undefined);
            return undefined;
        }
        return [this._objectProperties, this._additionalPropertyTypes];
    }
    get enumCases() {
        return (0, Support_js_1.panic)("We don't support enums in intersections");
    }
    getMemberKinds() {
        const kinds = (0, collection_utils_1.mapMap)((0, Support_js_1.defined)(this._primitiveTypes).entries(), (k) => (0, Support_js_1.defined)(this._primitiveAttributes.get(k)));
        const maybeDoubleAttributes = this._primitiveAttributes.get("double");
        // If double was eliminated, add its attributes to integer
        if (maybeDoubleAttributes !== undefined &&
            !kinds.has("double") &&
            kinds.has("integer")) {
            // FIXME: How can this ever happen???  Where do we "eliminate" double?
            (0, collection_utils_1.mapUpdateInto)(kinds, "integer", (a) => {
                return (0, TypeAttributes_js_1.combineTypeAttributes)("intersect", (0, Support_js_1.defined)(a), maybeDoubleAttributes);
            });
        }
        if (this._arrayItemTypes !== undefined &&
            this._arrayItemTypes !== false) {
            kinds.set("array", this._arrayAttributes);
        }
        else if (this._arrayAttributes.size > 0) {
            this._lostTypeAttributes = true;
        }
        if (this._objectProperties !== undefined) {
            kinds.set("object", this._objectAttributes);
        }
        else if (this._objectAttributes.size > 0) {
            this._lostTypeAttributes = true;
        }
        return kinds;
    }
    get lostTypeAttributes() {
        return this._lostTypeAttributes;
    }
}
class IntersectionUnionBuilder extends UnionBuilder_js_1.UnionBuilder {
    constructor() {
        super(...arguments);
        this._createdNewIntersections = false;
    }
    makeIntersection(members, attributes) {
        const reconstitutedMembers = (0, collection_utils_1.setMap)(members, (t) => this.typeBuilder.reconstituteTypeRef(t.typeRef));
        const first = (0, Support_js_1.defined)((0, collection_utils_1.iterableFirst)(reconstitutedMembers));
        if (reconstitutedMembers.size === 1) {
            this.typeBuilder.addAttributes(first, attributes);
            return first;
        }
        this._createdNewIntersections = true;
        return this.typeBuilder.getUniqueIntersectionType(attributes, reconstitutedMembers);
    }
    get createdNewIntersections() {
        return this._createdNewIntersections;
    }
    makeObject(maybeData, typeAttributes, forwardingRef) {
        if (maybeData === undefined) {
            return (0, Support_js_1.panic)("Either properties or additional properties must be given to make an object type");
        }
        const [propertyTypes, maybeAdditionalProperties] = maybeData;
        const properties = (0, collection_utils_1.mapMap)(propertyTypes, (cp) => this.typeBuilder.makeClassProperty(this.makeIntersection(cp.typeData, TypeAttributes_js_1.emptyTypeAttributes), cp.isOptional));
        const additionalProperties = maybeAdditionalProperties === undefined
            ? undefined
            : this.makeIntersection(maybeAdditionalProperties, TypeAttributes_js_1.emptyTypeAttributes);
        return this.typeBuilder.getUniqueObjectType(typeAttributes, properties, additionalProperties, forwardingRef);
    }
    makeArray(arrays, typeAttributes, forwardingRef) {
        // FIXME: attributes
        const itemsType = this.makeIntersection(arrays, TypeAttributes_js_1.emptyTypeAttributes);
        const tref = this.typeBuilder.getArrayType(typeAttributes, itemsType, forwardingRef);
        return tref;
    }
}
function resolveIntersections(graph, stringTypeMapping, debugPrintReconstitution) {
    let needsRepeat = false;
    function replace(types, builder, forwardingRef) {
        const intersections = (0, collection_utils_1.setFilter)(types, (t) => t instanceof index_js_1.IntersectionType);
        const [members, intersectionAttributes] = (0, TypeUtils_js_1.setOperationMembersRecursively)(Array.from(intersections), "intersect");
        if (members.size === 0) {
            const t = builder.getPrimitiveType("any", intersectionAttributes, forwardingRef);
            return t;
        }
        if (members.size === 1) {
            return builder.reconstituteType((0, Support_js_1.defined)((0, collection_utils_1.iterableFirst)(members)), intersectionAttributes, forwardingRef);
        }
        const accumulator = new IntersectionAccumulator();
        const extraAttributes = (0, TypeAttributes_js_1.makeTypeAttributesInferred)((0, TypeAttributes_js_1.combineTypeAttributes)("intersect", Array.from(members).map((t) => accumulator.addType(t))));
        const attributes = (0, TypeAttributes_js_1.combineTypeAttributes)("intersect", intersectionAttributes, extraAttributes);
        const unionBuilder = new IntersectionUnionBuilder(builder);
        const tref = unionBuilder.buildUnion(accumulator, true, attributes, forwardingRef);
        if (unionBuilder.createdNewIntersections) {
            needsRepeat = true;
        }
        return tref;
    }
    // FIXME: We need to handle intersections that resolve to the same set of types.
    // See for example the intersections-nested.schema example.
    const allIntersections = (0, collection_utils_1.setFilter)(graph.allTypesUnordered(), (t) => t instanceof index_js_1.IntersectionType);
    const resolvableIntersections = (0, collection_utils_1.setFilter)(allIntersections, canResolve);
    const groups = (0, TypeUtils_js_1.makeGroupsToFlatten)(resolvableIntersections, undefined);
    graph = graph.rewrite("resolve intersections", stringTypeMapping, false, groups, debugPrintReconstitution, replace);
    // console.log(`resolved ${resolvableIntersections.size} of ${intersections.size} intersections`);
    return [
        graph,
        !needsRepeat && allIntersections.size === resolvableIntersections.size,
    ];
}
