"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.posetLte = exports.comparePosetElements = exports.PosetComparisonResult = exports.fieldRealizer = exports.isRepoSuperset = exports.getAllowedContentDiscrepancies = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const index_js_1 = require("../../core/index.js");
const index_js_2 = require("../../util/index.js");
function getNodeSchemaType(nodeSchema) {
    if (nodeSchema instanceof index_js_1.ObjectNodeStoredSchema) {
        return "object";
    }
    else if (nodeSchema instanceof index_js_1.MapNodeStoredSchema) {
        return "map";
    }
    else if (nodeSchema instanceof index_js_1.LeafNodeStoredSchema) {
        return "leaf";
    }
    throwUnsupportedNodeType(nodeSchema.constructor.name);
}
/**
 * Finds and reports discrepancies between a view schema and a stored schema.
 *
 * See documentation on {@link Discrepancy} for details of possible discrepancies.
 * @remarks
 * This function does not attempt to distinguish between equivalent representations of a node/field involving extraneous never trees.
 * For example, a Forbidden field with allowed type set `[]` is equivalent to an optional field with allowed type set `[]`,
 * as well as an optional field with an allowed type set containing only unconstructable types.
 *
 * It is up to the caller to determine whether such discrepancies matter.
 */
function* getAllowedContentDiscrepancies(view, stored) {
    // check root schema discrepancies
    yield* getFieldDiscrepancies(view.rootFieldSchema, stored.rootFieldSchema, undefined, undefined);
    for (const result of compareMaps(view.nodeSchema, stored.nodeSchema)) {
        switch (result.type) {
            case "aExtra": {
                const viewType = getNodeSchemaType(result.value);
                yield {
                    identifier: result.key,
                    mismatch: "nodeKind",
                    view: viewType,
                    stored: undefined,
                };
                break;
            }
            case "bExtra": {
                const storedType = getNodeSchemaType(result.value);
                yield {
                    identifier: result.key,
                    mismatch: "nodeKind",
                    view: undefined,
                    stored: storedType,
                };
                break;
            }
            case "both": {
                yield* getNodeDiscrepancies(result.key, result.valueA, result.valueB);
                break;
            }
            default:
                break;
        }
    }
}
exports.getAllowedContentDiscrepancies = getAllowedContentDiscrepancies;
function* getNodeDiscrepancies(identifier, view, stored) {
    const viewType = getNodeSchemaType(view);
    const storedType = getNodeSchemaType(stored);
    if (viewType !== storedType) {
        yield {
            identifier,
            mismatch: "nodeKind",
            view: viewType,
            stored: storedType,
        };
        return;
    }
    switch (viewType) {
        case "object": {
            const differences = Array.from(trackObjectNodeDiscrepancies(identifier, view, stored));
            if (differences.length > 0) {
                yield {
                    identifier,
                    mismatch: "fields",
                    differences,
                };
            }
            break;
        }
        case "map":
            yield* getFieldDiscrepancies(view.mapFields, stored.mapFields, identifier, undefined);
            break;
        case "leaf": {
            const viewValue = view.leafValue;
            const storedValue = stored.leafValue;
            if (viewValue !== storedValue) {
                yield {
                    identifier,
                    mismatch: "valueSchema",
                    view: viewValue,
                    stored: storedValue,
                };
            }
            break;
        }
        default:
            break;
    }
}
/**
 * The function to track the discrepancies between two field stored schemas.
 *
 * @param keyOrRoot - If the key is missing, it indicates that this is the root field schema.
 */
