/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { isFluidHandle } from "@fluidframework/runtime-utils/internal";
import { assert, fail } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { aboveRootPlaceholder, EmptyKey, keyAsDetachedField, } from "../../core/index.js";
import { brand } from "../../util/index.js";
import { NodeKind } from "../core/index.js";
import { isTreeValue, stackTreeFieldCursor, stackTreeNodeCursor, } from "../../feature-libraries/index.js";
import { booleanSchema, handleSchema, nullSchema, numberSchema, stringSchema, } from "../leafNodeSchema.js";
import { isObjectNodeSchema } from "../node-kinds/index.js";
import { customFromCursor, replaceHandles, unknownTypeError, } from "./customTree.js";
import { getUnhydratedContext } from "../createContext.js";
/**
 * Use info from `schema` to convert `options` to {@link SchemalessParseOptions}.
 */
export function applySchemaToParserOptions(schema, options) {
    const config = {
        useStoredKeys: false,
        ...options,
    };
    const context = getUnhydratedContext(schema);
    return {
        keyConverter: config.useStoredKeys
            ? undefined
            : {
                encode: (type, key) => {
                    // translate stored key into property key.
                    const simpleNodeSchema = context.schema.get(brand(type)) ?? fail(0xb39 /* missing schema */);
                    if (isObjectNodeSchema(simpleNodeSchema)) {
                        const propertyKey = simpleNodeSchema.storedKeyToPropertyKey.get(key);
                        if (propertyKey !== undefined) {
                            return propertyKey;
                        }
                        // Looking up an out of schema key.
                        // This must point to a non-existent field.
                        // It's possible that the key, if we returned it unmodified, could point to some data
                        // (for example if looking up a key which is a stored key already when using property keys).
                        // Thus return an arbitrary key that was selected randomly, so should not exist on non-adversarial data:
                        const arbitrary = "arbitrary unused key: fe71614a-bf3e-43b3-b7b0-4cef39538e90";
                        assert(!simpleNodeSchema.storedKeyToPropertyKey.has(brand(arbitrary)), 0xa13 /* arbitrarily selected unused key was actually used */);
                        return arbitrary;
                    }
                    return key;
                },
                parse: (type, inputKey) => {
                    const simpleNodeSchema = context.schema.get(brand(type)) ?? unknownTypeError(type);
                    if (isObjectNodeSchema(simpleNodeSchema)) {
                        const info = simpleNodeSchema.flexKeyMap.get(inputKey);
                        if (info === undefined) {
                            throw new UsageError(`Failed to parse VerboseTree due to unexpected key ${JSON.stringify(inputKey)} on type ${JSON.stringify(type)}.`);
                        }
                        return info.storedKey;
                    }
                    return brand(inputKey);
                },
            },
    };
}
/**
 * Used to read a VerboseTree as a node cursor.
 *
 * @returns an {@link ITreeCursorSynchronous} for a single node in nodes mode.
 */
export function cursorFromVerbose(data, options) {
    return stackTreeNodeCursor(verboseTreeAdapter(options), data);
}
/**
 * Used to read a VerboseTree[] as a field cursor.
 *
 * @returns an {@link ITreeCursorSynchronous} for a single field in fields mode.
 */
export function fieldCursorFromVerbose(data, options) {
    return stackTreeFieldCursor(verboseTreeAdapter(options), { type: aboveRootPlaceholder, fields: data }, keyAsDetachedField(EmptyKey));
}
function verboseTreeAdapter(options) {
    return {
        value: (node) => {
            return isTreeValue(node) ? node : undefined;
        },
        type: (node) => {
            switch (typeof node) {
                case "number":
                    return brand(numberSchema.identifier);
                case "string":
                    return brand(stringSchema.identifier);
                case "boolean":
                    return brand(booleanSchema.identifier);
                default:
                    if (node === null) {
                        return brand(nullSchema.identifier);
                    }
                    if (isFluidHandle(node)) {
                        return brand(handleSchema.identifier);
                    }
                    return brand(node.type);
            }
        },
        keysFromNode: (node) => {
            switch (typeof node) {
                case "object": {
                    if (node === null) {
                        return [];
                    }
                    if (isFluidHandle(node)) {
                        return [];
                    }
                    if (Array.isArray(node.fields)) {
                        return node.fields.length === 0 ? [] : [EmptyKey];
                    }
                    const inputKeys = Object.keys(node.fields);
                    const converter = options.keyConverter;
                    if (converter === undefined) {
                        return inputKeys;
                    }
                    return inputKeys.map((k) => converter.parse(node.type, k));
                }
                default:
                    return [];
            }
        },
        getFieldFromNode: (node, key) => {
            // Object.prototype.hasOwnProperty can return true for strings (ex: with key "0"), so we have to filter them out.
            // Rather than just special casing strings, we can handle them with an early return for all primitives.
            if (typeof node !== "object") {
                return [];
            }
            if (node === null) {
                return [];
            }
            if (isFluidHandle(node)) {
                return [];
            }
            if (Array.isArray(node.fields)) {
                return key === EmptyKey ? node.fields : [];
            }
            const convertedKey = options.keyConverter === undefined ? key : options.keyConverter.encode(node.type, key);
            if (Object.prototype.hasOwnProperty.call(node.fields, convertedKey)) {
                const field = node.fields[convertedKey];
                return field === undefined ? [] : [field];
            }
            return [];
        },
    };
}
/**
 * Used to read a node cursor as a VerboseTree.
 */
export function verboseFromCursor(reader, rootSchema, options) {
    const config = {
        useStoredKeys: false,
        ...options,
    };
    const schemaMap = getUnhydratedContext(rootSchema).schema;
    return verboseFromCursorInner(reader, config, schemaMap);
}
function verboseFromCursorInner(reader, options, schema) {
    const fields = customFromCursor(reader, options, schema, verboseFromCursorInner);
    const nodeSchema = schema.get(reader.type) ?? fail(0xb3c /* missing schema for type in cursor */);
    if (nodeSchema.kind === NodeKind.Leaf) {
        return fields;
    }
    return {
        type: reader.type,
        fields: fields,
    };
}
/**
 * Clones tree, replacing any handles.
 * @remarks
 * A strongly types version of {@link replaceHandles}.
 * @alpha
 */
export function replaceVerboseTreeHandles(tree, replacer) {
    return replaceHandles(tree, replacer);
}
//# sourceMappingURL=verboseTree.js.map