/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, fail, oob, unreachableCase } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { getOrCreate } from "../../util/index.js";
import { NodeKind, } from "../core/index.js";
import { FieldKind } from "../fieldSchema.js";
import { isArrayNodeSchema, isMapNodeSchema, isObjectNodeSchema, isRecordNodeSchema, } from "../node-kinds/index.js";
import { toInitialSchema, toUnhydratedSchema, transformSimpleSchema, } from "../toStoredSchema.js";
import { createTreeSchema } from "../treeSchema.js";
import { walkFieldSchema } from "../walkFieldSchema.js";
const defaultTreeConfigurationOptions = {
    enableSchemaValidation: false,
    preventAmbiguity: false,
};
/**
 * Configuration for {@link ViewableTree.viewWith}.
 * @sealed @public
 */
export class TreeViewConfiguration {
    /**
     * Construct a new {@link TreeViewConfiguration}.
     *
     * @param props - Property bag of configuration options.
     *
     * @remarks
     * Performing this construction deeply validates the provided schema.
     * This means that when this constructor is called, all {@link LazyItem} {@link TreeNodeSchema} references will be evaluated (using {@link evaluateLazySchema}).
     * This means that the declarations for all transitively reachable {@link TreeNodeSchema} must be available at this time.
     *
     * For example, a schema reachable from this configuration cannot reference this configuration during its declaration,
     * since this would be a cyclic dependency that will cause an error when constructing this configuration.
     */
    constructor(props) {
        if (this.constructor === TreeViewConfiguration) {
            // Ensure all TreeViewConfiguration instances are actually TreeViewConfigurationAlpha, allowing `asAlpha` to work correctly.
            // If everything in TreeViewConfigurationAlpha is stabilized and this is removed, the `!` on the properties above should be removed to restore better type safety.
            return new TreeViewConfigurationAlpha(props);
        }
        assert(
        // The type cast here is needed to avoid this assert narrowing "this" to never, breaking the code below.
        this.constructor === TreeViewConfigurationAlpha, 0xc9e /* Invalid configuration class constructed. */);
        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 = [];
        // Validate the schema and collect ambiguity errors.
        // This does a lot of validation (throwing usage errors as a side effect) in addition to just collecting ambiguity errors.
        // ambiguityErrors are considered a lower priority, so only thrown if no other errors are found.
        walkFieldSchema(config.schema, {
            allowedTypes({ types }) {
                checkUnion(types.map((t) => t.type), config.preventAmbiguity, ambiguityErrors);
            },
        });
        if (ambiguityErrors.length > 0) {
            // Duplicate errors are common since when two types conflict, both orders error:
            const deduplicated = new Set(ambiguityErrors);
            throw new UsageError(`Ambiguous schema found:\n${[...deduplicated].join("\n")}`);
        }
    }
}
/**
 * {@link TreeViewConfiguration} extended with some alpha APIs.
 * @remarks
 * See {@link (asAlpha:2)} for an API to downcast from {@link TreeViewConfiguration} to this type.
 * @sealed @alpha
 */
export class TreeViewConfigurationAlpha extends TreeViewConfiguration {
    constructor(props) {
        super(props);
        const treeSchema = createTreeSchema(this.schema);
        this.root = treeSchema.root;
        this.definitions = treeSchema.definitions;
        // Eagerly perform these conversions to surface errors sooner.
        toInitialSchema(this.root);
        transformSimpleSchema(treeSchema, toUnhydratedSchema);
    }
}
/**
 * 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(", ")}]`;
}
/**
 * Check if union contents are valid (shallowly).
 *
 * @param union - The union of {@link TreeNodeSchema} to check.
 * @param preventAmbiguity - If true, detect cases documented in {@link ITreeConfigurationOptions.preventAmbiguity}, reporting them to `ambiguityErrors`.
 * @param ambiguityErrors - An array into which this function inserts any ambiguity errors, see {@link ITreeConfigurationOptions.preventAmbiguity}.
 *
 * @remarks
 * Includes checks for non-ambiguity errors as well: such as duplicate schemas in the union.
 * Any non-ambiguity errors are thrown as exceptions: `UsageError`s if causable by incorrect API use, and asserts if violating internal invariants.
 */
export function checkUnion(union, preventAmbiguity, ambiguityErrors) {
    const checked = new Set();
    const maps = [];
    const arrays = [];
    const records = [];
    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 UsageError(`Duplicate schema in allowed types: ${schema.identifier}`);
        }
        checked.add(schema);
        switch (schema.kind) {
            case NodeKind.Leaf: {
                // nothing to do
                break;
            }
            case NodeKind.Object: {
                assert(isObjectNodeSchema(schema), 0xbde /* Expected object schema. */);
                objects.push(schema);
                for (const key of schema.fields.keys()) {
                    getOrCreate(allObjectKeys, key, () => new Set()).add(schema);
                }
                break;
            }
            case NodeKind.Array: {
                assert(isArrayNodeSchema(schema), 0xbdf /* Expected array schema. */);
                arrays.push(schema);
                break;
            }
            case NodeKind.Map: {
                assert(isMapNodeSchema(schema), 0xbe0 /* Expected map schema. */);
                maps.push(schema);
                break;
            }
            case NodeKind.Record: {
                assert(isRecordNodeSchema(schema), 0xbe1 /* Expected record schema. */);
                records.push(schema);
                break;
            }
            default: {
                unreachableCase(schema.kind);
            }
        }
    }
    if (!preventAmbiguity) {
        // All remaining checks are for the preventAmbiguity case, so skip them if not enabled.
        return;
    }
    if (arrays.length > 1) {
        ambiguityErrors.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) {
        ambiguityErrors.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 (records.length > 1) {
        ambiguityErrors.push(`More than one kind of record allowed within union (${formatTypes(records)}). This would require type disambiguation which is not supported by records during import or export.`);
    }
    if (maps.length > 0 && arrays.length > 0) {
        ambiguityErrors.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.`);
    }
    const nodeKindListEntries = [];
    if (objects.length > 0) {
        nodeKindListEntries.push("objects");
    }
    if (maps.length > 0) {
        nodeKindListEntries.push("maps");
    }
    if (records.length > 0) {
        nodeKindListEntries.push("records");
    }
    if (nodeKindListEntries.length > 1) {
        const nodeKindListString = nodeKindListEntries.length === 2
            ? `${nodeKindListEntries[0] ?? oob()} and ${nodeKindListEntries[1] ?? oob()}`
            : `${nodeKindListEntries.slice(0, -1).join(", ")}, and ${nodeKindListEntries[nodeKindListEntries.length - 1]}`;
        ambiguityErrors.push(`A combination of ${nodeKindListString} is allowed within union (${formatTypes([...objects, ...maps, ...records])}). These 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 === FieldKind.Required) {
                const withKey = allObjectKeys.get(key) ?? fail(0xb35 /* 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/unhydratedFlexTreeFromInsertable.ts.
            ambiguityErrors.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.`);
        }
    }
}
//# sourceMappingURL=configuration.js.map