/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, fail, unreachableCase } from "@fluidframework/core-utils/internal";
import { EmptyKey, forbiddenFieldKindIdentifier, LeafNodeStoredSchema, MapNodeStoredSchema, ObjectNodeStoredSchema, storedEmptyFieldSchema, } from "../../core/index.js";
import { FieldKinds } from "../../feature-libraries/index.js";
import { brand } from "../../util/index.js";
import { NodeKind, normalizeAndEvaluateAnnotatedAllowedTypes, } from "../core/index.js";
import { createFieldSchema, FieldKind, FieldSchemaAlpha, } from "../fieldSchema.js";
import { LeafNodeSchema } from "../leafNodeSchema.js";
import { isArrayNodeSchema, isMapNodeSchema, isObjectNodeSchema, isRecordNodeSchema, } from "../node-kinds/index.js";
import { convertFieldKind } from "../toStoredSchema.js";
import { tryStoredSchemaAsArray } from "./customTree.js";
function getStoredNodeSchemaType(nodeSchema) {
    if (nodeSchema instanceof ObjectNodeStoredSchema) {
        return ObjectNodeStoredSchema;
    }
    if (nodeSchema instanceof MapNodeStoredSchema) {
        return MapNodeStoredSchema;
    }
    if (nodeSchema instanceof LeafNodeStoredSchema) {
        return LeafNodeStoredSchema;
    }
    fail(0xbe8 /* Invalid stored node schema type */);
}
function doesNodeKindMatchStoredNodeKind(viewKind, storedType) {
    switch (viewKind) {
        case NodeKind.Leaf: {
            return storedType === LeafNodeStoredSchema;
        }
        case NodeKind.Array:
        case NodeKind.Object: {
            return storedType === ObjectNodeStoredSchema;
        }
        case NodeKind.Map:
        case NodeKind.Record: {
            return storedType === MapNodeStoredSchema;
        }
        default: {
            unreachableCase(viewKind);
        }
    }
}
/**
 * Finds and reports discrepancies between a view schema and a stored schema which make "canView" false.
 * @remarks
 * See documentation on {@link Discrepancy} and its subtypes for details of possible discrepancies.
 */
export function* getDiscrepanciesInAllowedContent(view, stored) {
    // check root field discrepancies
    yield* getFieldDiscrepancies(view.root, stored.rootFieldSchema, undefined, undefined);
    // Check all of the stored nodes, including their fields for discrepancies.
    for (const [identifier, storedSchema] of stored.nodeSchema) {
        const viewSchema = view.definitions.get(identifier);
        // if the view schema has a node that's also in the stored schema, check it.
        if (viewSchema !== undefined) {
            yield* getNodeDiscrepancies(identifier, viewSchema, storedSchema);
        }
        // Note that nodes that are missing in the view schema are only a problem if other stored schema nodes actually reference them which will produce its own discrepancy, so we can rely on that to produce any needed discrepancies.
    }
}
function* getNodeDiscrepancies(identifier, view, stored) {
    if (!doesNodeKindMatchStoredNodeKind(view.kind, getStoredNodeSchemaType(stored))) {
        yield {
            identifier,
            mismatch: "nodeKind",
            view: view.kind,
            stored: getStoredNodeSchemaType(stored),
        };
        return;
    }
    switch (view.kind) {
        case NodeKind.Object: {
            assert(isObjectNodeSchema(view), 0xbe9 /* schema with node kind of object must implement ObjectNodeSchema */);
            yield* computeObjectNodeDiscrepancies(identifier, view, stored);
            break;
        }
        case NodeKind.Array: {
            assert(isArrayNodeSchema(view), 0xbea /* schema with node kind of array must implement ArrayNodeSchema */);
            const arrayStoredSchema = tryStoredSchemaAsArray(stored);
            if (arrayStoredSchema === undefined) {
                yield {
                    identifier,
                    mismatch: "nodeKind",
                    view: NodeKind.Array,
                    stored: getStoredNodeSchemaType(stored),
                };
                return;
            }
            yield* getAllowedTypeDiscrepancies(normalizeAndEvaluateAnnotatedAllowedTypes(view.info).types, arrayStoredSchema, brand(view.identifier), EmptyKey);
            break;
        }
        case NodeKind.Map: {
            assert(isMapNodeSchema(view), 0xbeb /* schema with node kind of map must implement MapNodeSchema */);
            yield* getFieldDiscrepancies(createFieldSchema(FieldKind.Optional, view.info), stored.mapFields, identifier, undefined);
            break;
        }
        case NodeKind.Record: {
            assert(isRecordNodeSchema(view), 0xbec /* schema with node kind of record must implement RecordNodeSchema */);
            yield* getFieldDiscrepancies(createFieldSchema(FieldKind.Optional, view.info), stored.mapFields, identifier, undefined);
            break;
        }
        case NodeKind.Leaf: {
            assert(view instanceof LeafNodeSchema, 0xbed /* schema with node kind of leaf must implement LeafNodeSchema */);
            // TODO: leafKind seems like a bad name
            const viewValue = view.leafKind;
            const storedValue = stored.leafValue;
            if (viewValue !== storedValue) {
                yield {
                    identifier,
                    mismatch: "valueSchema",
                    view: viewValue,
                    stored: storedValue,
                };
            }
            break;
        }
        default: {
            break;
        }
    }
}
/**
 * Returns the allowed types that are not present in both the given view and stored schemas.
 */