function* getFieldDiscrepancies(view, stored, identifier, fieldKey) {
    // Only track the symmetric differences of two sets.
    const findSetDiscrepancies = (a, b) => {
        const aDiff = [...a].filter((value) => !b.has(value));
        const bDiff = [...b].filter((value) => !a.has(value));
        return [aDiff, bDiff];
    };
    const [viewExtra, storedExtra] = findSetDiscrepancies(view.types, stored.types);
    if (viewExtra.length > 0 || storedExtra.length > 0) {
        yield {
            identifier,
            fieldKey,
            mismatch: "allowedTypes",
            view: viewExtra,
            stored: storedExtra,
        };
    }
    if (view.kind !== stored.kind) {
        yield {
            identifier,
            fieldKey,
            mismatch: "fieldKind",
            view: view.kind,
            stored: stored.kind,
        };
    }
}
function* trackObjectNodeDiscrepancies(identifier, view, stored) {
    /**
     * Similar to the logic used for tracking discrepancies between two node schemas, we will identify
     * three types of differences:
     * 1. Fields that exist in the view schema but not in the stored schema.
     * 2. Fields that exist in both schemas but have different contents.
     * 3. Fields that exist in the stored schema but not in the view schema.
     *
     * First, the view schema is iterated to track the first two types of differences.
     * Then, the stored schema is iterated to find the third type.
     */
    for (const result of compareMaps(view.objectNodeFields, stored.objectNodeFields)) {
        const fieldKey = result.key;
        switch (result.type) {
            case "aExtra": {
                if (result.value.kind === index_js_1.storedEmptyFieldSchema.kind) {
                    // In one of view/stored, this field is explicitly forbidden, but in the other it is implicitly forbidden
                    // (by way of omission). We treat these identically anyway.
                    break;
                }
                yield {
                    identifier,
                    fieldKey,
                    mismatch: "fieldKind",
                    view: result.value.kind,
                    stored: index_js_1.storedEmptyFieldSchema.kind,
                };
                break;
            }
            case "bExtra": {
                if (result.value.kind === index_js_1.storedEmptyFieldSchema.kind) {
                    // In one of view/stored, this field is explicitly forbidden, but in the other it is implicitly forbidden
                    // (by way of omission). We treat these identically anyway.
                    break;
                }
                yield {
                    identifier,
                    fieldKey,
                    mismatch: "fieldKind",
                    view: index_js_1.storedEmptyFieldSchema.kind,
                    stored: result.value.kind,
                };
                break;
            }
            case "both": {
                yield* getFieldDiscrepancies(result.valueA, result.valueB, identifier, fieldKey);
                break;
            }
            default:
                break;
        }
    }
}
function* compareMaps(a, b) {
    for (const [key, valueA] of a) {
        const valueB = b.get(key);
        yield valueB === undefined
            ? { type: "aExtra", key, value: valueA }
            : { type: "both", key, valueA, valueB };
    }
    for (const [key, valueB] of b) {
        if (!a.has(key)) {
            yield { type: "bExtra", key, value: valueB };
        }
    }
}
/**
 * @remarks
 *
 * This function uses incompatibilities to determine if changes to a document schema are backward-compatible, i.e., it determines
 * whether the `view` schema allows a superset of the documents that the `stored` schema allows.
 * According to the policy of schema evolution, `isRepoSuperset` supports three types of changes:
 * 1. Adding an optional field to an object node.
 * 2. Expanding the set of allowed types for a field.
 * 3. Relaxing a field kind to a more general field kind.
 *
 * Notes: We expect isRepoSuperset to return consistent results with allowsRepoSuperset. However, currently there are some scenarios
 * where the inconsistency will occur:
 *
 * - Different Node Kinds: If a and b have different node kinds (e.g., a is an objectNodeSchema and b is a mapNodeSchema),
 * `isRepoSuperset` will determine that a can never be the superset of b. In contrast, `allowsRepoSuperset` will continue
 * validating internal fields.
 */
