/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { isFluidHandle } from "@fluidframework/runtime-utils/internal";
import { assert } from "@fluidframework/core-utils/internal";
import { aboveRootPlaceholder, EmptyKey, forEachField, inCursorField, keyAsDetachedField, mapCursorField, } from "../../core/index.js";
import { brand, fail } from "../../util/index.js";
import { getSimpleNodeSchema, NodeKind } from "../core/index.js";
import { isTreeValue, stackTreeFieldCursor, stackTreeNodeCursor, } from "../../feature-libraries/index.js";
import { booleanSchema, handleSchema, nullSchema, numberSchema, stringSchema, } from "../leafNodeSchema.js";
import { toFlexSchema } from "../toFlexSchema.js";
import { isObjectNodeSchema } from "../objectNodeTypes.js";
import { walkFieldSchema } from "../walkSchema.js";
/**
 * Use info from `schema` to convert `options` to {@link SchemalessParseOptions}.
 */
export function applySchemaToParserOptions(schema, options) {
    const config = {
        useStoredKeys: false,
        ...options,
    };
    // TODO: should provide a way to look up schema by name efficiently without converting to flex tree schema and back.
    // Maybe cache identifier->schema map on simple tree schema lazily.
    const flexSchema = toFlexSchema(schema);
    return {
        valueConverter: config.valueConverter,
        keyConverter: config.useStoredKeys
            ? undefined
            : {
                encode: (type, key) => {
                    // translate stored key into property key.
                    const flexNodeSchema = flexSchema.nodeSchema.get(brand(type)) ?? fail("missing schema");
                    const simpleNodeSchema = getSimpleNodeSchema(flexNodeSchema);
                    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 flexNodeSchema = flexSchema.nodeSchema.get(brand(type)) ?? fail("missing schema");
                    const simpleNodeSchema = getSimpleNodeSchema(flexNodeSchema);
                    if (isObjectNodeSchema(simpleNodeSchema)) {
                        const info = simpleNodeSchema.flexKeyMap.get(inputKey) ?? fail("missing field info");
                        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: (input) => {
            const node = options.valueConverter(input);
            return isTreeValue(node) ? node : undefined;
        },
        type: (input) => {
            const node = options.valueConverter(input);
            switch (typeof node) {
                case "number":
                    return numberSchema.identifier;
                case "string":
                    return stringSchema.identifier;
                case "boolean":
                    return booleanSchema.identifier;
                default:
                    if (node === null) {
                        return nullSchema.identifier;
                    }
                    if (isFluidHandle(node)) {
                        return handleSchema.identifier;
                    }
                    return node.type;
            }
        },
        keysFromNode: (input) => {
            const node = options.valueConverter(input);
            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: (input, key) => {
            const node = options.valueConverter(input);
            // 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 = new Map();
    walkFieldSchema(rootSchema, {
        node(schema) {
            schemaMap.set(schema.identifier, schema);
        },
    });
    return verboseFromCursorInner(reader, config, schemaMap);
}
function verboseFromCursorInner(reader, options, schema) {
    const type = reader.type;
    const nodeSchema = schema.get(type) ?? fail("missing schema for type in cursor");
    switch (type) {
        case numberSchema.identifier:
        case booleanSchema.identifier:
        case nullSchema.identifier:
        case stringSchema.identifier:
            assert(reader.value !== undefined, 0xa14 /* out of schema: missing value */);
            assert(!isFluidHandle(reader.value), 0xa15 /* out of schema: unexpected FluidHandle */);
            return reader.value;
        case handleSchema.identifier:
            assert(reader.value !== undefined, 0xa16 /* out of schema: missing value */);
            assert(isFluidHandle(reader.value), 0xa17 /* out of schema: unexpected FluidHandle */);
            return options.valueConverter(reader.value);
        default: {
            assert(reader.value === undefined, 0xa18 /* out of schema: unexpected value */);
            if (nodeSchema.kind === NodeKind.Array) {
                const fields = inCursorField(reader, EmptyKey, () => mapCursorField(reader, () => verboseFromCursorInner(reader, options, schema)));
                return { type, fields };
            }
            else {
                const fields = {};
                forEachField(reader, () => {
                    const children = mapCursorField(reader, () => verboseFromCursorInner(reader, options, schema));
                    if (children.length === 1) {
                        const storedKey = reader.getFieldKey();
                        const key = isObjectNodeSchema(nodeSchema) && !options.useStoredKeys
                            ? nodeSchema.storedKeyToPropertyKey.get(storedKey) ??
                                fail("missing property key")
                            : storedKey;
                        // Length is checked above.
                        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                        fields[key] = children[0];
                    }
                    else {
                        assert(children.length === 0, 0xa19 /* invalid children number */);
                    }
                });
                return { type, fields };
            }
        }
    }
}
//# sourceMappingURL=verboseTree.js.map