/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, debugAssert, fail, unreachableCase, } from "@fluidframework/core-utils/internal";
import { createIdCompressor } from "@fluidframework/id-compressor/internal";
import { isFluidHandle } from "@fluidframework/runtime-utils";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { FluidClientVersion, FormatValidatorNoOp, } from "../codec/index.js";
import { EmptyKey } from "../core/index.js";
import { cursorForMapTreeField, defaultSchemaPolicy, isTreeValue, fieldBatchCodecBuilder, mapTreeFromCursor, TreeCompressionStrategy, withObservation, } from "../feature-libraries/index.js";
import { asIndex, getKernel, TreeNode, TreeBeta, tryGetSchema, createFromCursor, FieldKind, normalizeFieldSchema, applySchemaToParserOptions, cursorFromVerbose, verboseFromCursor, extractPersistedSchema, TreeViewConfigurationAlpha, getStoredKey, getPropertyKeyFromStoredKey, treeNodeApi, getIdentifierFromNode, unhydratedFlexTreeFromInsertable, getOrCreateNodeFromInnerNode, getOrCreateNodeFromInnerUnboxedNode, getInnerNode, NodeKind, tryGetTreeNodeForField, isObjectNodeSchema, isTreeNode, toInitialSchema, importConcise, exportConcise, borrowCursorFromTreeNodeOrValue, contentSchemaSymbol, getUnhydratedContext, } from "../simple-tree/index.js";
import { brand, extractFromOpaque } from "../util/index.js";
import { independentInitializedView } from "./independentView.js";
import { SchematizingSimpleTreeView, ViewSlot } from "./schematizingTreeView.js";
import { UnhydratedTreeContext } from "./unhydratedTreeContext.js";
const identifier = (node) => {
    return getIdentifierFromNode(node, "uncompressed");
};
identifier.shorten = (branch, nodeIdentifier) => {
    assert(branch instanceof SchematizingSimpleTreeView, 0xcac /* Unexpected branch implementation */);
    const { nodeKeyManager } = branch;
    const localNodeKey = nodeKeyManager.tryLocalizeNodeIdentifier(nodeIdentifier);
    return localNodeKey === undefined ? undefined : extractFromOpaque(localNodeKey);
};
identifier.lengthen = (branch, nodeIdentifier) => {
    assert(branch instanceof SchematizingSimpleTreeView, 0xcad /* Unexpected branch implementation */);
    const { nodeKeyManager } = branch;
    const local = brand(nodeIdentifier);
    return nodeKeyManager.stabilizeNodeIdentifier(local);
};
identifier.getShort = (node) => {
    const shortIdentifier = getIdentifierFromNode(node, "compressed");
    return typeof shortIdentifier === "number" ? shortIdentifier : undefined;
};
identifier.create = (branch) => {
    const nodeKeyManager = branch
        .nodeKeyManager;
    return nodeKeyManager.stabilizeNodeIdentifier(nodeKeyManager.generateLocalNodeIdentifier());
};
Object.freeze(identifier);
/**
 * Subscription to changes on a single node.
 * @remarks
 * Either tracks some set of fields, or all fields and can be updated to track more fields.
 */