function isRepoSuperset(view, stored) {
    const discrepancies = getAllowedContentDiscrepancies(view, stored);
    for (const discrepancy of discrepancies) {
        switch (discrepancy.mismatch) {
            case "nodeKind": {
                if (discrepancy.stored !== undefined) {
                    // It's fine for the view schema to know of a node type that the stored schema doesn't know about.
                    return false;
                }
                break;
            }
            case "valueSchema":
            case "allowedTypes":
            case "fieldKind": {
                if (!isFieldDiscrepancyCompatible(discrepancy)) {
                    return false;
                }
                break;
            }
            case "fields": {
                if (discrepancy.differences.some((difference) => !isFieldDiscrepancyCompatible(difference))) {
                    return false;
                }
                break;
            }
            // No default
        }
    }
    return true;
}
exports.isRepoSuperset = isRepoSuperset;
function isFieldDiscrepancyCompatible(discrepancy) {
    switch (discrepancy.mismatch) {
        case "allowedTypes": {
            // Since we only track the symmetric difference between the allowed types in the view and
            // stored schemas, it's sufficient to check if any extra allowed types still exist in the
            // stored schema.
            return discrepancy.stored.length === 0;
        }
        case "fieldKind": {
            return posetLte(discrepancy.stored, discrepancy.view, exports.fieldRealizer);
        }
        case "valueSchema": {
            return false;
        }
        // No default
    }
    return false;
}
/**
 * @privateRemarks
 * TODO: Knowledge of specific field kinds is not appropriate for modular schema.
 * This bit of field comparison should be dependency injected by default-schema if this comparison logic remains in modular-schema
 * (this is analogous to what is done in comparison.ts).
 */
const FieldKindIdentifiers = {
    forbidden: (0, index_js_2.brand)("Forbidden"),
    required: (0, index_js_2.brand)("Value"),
    identifier: (0, index_js_2.brand)("Identifier"),
    optional: (0, index_js_2.brand)("Optional"),
    sequence: (0, index_js_2.brand)("Sequence"),
};
/**
 * A realizer for the partial order of field kind relaxability.
 *
 * It seems extremely likely that this partial order will remain dimension 2 over time (i.e. the set of allowed relaxations can be visualized
 * with a [dominance drawing](https://en.wikipedia.org/wiki/Dominance_drawing)), so this strategy allows efficient comarison between field kinds
 * without excessive casework.
 *
 * Hasse diagram for the partial order is shown below (lower fields can be relaxed to higher fields):
 * ```
 * sequence
 *    |
 * optional
 *    |    \
 * required forbidden
 *    |
 * identifier
 * ```
 */
exports.fieldRealizer = [
    [
        FieldKindIdentifiers.forbidden,
        FieldKindIdentifiers.identifier,
        FieldKindIdentifiers.required,
        FieldKindIdentifiers.optional,
        FieldKindIdentifiers.sequence,
    ],
    [
        FieldKindIdentifiers.identifier,
        FieldKindIdentifiers.required,
        FieldKindIdentifiers.forbidden,
        FieldKindIdentifiers.optional,
        FieldKindIdentifiers.sequence,
    ],
].map((extension) => new Map(extension.map((identifier, index) => [identifier, index])));
exports.PosetComparisonResult = {
    Less: "<",
    Greater: ">",
    Equal: "=",
    Incomparable: "||",
};
function comparePosetElements(a, b, realizer) {
    let hasLessThanResult = false;
    let hasGreaterThanResult = false;
    for (const extension of realizer) {
        const aIndex = extension.get(a);
        const bIndex = extension.get(b);
        (0, internal_1.assert)(aIndex !== undefined && bIndex !== undefined, 0xa72 /* Invalid realizer */);
        if (aIndex < bIndex) {
            hasLessThanResult = true;
        }
        else if (aIndex > bIndex) {
            hasGreaterThanResult = true;
        }
    }
    return hasLessThanResult
        ? hasGreaterThanResult
            ? exports.PosetComparisonResult.Incomparable
            : exports.PosetComparisonResult.Less
        : hasGreaterThanResult
            ? exports.PosetComparisonResult.Greater
            : exports.PosetComparisonResult.Equal;
}
exports.comparePosetElements = comparePosetElements;
function posetLte(a, b, realizer) {
    const comparison = comparePosetElements(a, b, realizer);
    return (comparison === exports.PosetComparisonResult.Less || comparison === exports.PosetComparisonResult.Equal);
}
exports.posetLte = posetLte;
function throwUnsupportedNodeType(type) {
    throw new TypeError(`Unsupported node stored schema type: ${type}`);
}
//# sourceMappingURL=discrepancies.js.map