/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
import { Multiplicity, rootFieldKey } from "../core/index.js";
import { FieldKinds, isLazy, isTreeValue, } from "../feature-libraries/index.js";
import { fail, extractFromOpaque, isReadonlyArray } from "../util/index.js";
import { getOrCreateNodeProxy, isTreeNode } from "./proxies.js";
import { getFlexNode } from "./proxyBinding.js";
import { tryGetSimpleNodeSchema } from "./schemaCaching.js";
import { NodeKind, FieldSchema, } from "./schemaTypes.js";
import { booleanSchema, handleSchema, nullSchema, numberSchema, stringSchema, } from "./leafNodeSchema.js";
import { isFluidHandle } from "@fluidframework/runtime-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
/**
 * The `Tree` object holds various functions for analyzing {@link TreeNode}s.
 */
export const treeNodeApi = {
    parent: (node) => {
        const editNode = getFlexNode(node).parentField.parent.parent;
        if (editNode === undefined) {
            return undefined;
        }
        const output = getOrCreateNodeProxy(editNode);
        assert(!isTreeValue(output), 0x87f /* Parent can't be a leaf, so it should be a node not a value */);
        return output;
    },
    key: (node) => {
        // If the parent is undefined, then this node is under the root field,
        // so we know its key is the special root one.
        const parent = treeNodeApi.parent(node);
        if (parent === undefined) {
            return rootFieldKey;
        }
        // The flex-domain strictly operates in terms of "stored keys".
        // To find the associated developer-facing "view key", we need to look up the field associated with
        // the stored key from the flex-domain, and get view key its simple-domain counterpart was created with.
        const storedKey = getStoredKey(node);
        const parentSchema = treeNodeApi.schema(parent);
        const viewKey = getViewKeyFromStoredKey(parentSchema, storedKey);
        return viewKey;
    },
    on: (node, eventName, listener) => {
        const flex = getFlexNode(node);
        switch (eventName) {
            case "nodeChanged":
                return flex.on("nodeChanged", listener);
            case "treeChanged":
                return flex.on("treeChanged", listener);
            default:
                return unreachableCase(eventName);
        }
    },
    status: (node) => {
        return getFlexNode(node, true).treeStatus();
    },
    is: (value, schema) => {
        const actualSchema = tryGetSchema(value);
        if (actualSchema === undefined) {
            return false;
        }
        if (isReadonlyArray(schema)) {
            for (const singleSchema of schema) {
                const testSchema = isLazy(singleSchema) ? singleSchema() : singleSchema;
                if (testSchema === actualSchema) {
                    return true;
                }
            }
            return false;
        }
        else {
            return schema === actualSchema;
        }
    },
    schema(node) {
        return tryGetSchema(node) ?? fail("Not a tree node");
    },
    shortId(node) {
        const flexNode = getFlexNode(node);
        let shortId;
        for (const field of flexNode.boxedIterator()) {
            if (field.schema.kind === FieldKinds.identifier) {
                if (shortId !== undefined) {
                    throw new UsageError("shortId() may not be called on a node with more than one identifier. Consider converting extraneous identifier fields to string fields.");
                }
                const identifier = field.boxedAt(0);
                assert(identifier !== undefined, 0x927 /* The identifier must exist */);
                const identifierValue = identifier.value;
                const localNodeKey = identifier.context.nodeKeyManager.tryLocalizeNodeKey(identifierValue);
                shortId =
                    localNodeKey !== undefined ? extractFromOpaque(localNodeKey) : identifierValue;
            }
        }
        return shortId;
    },
};
/**
 * Returns a schema for a value if the value is a {@link TreeNode} or a {@link TreeLeafValue}.
 * Returns undefined for other values.
 */
export function tryGetSchema(value) {
    switch (typeof value) {
        case "string":
            return stringSchema;
        case "number":
            return numberSchema;
        case "boolean":
            return booleanSchema;
        case "object": {
            if (isTreeNode(value)) {
                // This case could be optimized, for example by placing the simple schema in a symbol on tree nodes.
                return tryGetSimpleNodeSchema(getFlexNode(value).schema);
            }
            if (value === null) {
                return nullSchema;
            }
            if (isFluidHandle(value)) {
                return handleSchema;
            }
        }
        default:
            return undefined;
    }
}
/**
 * Gets the stored key with which the provided node is associated in the parent.
 */
function getStoredKey(node) {
    // Note: the flex domain strictly works with "stored keys", and knows nothing about the developer-facing
    // "view keys".
    const parentField = getFlexNode(node).parentField;
    if (parentField.parent.schema.kind.multiplicity === Multiplicity.Sequence) {
        // The parent of `node` is an array node
        return parentField.index;
    }
    // The parent of `node` is an object, a map, or undefined (and therefore `node` is a root/detached node).
    return parentField.parent.key;
}
/**
 * Given a node schema, gets the view key corresponding with the provided {@link FieldProps.key | stored key}.
 */
function getViewKeyFromStoredKey(schema, storedKey) {
    // Only object nodes have the concept of a "stored key", differentiated from the developer-facing "view key".
    // For any other kind of node, the stored key and the view key are the same.
    if (schema.kind !== NodeKind.Object) {
        return storedKey;
    }
    const fields = schema.info;
    // Invariants:
    // - The set of all view keys under an object must be unique.
    // - The set of all stored keys (including those implicitly created from view keys) must be unique.
    // To find the view key associated with the provided stored key, first check for any stored key matches (which are optionally populated).
    // If we don't find any, then search for a matching view key.
    for (const [viewKey, fieldSchema] of Object.entries(fields)) {
        if (fieldSchema instanceof FieldSchema && fieldSchema.props?.key === storedKey) {
            return viewKey;
        }
    }
    if (fields[storedKey] === undefined) {
        fail("Existing stored key should always map to a view key");
    }
    return storedKey;
}
//# sourceMappingURL=treeNodeApi.js.map