class NodeSubscription {
    constructor(onInvalidation, flexNode) {
        this.onInvalidation = onInvalidation;
        // TODO:Performance: It is possible to optimize this to not use the public TreeNode API.
        const node = getOrCreateNodeFromInnerNode(flexNode);
        assert(node instanceof TreeNode, 0xc54 /* Unexpected leaf value */);
        const handler = (data) => {
            if (this.keys === "deep") {
                return;
            }
            if (this.keys === undefined || data.changedProperties === undefined) {
                this.onInvalidation();
            }
            else {
                let keyMap;
                const schema = treeNodeApi.schema(node);
                if (isObjectNodeSchema(schema)) {
                    keyMap = schema.storedKeyToPropertyKey;
                }
                // TODO:Performance: Ideally this would use Set.prototype.isDisjointFrom when available.
                for (const flexKey of this.keys) {
                    // TODO:Performance: doing everything at the flex tree layer could avoid this translation
                    const key = keyMap?.get(flexKey) ?? flexKey;
                    if (data.changedProperties.has(key)) {
                        this.onInvalidation();
                        return;
                    }
                }
            }
        };
        // TODO:Performance: It would be better to defer subscribing to events so that this can subscribe to the correct one instead of both.
        const shallow = TreeBeta.on(node, "nodeChanged", handler);
        const deep = TreeBeta.on(node, "treeChanged", () => {
            if (this.keys === "deep") {
                this.onInvalidation();
            }
        });
        this.unsubscribe = () => {
            shallow();
            deep();
        };
    }
    /**
     * Create an {@link Observer} which subscribes to what was observed in {@link NodeSubscription}s.
     */
    static createObserver(invalidate, onlyOnce = false) {
        const subscriptions = new Map();
        const observer = {
            observeNodeDeep(flexNode) {
                if (flexNode.value !== undefined) {
                    // Leaf value: the set of fields (and thus their content) is always empty, and cannot change, so no need to subscribe.
                    return;
                }
                const subscription = subscriptions.get(flexNode);
                if (subscription === undefined) {
                    const newSubscription = new NodeSubscription(invalidate, flexNode);
                    newSubscription.keys = "deep";
                    subscriptions.set(flexNode, newSubscription);
                }
                else {
                    // Already subscribed to this node.
                    subscription.keys = "deep"; // Now subscribed to subtree changes (deep observation).
                }
            },
            observeNodeFields(flexNode) {
                if (flexNode.value !== undefined) {
                    // Leaf value: the set of fields is always empty, and cannot change, so no need to subscribe.
                    return;
                }
                // Subscribe to any change to any field to ensure that any change which empties or fills a field will invalidate.
                // This could be more targeted by detecting specifically edits which change the emptiness of fields if desired.
                const subscription = subscriptions.get(flexNode);
                if (subscription === undefined) {
                    const newSubscription = new NodeSubscription(invalidate, flexNode);
                    subscriptions.set(flexNode, newSubscription);
                }
                else {
                    // Already subscribed to this node.
                    if (subscription.keys instanceof Set) {
                        subscription.keys = undefined; // Now subscribed to all keys.
                    }
                }
            },
            observeNodeField(flexNode, key) {
                if (flexNode.value !== undefined) {
                    // Leaf value, nothing to observe.
                    return;
                }
                const subscription = subscriptions.get(flexNode);
                if (subscription === undefined) {
                    const newSubscription = new NodeSubscription(invalidate, flexNode);
                    newSubscription.keys = new Set([key]);
                    subscriptions.set(flexNode, newSubscription);
                }
                else {
                    // Already subscribed to this node: if not subscribed to all keys or "deep", subscribe to this one.
                    if (subscription.keys instanceof Set) {
                        // TODO:Performance: due to how JavaScript set ordering works,
                        // it might be faster to check `has` and only add if not present in case the same field is viewed many times.
                        subscription.keys.add(key);
                    }
                }
            },
            observeParentOf(node) {
                // Supporting parent tracking is more difficult that it might seem at first.
                // There are two main complicating factors:
                // 1. The parent may be undefined (the node is a root).
                // 2. If tracking this by subscribing to the parent's changes, then which events are subscribed to needs to be updated after the parent changes.
                //
                // If not supporting the first case (undefined parents), the second case gets problematic: edits which un-parent a node could error due to being unable to update the event subscription.
                // For now this is mitigated by only supporting one of tracking (non-undefined) parents or maintaining event subscriptions across edits.
                if (!onlyOnce) {
                    // TODO: better APIS should be provided which make handling this case practical.
                    throw new UsageError("Observation tracking for parents is currently not supported.");
                }
                const parent = withObservation(undefined, () => node.parentField.parent);
                if (parent.parent === undefined) {
                    // TODO: better APIS should be provided which make handling this case practical.
                    throw new UsageError("Observation tracking for parents is currently not supported when parent is undefined.");
                }
                observer.observeNodeField(parent.parent, parent.key);
            },
        };
        let subscribed = true;
        return {
            observer,
            unsubscribe: () => {
                if (!subscribed) {
                    throw new UsageError("Already unsubscribed");
                }
                subscribed = false;
                for (const subscription of subscriptions.values()) {
                    subscription.unsubscribe();
                }
            },
        };
    }
}
/**
 * Handles both {@link (TreeAlpha:interface).trackObservations} and {@link (TreeAlpha:interface).trackObservationsOnce}.
 */
function trackObservations(onInvalidation, trackDuring, onlyOnce = false) {
    let observing = true;
    const invalidate = () => {
        if (observing) {
            throw new UsageError("Cannot invalidate while tracking observations");
        }
        onInvalidation();
    };
    const { observer, unsubscribe } = NodeSubscription.createObserver(invalidate, onlyOnce);
    const result = withObservation(observer, trackDuring);
    observing = false;
    return {
        result,
        unsubscribe,
    };
}
/**
 * Extensions to {@link (Tree:variable)} and {@link (TreeBeta:variable)} which are not yet stable.
 * @see {@link (TreeAlpha:interface)}.
 * @alpha
 */
