"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPropertyKeyFromStoredKey = exports.getStoredKey = exports.getIdentifierFromNode = exports.tryGetSchema = exports.treeNodeApi = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const index_js_1 = require("../../core/index.js");
const index_js_2 = require("../../feature-libraries/index.js");
const index_js_3 = require("../../util/index.js");
const schemaTypes_js_1 = require("../schemaTypes.js");
const leafNodeSchema_js_1 = require("../leafNodeSchema.js");
const internal_2 = require("@fluidframework/runtime-utils/internal");
const internal_3 = require("@fluidframework/telemetry-utils/internal");
const index_js_4 = require("../core/index.js");
const index_js_5 = require("../node-kinds/index.js");
const getTreeNodeForField_js_1 = require("../getTreeNodeForField.js");
/**
 * {@inheritDoc TreeNodeApi}
 */
exports.treeNodeApi = {
    parent(node) {
        const editNode = (0, index_js_4.getOrCreateInnerNode)(node).parentField.parent.parent;
        if (editNode === undefined) {
            return undefined;
        }
        const output = (0, index_js_4.getOrCreateNodeFromInnerNode)(editNode);
        (0, internal_1.assert)(!(0, index_js_2.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 = exports.treeNodeApi.parent(node);
        if (parent === undefined) {
            return index_js_1.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 = exports.treeNodeApi.schema(parent);
        const propertyKey = getPropertyKeyFromStoredKey(parentSchema, storedKey);
        return propertyKey;
    },
    on(node, eventName, listener) {
        const kernel = (0, index_js_4.getKernel)(node);
        switch (eventName) {
            case "nodeChanged": {
                const nodeSchema = kernel.schema;
                if ((0, index_js_5.isObjectNodeSchema)(nodeSchema)) {
                    return kernel.events.on("childrenChangedAfterBatch", ({ changedFields }) => {
                        const changedProperties = new Set(Array.from(changedFields, (field) => nodeSchema.storedKeyToPropertyKey.get(field) ??
                            (0, internal_1.fail)(0xb36 /* Could not find stored key in schema. */)));
                        listener({ changedProperties });
                    });
                }
                else if (nodeSchema.kind === index_js_4.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 internal_3.UsageError(`No event named ${JSON.stringify(eventName)}.`);
        }
    },
    status(node) {
        return (0, index_js_4.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 (0, schemaTypes_js_1.normalizeAllowedTypes)(schema).has(actualSchema);
    },
    schema(node) {
        return tryGetSchema(node) ?? (0, internal_1.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.
 */
function tryGetSchema(value) {
    switch (typeof value) {
        case "string":
            return leafNodeSchema_js_1.stringSchema;
        case "number":
            return leafNodeSchema_js_1.numberSchema;
        case "boolean":
            return leafNodeSchema_js_1.booleanSchema;
        case "object": {
            if ((0, index_js_4.isTreeNode)(value)) {
                // TODO: This case could be optimized, for example by placing the simple schema in a symbol on tree nodes.
                return (0, index_js_4.tryGetTreeNodeSchema)(value);
            }
            if (value === null) {
                return leafNodeSchema_js_1.nullSchema;
            }
            if ((0, internal_2.isFluidHandle)(value)) {
                return leafNodeSchema_js_1.handleSchema;
            }
        }
        default:
            return undefined;
    }
}
exports.tryGetSchema = tryGetSchema;
function getIdentifierFromNode(node, compression) {
    const schema = node[index_js_4.typeSchemaSymbol];
    if (!(0, index_js_5.isObjectNodeSchema)(schema)) {
        return undefined;
    }
    const flexNode = (0, index_js_4.getOrCreateInnerNode)(node);
    const identifierFieldKeys = schema.identifierFieldKeys;
    switch (identifierFieldKeys.length) {
        case 0:
            return undefined;
        case 1: {
            const key = identifierFieldKeys[0] ?? (0, internal_1.oob)();
            const identifierField = flexNode.tryGetField(key);
            (0, internal_1.assert)(identifierField !== undefined, 0xbb5 /* missing identifier field */);
            const identifierValue = (0, getTreeNodeForField_js_1.tryGetTreeNodeForField)(identifierField);
            (0, internal_1.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
                            ? (0, index_js_3.extractFromOpaque)(localNodeKey)
                            : identifierValue;
                    }
                    else {
                        return identifierValue;
                    }
                }
                case "compressed": {
                    if (context.isHydrated()) {
                        const localNodeKey = context.nodeKeyManager.tryLocalizeNodeIdentifier(identifierValue);
                        return localNodeKey !== undefined ? (0, index_js_3.extractFromOpaque)(localNodeKey) : undefined;
                    }
                    else {
                        return undefined;
                    }
                }
                case "uncompressed": {
                    return identifierValue;
                }
                default:
                    (0, internal_1.unreachableCase)(compression);
            }
        }
        default:
            throw new internal_3.UsageError("shortId() may not be called on a node with more than one identifier. Consider converting extraneous identifier fields to string fields.");
    }
}
exports.getIdentifierFromNode = getIdentifierFromNode;
/**
 * 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
    // "property keys".
    const parentField = (0, index_js_4.getOrCreateInnerNode)(node).parentField;
    if (parentField.parent.schema === index_js_2.FieldKinds.sequence.identifier) {
        // The parent of `node` is an array node
        (0, internal_1.assert)(parentField.parent.key === index_js_1.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.
    (0, internal_1.assert)(parentField.index === 0, 0xa29 /* When using field key as key, index should be 0 */);
    return parentField.parent.key;
}
exports.getStoredKey = getStoredKey;
/**
 * Given a node schema, gets the property key corresponding with the provided {@link FieldProps.key | stored key}.
 */
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 !== index_js_4.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 schemaTypes_js_1.FieldSchema && fieldSchema.props?.key === storedKey) {
            return propertyKey;
        }
    }
    if (fields[storedKey] === undefined) {
        (0, internal_1.fail)(0xb38 /* Existing stored key should always map to a property key */);
    }
    return storedKey;
}
exports.getPropertyKeyFromStoredKey = getPropertyKeyFromStoredKey;
//# sourceMappingURL=treeNodeApi.js.map