/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, oob, fail, unreachableCase } from "@fluidframework/core-utils/internal";
import { EmptyKey, rootFieldKey } from "../../core/index.js";
import { isTreeValue, FieldKinds } from "../../feature-libraries/index.js";
import { extractFromOpaque } from "../../util/index.js";
import { FieldSchema, normalizeAllowedTypes, } 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";
import { getKernel, isTreeNode, NodeKind, tryGetTreeNodeSchema, getOrCreateNodeFromInnerNode, typeSchemaSymbol, getOrCreateInnerNode, } from "../core/index.js";
import { isObjectNodeSchema } from "../node-kinds/index.js";
import { tryGetTreeNodeForField } from "../getTreeNodeForField.js";
/**
 * {@inheritDoc TreeNodeApi}
 */
export const treeNodeApi = {
    parent(node) {
        const editNode = getOrCreateInnerNode(node).parentField.parent.parent;
        if (editNode === undefined) {
            return undefined;
        }
        const output = getOrCreateNodeFromInnerNode(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 "property key", we need to look up the field associated with
        // the stored key from the flex-domain, and get property key its simple-domain counterpart was created with.
        const storedKey = getStoredKey(node);
        const parentSchema = treeNodeApi.schema(parent);
        const propertyKey = getPropertyKeyFromStoredKey(parentSchema, storedKey);
        return propertyKey;
    },
    on(node, eventName, listener) {
        const kernel = getKernel(node);
        switch (eventName) {
            case "nodeChanged": {
                const nodeSchema = kernel.schema;
                if (isObjectNodeSchema(nodeSchema)) {
                    return kernel.events.on("childrenChangedAfterBatch", ({ changedFields }) => {
                        const changedProperties = new Set(Array.from(changedFields, (field) => nodeSchema.storedKeyToPropertyKey.get(field) ??
                            fail(0xb36 /* Could not find stored key in schema. */)));
                        listener({ changedProperties });
                    });
                }
                else if (nodeSchema.kind === NodeKind.Array) {
                    return kernel.events.on("childrenChangedAfterBatch", () => {
                        listener({ changedProperties: undefined });
                    });
                }
                else {
                    return kernel.events.on("childrenChangedAfterBatch", ({ changedFields }) => {
                        listener({ changedProperties: changedFields });
                    });
                }
            }
            case "treeChanged": {
                return kernel.events.on("subtreeChangedAfterBatch", () => listener({}));
            }
            default:
                throw new UsageError(`No event named ${JSON.stringify(eventName)}.`);
        }
    },
    status(node) {
        return getKernel(node).getStatus();
    },
    is(value, schema) {
        // This "is" utility would return false if the provided schema is a base type of the actual schema.
        // This could be confusing, and that case can only be hit when violating the rule that there is a single most derived schema that gets used (See documentation on TreeNodeSchemaClass).
        // Therefore this uses markSchemaMostDerived to ensure an informative usage error is thrown in the case where a base type is used.
        const actualSchema = tryGetSchema(value);
        if (actualSchema === undefined) {
            return false;
        }
        return normalizeAllowedTypes(schema).has(actualSchema);
    },
    schema(node) {
        return tryGetSchema(node) ?? fail(0xb37 /* Not a tree node */);
    },
    shortId(node) {
        return getIdentifierFromNode(node, "preferCompressed");
    },
};
/**
 * 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)) {
                // TODO: This case could be optimized, for example by placing the simple schema in a symbol on tree nodes.
                return tryGetTreeNodeSchema(value);
            }
            if (value === null) {
                return nullSchema;
            }
            if (isFluidHandle(value)) {
                return handleSchema;
            }
        }
        default:
            return undefined;
    }
}
export function getIdentifierFromNode(node, compression) {
    const schema = node[typeSchemaSymbol];
    if (!isObjectNodeSchema(schema)) {
        return undefined;
    }
    const flexNode = getOrCreateInnerNode(node);
    const identifierFieldKeys = schema.identifierFieldKeys;
    switch (identifierFieldKeys.length) {
        case 0:
            return undefined;
        case 1: {
            const key = identifierFieldKeys[0] ?? oob();
            const identifierField = flexNode.tryGetField(key);
            assert(identifierField !== undefined, 0xbb5 /* missing identifier field */);
            const identifierValue = tryGetTreeNodeForField(identifierField);
            assert(typeof identifierValue === "string", 0xbb6 /* identifier not a string */);
            const context = flexNode.context;
            switch (compression) {
                case "preferCompressed": {
                    if (context.isHydrated()) {
                        const localNodeKey = context.nodeKeyManager.tryLocalizeNodeIdentifier(identifierValue);
                        return localNodeKey !== undefined
                            ? extractFromOpaque(localNodeKey)
                            : identifierValue;
                    }
                    else {
                        return identifierValue;
                    }
                }
                case "compressed": {
                    if (context.isHydrated()) {
                        const localNodeKey = context.nodeKeyManager.tryLocalizeNodeIdentifier(identifierValue);
                        return localNodeKey !== undefined ? extractFromOpaque(localNodeKey) : undefined;
                    }
                    else {
                        return undefined;
                    }
                }
                case "uncompressed": {
                    return identifierValue;
                }
                default:
                    unreachableCase(compression);
            }
        }
        default:
            throw new UsageError("shortId() may not be called on a node with more than one identifier. Consider converting extraneous identifier fields to string fields.");
    }
}
/**
 * Gets the stored key with which the provided node is associated in the parent.
 */
export function getStoredKey(node) {
    // Note: the flex domain strictly works with "stored keys", and knows nothing about the developer-facing
    // "property keys".
    const parentField = getOrCreateInnerNode(node).parentField;
    if (parentField.parent.schema === FieldKinds.sequence.identifier) {
        // The parent of `node` is an array node
        assert(parentField.parent.key === EmptyKey, 0xa28 /* When using index as key, field should use EmptyKey */);
        return parentField.index;
    }
    // The parent of `node` is an object, a map, or undefined. If undefined, then `node` is a root/detached node.
    assert(parentField.index === 0, 0xa29 /* When using field key as key, index should be 0 */);
    return parentField.parent.key;
}
/**
 * Given a node schema, gets the property key corresponding with the provided {@link FieldProps.key | stored key}.
 */
export function getPropertyKeyFromStoredKey(schema, storedKey) {
    // Only object nodes have the concept of a "stored key", differentiated from the developer-facing "property key".
    // For any other kind of node, the stored key and the property key are the same.
    if (schema.kind !== NodeKind.Object) {
        return storedKey;
    }
    const fields = schema.info;
    // Invariants:
    // - The set of all property keys under an object must be unique.
    // - The set of all stored keys (including those implicitly created from property keys) must be unique.
    // To find the property 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 property key.
    for (const [propertyKey, fieldSchema] of Object.entries(fields)) {
        if (fieldSchema instanceof FieldSchema && fieldSchema.props?.key === storedKey) {
            return propertyKey;
        }
    }
    if (fields[storedKey] === undefined) {
        fail(0xb38 /* Existing stored key should always map to a property key */);
    }
    return storedKey;
}
//# sourceMappingURL=treeNodeApi.js.map