export const TreeAlpha = {
    on(node, eventName, listener) {
        return treeNodeApi.on(node, eventName, listener);
    },
    trackObservations(onInvalidation, trackDuring) {
        return trackObservations(onInvalidation, trackDuring);
    },
    trackObservationsOnce(onInvalidation, trackDuring) {
        const result = trackObservations(() => {
            // trackObservations ensures no invalidation occurs while its running,
            // so this callback can only run after trackObservations has returns and thus result is defined.
            result.unsubscribe();
            onInvalidation();
        }, trackDuring, true);
        return result;
    },
    context(node) {
        return this.branch(node) ?? UnhydratedTreeContext.instance;
    },
    branch(node) {
        const kernel = getKernel(node);
        if (!kernel.isHydrated()) {
            return undefined;
        }
        const view = kernel.anchorNode.anchorSet.slots.get(ViewSlot);
        assert(view instanceof SchematizingSimpleTreeView, 0xa5c /* Unexpected view implementation */);
        return view;
    },
    create(schema, data) {
        const mapTree = unhydratedFlexTreeFromInsertable(data, schema);
        const result = mapTree === undefined ? undefined : getOrCreateNodeFromInnerNode(mapTree);
        return result;
    },
    importConcise(schema, data) {
        return importConcise(schema, data);
    },
    exportConcise,
    importVerbose(schema, data, options) {
        const config = { ...options };
        // Create a config which is standalone, and thus can be used without having to refer back to the schema.
        const schemalessConfig = applySchemaToParserOptions(schema, config);
        if (data === undefined) {
            const field = normalizeFieldSchema(schema);
            if (field.kind !== FieldKind.Optional) {
                throw new UsageError("undefined provided for non-optional field.");
            }
            return undefined;
        }
        const cursor = cursorFromVerbose(data, schemalessConfig);
        return createFromCursor(schema, cursor, getUnhydratedContext(schema).flexContext.schema.rootFieldSchema);
    },
    exportVerbose(node, options) {
        if (isTreeValue(node)) {
            return node;
        }
        const config = { ...options };
        const cursor = borrowCursorFromTreeNodeOrValue(node);
        const kernel = getKernel(node);
        return verboseFromCursor(cursor, kernel.context, config);
    },
    exportCompressed(node, options) {
        const schema = tryGetSchema(node) ?? fail(0xacf /* invalid input */);
        const codec = fieldBatchCodecBuilder.build({
            jsonValidator: FormatValidatorNoOp,
            minVersionForCollab: options.minVersionForCollab,
        });
        const cursor = borrowFieldCursorFromTreeNodeOrValue(node);
        const batch = [cursor];
        // If none provided, create a compressor which will not compress anything.
        const idCompressor = options.idCompressor ?? createIdCompressor();
        // Grabbing an existing stored schema from the node is important to ensure that unknown optional fields can be preserved.
        // Note that if the node is unhydrated, this can result in all staged allowed types being included in the schema, which might be undesired.
        const storedSchema = isTreeNode(node)
            ? getKernel(node).context.flexContext.schema
            : toInitialSchema(schema);
        const context = {
            encodeType: TreeCompressionStrategy.Compressed,
            idCompressor,
            originatorId: idCompressor.localSessionId, // TODO: Why is this needed?
            schema: { schema: storedSchema, policy: defaultSchemaPolicy },
            // Not a summary blob — this is `TreeAlpha`'s ad-hoc encoder.
            isSummary: false,
        };
        const result = codec.encode(batch, context);
        // TODO: codecs should better track which ones can contain handles, and which cannot. When done properly, casts like this can be removed.
        return result;
    },
    importCompressed(schema, compressedData, options) {
        const config = new TreeViewConfigurationAlpha({ schema });
        const content = {
            // Always use a v1 schema codec for consistency.
            // TODO: reevaluate how staged schema should behave in schema import/export APIs before stabilizing this.
            schema: extractPersistedSchema(config.schema, FluidClientVersion.v2_0, () => true),
            tree: compressedData,
            idCompressor: options.idCompressor ?? createIdCompressor(),
        };
        const view = independentInitializedView(config, options, content);
        return TreeBeta.clone(view.root);
    },
    identifier,
    key2(node) {
        // If the parent is undefined, then this node is under the root field,
        const parent = treeNodeApi.parent(node);
        if (parent === undefined) {
            return undefined;
        }
        // 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);
        return getPropertyKeyFromStoredKey(parentSchema, storedKey);
    },
    child: (node, propertyKey) => {
        const flexNode = getInnerNode(node);
        debugAssert(() => !flexNode.context.isDisposed() || "The provided tree node has been disposed.");
        const schema = treeNodeApi.schema(node);
        switch (schema.kind) {
            case NodeKind.Array: {
                const sequence = flexNode.tryGetField(EmptyKey);
                // Empty sequence - cannot have children.
                if (sequence === undefined) {
                    return undefined;
                }
                const index = typeof propertyKey === "number"
                    ? propertyKey
                    : asIndex(propertyKey, Number.POSITIVE_INFINITY);
                // If the key is not a valid index, then there is no corresponding child.
                if (index === undefined) {
                    return undefined;
                }
                const childFlexTree = sequence.at(index);
                // No child at the given index.
                if (childFlexTree === undefined) {
                    return undefined;
                }
                return getOrCreateNodeFromInnerUnboxedNode(childFlexTree);
            }
            case NodeKind.Map: {
                if (typeof propertyKey !== "string") {
                    // Map nodes only support string keys.
                    return undefined;
                }
            }
            // Fall through
            case NodeKind.Record:
            case NodeKind.Object: {
                let storedKey = propertyKey;
                if (isObjectNodeSchema(schema)) {
                    const fieldSchema = schema.fields.get(String(propertyKey));
                    if (fieldSchema === undefined) {
                        return undefined;
                    }
                    storedKey = fieldSchema.storedKey;
                }
                const field = flexNode.tryGetField(brand(String(storedKey)));
                if (field !== undefined) {
                    return tryGetTreeNodeForField(field);
                }
                return undefined;
            }
            case NodeKind.Leaf: {
                fail(0xbc3 /* Leaf schema associated with non-leaf tree node. */);
            }
            default: {
                unreachableCase(schema.kind);
            }
        }
    },
    children(node) {
        const flexNode = getInnerNode(node);
        debugAssert(() => !flexNode.context.isDisposed() || "The provided tree node has been disposed.");
        const schema = treeNodeApi.schema(node);
        const result = [];
        switch (schema.kind) {
            case NodeKind.Array: {
                const sequence = flexNode.tryGetField(EmptyKey);
                if (sequence === undefined) {
                    break;
                }
                for (let index = 0; index < sequence.length; index++) {
                    const childFlexTree = sequence.at(index);
                    assert(childFlexTree !== undefined, 0xbc4 /* Sequence child was undefined. */);
                    const childTree = getOrCreateNodeFromInnerUnboxedNode(childFlexTree);
                    result.push([index, childTree]);
                }
                break;
            }
            case NodeKind.Map:
            case NodeKind.Record: {
                for (const [key, flexField] of flexNode.fields) {
                    const childTreeNode = tryGetTreeNodeForField(flexField);
                    if (childTreeNode !== undefined) {
                        result.push([key, childTreeNode]);
                    }
                }
                break;
            }
            case NodeKind.Object: {
                assert(isObjectNodeSchema(schema), 0xbc5 /* Expected object schema. */);
                for (const [propertyKey, fieldSchema] of schema.fields) {
                    const storedKey = fieldSchema.storedKey;
                    const flexField = flexNode.tryGetField(brand(String(storedKey)));
                    if (flexField !== undefined) {
                        const childTreeNode = tryGetTreeNodeForField(flexField);
                        assert(childTreeNode !== undefined, 0xbc6 /* Expected child tree node for field. */);
                        result.push([propertyKey, childTreeNode]);
                    }
                }
                break;
            }
            case NodeKind.Leaf: {
                fail(0xbc7 /* Leaf schema associated with non-leaf tree node. */);
            }
            default: {
                unreachableCase(schema.kind);
            }
        }
        return result;
    },
    tagContentSchema(schema, node) {
        if (typeof node === "object" && node !== null && !isFluidHandle(node)) {
            Reflect.defineProperty(node, contentSchemaSymbol, {
                configurable: false,
                enumerable: false,
                writable: true,
                value: schema.identifier,
            });
        }
        return node;
    },
};
/**
 * Borrow a cursor from a field.
 * @remarks
 * The cursor must be put back to its original location before the node is used again.
 */
function borrowFieldCursorFromTreeNodeOrValue(node) {
    if (node === undefined) {
        return cursorForMapTreeField([]);
    }
    const cursor = borrowCursorFromTreeNodeOrValue(node);
    // TODO: avoid copy: borrow cursor from field instead.
    const mapTree = mapTreeFromCursor(cursor);
    return cursorForMapTreeField([mapTree]);
}
//# sourceMappingURL=treeAlpha.js.map