"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkUnion = exports.TreeViewConfiguration = void 0;
const internal_1 = require("@fluidframework/telemetry-utils/internal");
const schemaTypes_js_1 = require("../schemaTypes.js");
const index_js_1 = require("../core/index.js");
const toFlexSchema_js_1 = require("../toFlexSchema.js");
const leafNodeSchema_js_1 = require("../leafNodeSchema.js");
const internal_2 = require("@fluidframework/core-utils/internal");
const objectNodeTypes_js_1 = require("../objectNodeTypes.js");
const schemaFactory_js_1 = require("./schemaFactory.js");
const index_js_2 = require("../../util/index.js");
const walkSchema_js_1 = require("../walkSchema.js");
const defaultTreeConfigurationOptions = {
    enableSchemaValidation: false,
    preventAmbiguity: false,
};
/**
 * Configuration for {@link ITree.viewWith}.
 * @sealed @public
 */
class TreeViewConfiguration {
    /**
     * @param props - Property bag of configuration options.
     */
    constructor(props) {
        const config = { ...defaultTreeConfigurationOptions, ...props };
        this.schema = config.schema;
        this.enableSchemaValidation = config.enableSchemaValidation;
        this.preventAmbiguity = config.preventAmbiguity;
        // Ambiguity errors are lower priority to report than invalid schema errors, so collect these in an array and report them all at once.
        const ambiguityErrors = [];
        (0, walkSchema_js_1.walkFieldSchema)(config.schema, {
            // Ensure all reachable schema are marked as most derived.
            // This ensures if multiple schema extending the same schema factory generated class are present (or have been constructed, or get constructed in the future),
            // an error is reported.
            node: schemaFactory_js_1.markSchemaMostDerived,
            allowedTypes(types) {
                if (config.preventAmbiguity) {
                    checkUnion(types, ambiguityErrors);
                }
            },
        });
        if (ambiguityErrors.length !== 0) {
            // Duplicate errors are common since when two types conflict, both orders error:
            const deduplicated = new Set(ambiguityErrors);
            throw new internal_1.UsageError(`Ambigious schema found:\n${[...deduplicated].join("\n")}`);
        }
        // Eagerly perform this conversion to surface errors sooner.
        (0, toFlexSchema_js_1.toFlexSchema)(config.schema);
    }
}
exports.TreeViewConfiguration = TreeViewConfiguration;
/**
 * Pretty print a set of types for use in error messages.
 */
function formatTypes(allowed) {
    // Use JSON.stringify to quote and escape identifiers.
    // Don't just use a single array JSON.stringify since that omits spaces between items
    return `[${Array.from(allowed, (s) => JSON.stringify(s.identifier)).join(", ")}]`;
}
/**
 * Detect cases documented in {@link ITreeConfigurationOptions.preventAmbiguity}.
 */
function checkUnion(union, errors) {
    const checked = new Set();
    const maps = [];
    const arrays = [];
    const objects = [];
    // Map from key to schema using that key
    const allObjectKeys = new Map();
    for (const schema of union) {
        if (checked.has(schema)) {
            throw new internal_1.UsageError(`Duplicate schema in allowed types: ${schema.identifier}`);
        }
        checked.add(schema);
        if (schema instanceof leafNodeSchema_js_1.LeafNodeSchema) {
            // nothing to do
        }
        else if ((0, objectNodeTypes_js_1.isObjectNodeSchema)(schema)) {
            objects.push(schema);
            for (const key of schema.fields.keys()) {
                (0, index_js_2.getOrCreate)(allObjectKeys, key, () => new Set()).add(schema);
            }
        }
        else if (schema.kind === index_js_1.NodeKind.Array) {
            arrays.push(schema);
        }
        else {
            (0, internal_2.assert)(schema.kind === index_js_1.NodeKind.Map, 0x9e7 /* invalid schema */);
            maps.push(schema);
        }
    }
    if (arrays.length > 1) {
        errors.push(`More than one kind of array allowed within union (${formatTypes(arrays)}). This would require type disambiguation which is not supported by arrays during import or export.`);
    }
    if (maps.length > 1) {
        errors.push(`More than one kind of map allowed within union (${formatTypes(maps)}). This would require type disambiguation which is not supported by maps during import or export.`);
    }
    if (maps.length > 0 && arrays.length > 0) {
        errors.push(`Both a map and an array allowed within union (${formatTypes([...arrays, ...maps])}). Both can be implicitly constructed from iterables like arrays, which are ambiguous when the array is empty.`);
    }
    if (objects.length > 0 && maps.length > 0) {
        errors.push(`Both a object and a map allowed within union (${formatTypes([...objects, ...maps])}). Both can be constructed from objects and can be ambiguous.`);
    }
    // Check for objects which fully overlap:
    for (const schema of objects) {
        // All objects which might be ambiguous relative to `schema`.
        const possiblyAmbiguous = new Set(objects);
        // A schema can't be ambiguous with itself
        possiblyAmbiguous.delete(schema);
        // For each field of schema, remove schema from possiblyAmbiguous that do not have that field
        for (const [key, field] of schema.fields) {
            if (field.kind === schemaTypes_js_1.FieldKind.Required) {
                const withKey = allObjectKeys.get(key) ?? (0, index_js_2.fail)("missing schema");
                for (const candidate of possiblyAmbiguous) {
                    if (!withKey.has(candidate)) {
                        possiblyAmbiguous.delete(candidate);
                    }
                }
            }
        }
        if (possiblyAmbiguous.size > 0) {
            // TODO: make this check more permissive.
            // Allow using the type of the field to disambiguate, at least for leaf types.
            // Add "constant" fields which can be used to disambiguate even more cases without adding persisted data: maybe make them optional in constructor?
            // Consider separating unambiguous implicit construction format from constructor arguments at type level, allowing constructor to superset the implicit construction options (ex: optional constant fields).
            // The policy here however must remain at least as conservative as shallowCompatibilityTest in src/simple-tree/toMapTree.ts.
            errors.push(`The required fields of ${JSON.stringify(schema.identifier)} are insufficient to differentiate it from the following types: ${formatTypes(possiblyAmbiguous)}. For objects to be considered unambiguous, each must have required fields that do not all occur on any other object in the union.`);
        }
    }
}
exports.checkUnion = checkUnion;
//# sourceMappingURL=tree.js.map