/*!
 * 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 { isFluidHandle } from "@fluidframework/runtime-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { EmptyKey, rootFieldKey } from "../../core/index.js";
import { isTreeValue, FieldKinds } from "../../feature-libraries/index.js";
import { extractFromOpaque } from "../../util/index.js";
import { getKernel, isTreeNode, NodeKind, tryGetTreeNodeSchema, getOrCreateNodeFromInnerNode, typeSchemaSymbol, getInnerNode, normalizeAllowedTypes, } from "../core/index.js";
import { FieldSchema } from "../fieldSchema.js";
import { tryGetTreeNodeForField } from "../getTreeNodeForField.js";
import { booleanSchema, handleSchema, nullSchema, numberSchema, stringSchema, } from "../leafNodeSchema.js";
import { isArrayNodeSchema, isObjectNodeSchema } from "../node-kinds/index.js";
/**
 * {@inheritDoc TreeNodeApi}
 */
export const treeNodeApi = {
    parent(node) {
        const editNode = getInnerNode(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 (isArrayNodeSchema(nodeSchema)) {
                    return kernel.events.on("childrenChangedAfterBatch", ({ fieldMarks }) => {
                        const marks = fieldMarks.get(EmptyKey);
                        // nodeChanged fires only for shallow changes (insert, remove, move).
                        // Deep changes (e.g. a property of an element changed) are
                        // surfaced via TreeChangeEventsAlpha.treeChanged with a delta payload instead.
                        // When marks are undefined (marks could not be composed across multiple
                        // internal passes), we conservatively fire nodeChanged rather than silently
                        // dropping the event, even though the underlying change may have been
                        // purely deep. This is a known limitation of the current eventing stack.
                        const hasShallowChange = marks === undefined ||
                            marks.some((m) => m.attach !== undefined || m.detach !== undefined);
                        if (!hasShallowChange) {
                            return;
                        }
                        // `marks` is undefined when the field was modified across multiple batches
                        // within a single flush (e.g. due to an interleaved schema change) and the
                        // marks could not be composed. Emit `undefined` so callers know the delta is
                        // unavailable rather than receiving stale marks from only the first batch.
                        // TODO: Once the eventing stack is rewritten to walk the composed delta at
                        // flush time, `marks` will always be defined. Remove the `undefined` fallback.
                        const delta = marks === undefined ? undefined : deltaMarksToArrayOps(marks);
                        listener({ delta });
                    });
                }
                else {
                    return kernel.events.on("childrenChangedAfterBatch", ({ changedFields }) => {
                        listener({ changedProperties: changedFields });
                    });
                }
            }
            case "treeChanged": {
                if (isArrayNodeSchema(kernel.schema)) {
                    // For array nodes, treeChanged fires via childrenChangedAfterBatch so that a
                    // delta payload can be provided. This covers both shallow changes
                    // (insert/remove/move) and deep element changes. Stable (non-alpha) listeners
                    // typed as () => void will silently ignore the extra argument at runtime.
                    return kernel.events.on("childrenChangedAfterBatch", ({ fieldMarks }) => {
                        const marks = fieldMarks.get(EmptyKey);
                        const delta = marks === undefined ? undefined : deltaMarksToArrayOpsForTreeChanged(marks);
                        listener({ delta });
                    });
                }
                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).evaluateSet().has(actualSchema);
    },
    schema(node) {
        return tryGetSchema(node) ?? fail(0xb37 /* Not a tree node */);
    },
    shortId(node) {
        return getIdentifierFromNode(node, "preferCompressed");
    },
};
/**
 * Converts an array of internal {@link DeltaMark}s for a sequence field into sequential
 * array delta ops suitable for inclusion in {@link NodeChangedDataDelta.delta}.
 *
 * Each mark in the delta describes a contiguous run of positions in the original array:
 * - A mark with only `count` (no attach/detach) → `"retain"` with no subtree information
 * - A mark with only `attach` → `"insert"` (new elements added)
 * - A mark with only `detach` → `"remove"` (elements removed)
 * - A mark with both `attach` and `detach` → `"remove"` + `"insert"`
 *
 * @param marks - The low-level delta marks for the array's sequence field.
 *
 * @privateRemarks
 * The case where both `attach` and `detach` are set is unreachable today: the sequence-field
 * encoder never emits such marks for array (EmptyKey) fields. It is handled defensively.
 */
export function deltaMarksToArrayOps(marks) {
    const ops = [];
    for (const mark of marks) {
        if (mark.detach !== undefined) {
            ops.push({ type: "remove", count: mark.count });
        }
        if (mark.attach !== undefined) {
            ops.push({ type: "insert", count: mark.count });
        }
        else if (mark.detach === undefined) {
            // Retain: elements were not added or removed.
            ops.push({ type: "retain", count: mark.count });
        }
    }
    return ops;
}
/**
 * Converts an array of internal {@link DeltaMark}s for a sequence field into sequential
 * {@link ArrayNodeTreeChangedDeltaOp}s suitable for inclusion in
 * {@link NodeChangedDataTreeDelta.delta} (delivered to {@link TreeChangeEventsAlpha.treeChanged}).
 *
 * Same conversion rules as {@link deltaMarksToArrayOps}, but retain ops additionally carry a
 * {@link ArrayNodeTreeChangedRetainOp.subtreeChanged} flag derived from whether the mark has
 * a `fields` property (indicating a descendant changed).
 *
 * @param marks - The low-level delta marks for the array's sequence field.
 *
 * @privateRemarks
 * The case where both `attach` and `detach` are set is unreachable today: the sequence-field
 * encoder never emits such marks for array (EmptyKey) fields. It is handled defensively.
 */
export function deltaMarksToArrayOpsForTreeChanged(marks) {
    const ops = [];
    for (const mark of marks) {
        if (mark.detach !== undefined) {
            ops.push({ type: "remove", count: mark.count });
        }
        if (mark.attach !== undefined) {
            ops.push({ type: "insert", count: mark.count });
        }
        else if (mark.detach === undefined) {
            // Retain: elements were not added or removed (but may have deep changes).
            // When `fields` is set, `count` is guaranteed to be 1 (DeltaMark invariant).
            ops.push({
                type: "retain",
                count: mark.count,
                subtreeChanged: mark.fields !== undefined,
            });
        }
    }
    return ops;
}
/**
 * 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 = getInnerNode(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
                            ? identifierValue
                            : extractFromOpaque(localNodeKey);
                    }
                    else {
                        return identifierValue;
                    }
                }
                case "compressed": {
                    if (context.isHydrated()) {
                        const localNodeKey = context.nodeKeyManager.tryLocalizeNodeIdentifier(identifierValue);
                        return localNodeKey === undefined ? undefined : extractFromOpaque(localNodeKey);
                    }
                    else {
                        return undefined;
                    }
                }
                case "uncompressed": {
                    return identifierValue;
                }
                default: {
                    unreachableCase(compression);
                }
            }
        }
        default: {
            throw new UsageError("The node has more than one identifier. Retrieve identifiers individually via their fields instead.");
        }
    }
}
/**
 * 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 = getInnerNode(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