export function findExtraAllowedTypes(viewAllowedTypes, storedAllowedTypes) {
    const viewNodeSchemaIdentifiers = new Set(viewAllowedTypes.map((value) => value.type.identifier));
    // TODO:
    // Add detecting which SchemaUpgrades have been made.
    // When doing so, each will fall into one of three states: staged, partially applied, or fully applied.
    // This would allow for nicer compatibility errors and more detailed compatibility status in the future.
    const view = viewAllowedTypes.filter((value) => !storedAllowedTypes.has(brand(value.type.identifier)) &&
        value.metadata.stagedSchemaUpgrade === undefined);
    const stored = [...storedAllowedTypes].filter((value) => !viewNodeSchemaIdentifiers.has(value));
    return { view, stored };
}
/**
 * The function to track the discrepancies between a field view schema and a stored schema.
 *
 * @remarks
 * This function yields discrepancies in the following cases:
 * 1. If the view schema has allowed types that are not present in the stored schema.
 * 2. If the stored schema has allowed types that are not present in the view schema.
 * 3. If the field kind in the view schema is not compatible with the stored schema.
 *
 * This function does not recurse into the nodes of the view schema and only makes comparisons at the field level.
 *
 * @param fieldKey - If the key is missing, it indicates that this is the root field schema.
 */
function* getFieldDiscrepancies(view, stored, identifier, fieldKey) {
    assert(view instanceof FieldSchemaAlpha, 0xbee /* all field schema should be FieldSchemaAlpha */);
    yield* getAllowedTypeDiscrepancies(view.allowedTypesFull.evaluate().types, stored.types, identifier, fieldKey);
    const viewKind = convertFieldKind.get(view.kind) ??
        fail(0xbef /* A conversion from a FieldKind to a FlexFieldKind should exist */);
    // This checks if the field kind in the view schema is not compatible with the stored schema.
    // A staged optional field (optional in the view) is compatible with a required stored field
    // during the rollout period — suppress the kind discrepancy in that case.
    if (viewKind.identifier !== stored.kind &&
        !(view.isStagedOptional !== false && stored.kind === FieldKinds.required.identifier)) {
        yield {
            identifier,
            fieldKey,
            mismatch: "fieldKind",
            view: viewKind.identifier,
            stored: stored.kind,
        };
    }
}
/**
 * The function to track the discrepancies between a field view schema and a stored schema.
 *
 * @remarks
 * This function yields discrepancies in the following cases:
 * 1. If the view schema has allowed types that are not present in the stored schema.
 * 2. If the stored schema has allowed types that are not present in the view schema.
 *
 * This function does not recurse into the nodes of the view schema and only makes comparisons at the field level.
 *
 * @param fieldKey - If the key is missing, it indicates that this is the root field schema.
 */
function* getAllowedTypeDiscrepancies(view, stored, identifier, fieldKey) {
    const discrepancies = findExtraAllowedTypes(view, stored);
    if (discrepancies.view.length > 0 || discrepancies.stored.length > 0) {
        yield {
            ...discrepancies,
            identifier,
            fieldKey,
            mismatch: "allowedTypes",
        };
    }
}
/**
 * Computes discrepancies between a view schema and a stored schema for nodes that are treated as object nodes in the stored schema.
 * This includes both view object nodes and view array nodes.
 *
 * This function yields discrepancies in the following cases:
 *
 * 1. If the view schema has fields that are not present in the stored schema.
 * 2. If the stored schema has fields that are not present in the view schema.
 * 3. If the field kind or allowed types of a field in the view schema is not compatible with the stored schema.
 *
 * This function includes discrepancies within the common fields and their allowed types, but does NOT recurse to report and discrepancies within the node types referenced by those fields.
 */
function* computeObjectNodeDiscrepancies(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.
     */
    const viewKeys = new Set();
    for (const [_, { storedKey: fieldKey, schema: fieldSchema }] of view.flexKeyMap) {
        const storedSchema = stored.objectNodeFields.get(fieldKey);
        viewKeys.add(fieldKey);
        // If the view schema has a field that's not in the stored schema
        if (storedSchema === undefined) {
            const viewKind = convertFieldKind.get(fieldSchema.kind) ??
                fail(0xbf0 /* A conversion from a FieldKind to a FlexFieldKind should exist */);
            yield {
                identifier,
                fieldKey,
                mismatch: "fieldKind",
                view: viewKind.identifier,
                stored: storedEmptyFieldSchema.kind,
            };
        }
        else {
            yield* getFieldDiscrepancies(fieldSchema, storedSchema, identifier, fieldKey);
        }
    }
    for (const [fieldKey, schema] of stored.objectNodeFields) {
        if (schema.kind === forbiddenFieldKindIdentifier) {
            // In the stored schema the field is explicitly forbidden.
            // This has the same semantics of the field not being mentioned in the stored schema,
            // and thus can be skipped.
            continue;
        }
        // If the stored schema has a field that's not in the view schema
        // When the application has opted into it, we allow viewing documents which have additional
        // optional fields in the stored schema that are not present in the view schema.
        if (!viewKeys.has(fieldKey) &&
            (!view.allowUnknownOptionalFields || schema.kind !== FieldKinds.optional.identifier)) {
            yield {
                identifier,
                fieldKey,
                mismatch: "fieldKind",
                view: storedEmptyFieldSchema.kind,
                stored: schema.kind,
            };
        }
    }
}
//# sourceMappingURL=discrepancies.js.map