"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
    return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
    if (kind === "m") throw new TypeError("Private method is not writable");
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
    return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _TreeNodeKernel_hydrationState, _TreeNodeKernel_unhydratedEvents;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createTreeNodeFromInner = exports.treeNodeFromAnchor = exports.getOrCreateInnerNode = exports.getSimpleContextFromInnerNode = exports.getSimpleNodeSchemaFromInnerNode = exports.tryDisposeTreeNode = exports.simpleTreeNodeSlot = exports.splitInnerNodeType = exports.TreeNodeKernel = exports.tryGetTreeNodeSchema = exports.isTreeNode = exports.getKernel = void 0;
const client_utils_1 = require("@fluid-internal/client-utils");
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const index_js_1 = require("../../core/index.js");
// TODO: decide how to deal with dependencies on flex-tree implementation.
// eslint-disable-next-line import/no-internal-modules
const lazyNode_js_1 = require("../../feature-libraries/flex-tree/lazyNode.js");
const index_js_2 = require("../../feature-libraries/index.js");
const context_js_1 = require("./context.js");
const unhydratedFlexTree_js_1 = require("./unhydratedFlexTree.js");
const treeNodeToKernel = new WeakMap();
function getKernel(node) {
    const kernel = treeNodeToKernel.get(node);
    (0, internal_1.assert)(kernel !== undefined, 0x9b1 /* Expected tree node to have kernel */);
    return kernel;
}
exports.getKernel = getKernel;
/**
 * Detects if the given 'candidate' is a TreeNode.
 *
 * @remarks
 * Supports both Hydrated and {@link Unhydrated} TreeNodes, both of which return true.
 *
 * Because the common usage is to check if a value being inserted/set is a TreeNode,
 * this function permits calling with primitives as well as objects.
 *
 * Primitives will always return false (as they are copies of data, not references to nodes).
 *
 * @param candidate - Value which may be a TreeNode
 * @returns true if the given 'candidate' is a hydrated TreeNode.
 */
function isTreeNode(candidate) {
    return treeNodeToKernel.has(candidate);
}
exports.isTreeNode = isTreeNode;
/**
 * Returns a schema for a value if the value is a {@link TreeNode}.
 *
 * Returns undefined for other values.
 * @remarks
 * Does not give schema for a {@link TreeLeafValue}.
 */
function tryGetTreeNodeSchema(value) {
    const kernel = treeNodeToKernel.get(value);
    return kernel?.schema;
}
exports.tryGetTreeNodeSchema = tryGetTreeNodeSchema;
/** True if and only if the given {@link HydrationState} is post-hydration */
function isHydrated(state) {
    return state.anchorNode !== undefined;
}
/**
 * Contains state and an internal API for managing {@link TreeNode}s.
 * @remarks All {@link TreeNode}s have an associated kernel object.
 * The kernel has the same lifetime as the node and spans both its unhydrated and hydrated states.
 */
class TreeNodeKernel {
    /**
     * Create a TreeNodeKernel which can be looked up with {@link getKernel}.
     *
     * @param initialContext - context from when this node was originally crated.
     * @param innerNode - When unhydrated/raw or marinated the MapTreeNode. FlexTreeNode when cooked.
     * @remarks
     * Exactly one kernel per TreeNode should be created.
     */
    constructor(node, schema, innerNode, initialContext) {
        this.node = node;
        this.schema = schema;
        this.initialContext = initialContext;
        this.disposed = false;
        /**
         * Generation number which is incremented any time we have an edit on the node.
         * Used during iteration to make sure there has been no edits that were concurrently made.
         * @remarks
         * This is updated monotonically by this class when edits are applied.
         * TODO: update this when applying edits to unhydrated trees.
         *
         * If TypeScript supported making this immutable from outside the class without making it readonly from inside, that would be used here,
         * but they only way to do that is add a separate public accessor and make it private, which was deemed not worth the boilerplate, runtime overhead and bundle size.
         */
        this.generationNumber = 0;
        _TreeNodeKernel_hydrationState.set(this, void 0);
        /**
         * Events registered before hydration.
         * @remarks
         * Since these are usually not used, they are allocated lazily as an optimization.
         * The laziness also avoids extra forwarding overhead for events from this kernel's anchor node and also avoids registering for events that are unneeded.
         * This means optimizations like skipping processing data in subtrees where no subtreeChanged events are subscribed to would be able to work,
         * since the kernel does not unconditionally subscribe to those events (like a design which simply forwards all events would).
         */
        _TreeNodeKernel_unhydratedEvents.set(this, new internal_1.Lazy((client_utils_1.createEmitter)));
        splitInnerNodeType(innerNode);
        (0, internal_1.assert)(!treeNodeToKernel.has(node), 0xa1a /* only one kernel per node can be made */);
        treeNodeToKernel.set(node, this);
        if (innerNode instanceof unhydratedFlexTree_js_1.UnhydratedFlexTreeNode) {
            // Unhydrated case
            (0, internal_1.debugAssert)(() => innerNode.treeNode === undefined);
            innerNode.treeNode = node;
            // Register for change events from the unhydrated flex node.
            // These will be fired if the unhydrated node is edited, and will also be forwarded later to the hydrated node.
            __classPrivateFieldSet(this, _TreeNodeKernel_hydrationState, {
                innerNode,
                off: innerNode.events.on("childrenChangedAfterBatch", ({ changedFields }) => {
                    __classPrivateFieldGet(this, _TreeNodeKernel_unhydratedEvents, "f").value.emit("childrenChangedAfterBatch", {
                        changedFields,
                    });
                    let unhydratedNode = innerNode;
                    while (unhydratedNode !== undefined) {
                        const treeNode = unhydratedNode.treeNode;
                        if (treeNode !== undefined) {
                            const kernel = getKernel(treeNode);
                            __classPrivateFieldGet(kernel, _TreeNodeKernel_unhydratedEvents, "f").value.emit("subtreeChangedAfterBatch");
                        }
                        const parentNode = unhydratedNode.parentField.parent.parent;
                        (0, internal_1.assert)(parentNode === undefined || parentNode instanceof unhydratedFlexTree_js_1.UnhydratedFlexTreeNode, 0xb76 /* Unhydrated node's parent should be an unhydrated node */);
                        unhydratedNode = parentNode;
                    }
                }),
            }, "f");
        }
        else {
            // Hydrated case
            __classPrivateFieldSet(this, _TreeNodeKernel_hydrationState, this.createHydratedState(innerNode.anchorNode), "f");
            __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode = innerNode;
        }
    }
    get context() {
        if (isHydrated(__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f"))) {
            // This can't be cached on this.#hydrated during hydration since initial tree is hydrated before the context is cached on the anchorSet.
            return (__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f")?.anchorNode.anchorSet.slots.get(context_js_1.SimpleContextSlot) ??
                (0, internal_1.fail)(0xb40 /* missing simple-tree context */));
        }
        return this.initialContext;
    }
    /**
     * Transition from {@link Unhydrated} to hydrated.
     * Bi-directionally associates the given hydrated TreeNode to the anchor node at the provided path.
     * @remarks
     * Happens at most once for any given node.
     * Cleans up mappings to {@link UnhydratedFlexTreeNode} - it is assumed that they are no longer needed once this node has an anchor node.
     */
    hydrate(anchors, path) {
        (0, internal_1.assert)(!this.disposed, 0xa2a /* cannot hydrate a disposed node */);
        (0, internal_1.assert)(!isHydrated(__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f")), 0xa2b /* hydration should only happen once */);
        const anchor = anchors.track(path);
        const anchorNode = anchors.locate(anchor) ?? (0, internal_1.fail)(0xb42 /* Expected anchor node to be present */);
        __classPrivateFieldSet(this, _TreeNodeKernel_hydrationState, this.createHydratedState(anchorNode), "f");
        __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").offAnchorNode.add(() => anchors.forget(anchor));
        // If needed, register forwarding emitters for events from before hydration
        if (__classPrivateFieldGet(this, _TreeNodeKernel_unhydratedEvents, "f").evaluated) {
            const events = __classPrivateFieldGet(this, _TreeNodeKernel_unhydratedEvents, "f").value;
            for (const eventName of kernelEvents) {
                if (events.hasListeners(eventName)) {
                    __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").offAnchorNode.add(
                    // Argument is forwarded between matching events, so the type should be correct.
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
                    anchorNode.events.on(eventName, (arg) => events.emit(eventName, arg)));
                }
            }
        }
    }
    createHydratedState(anchorNode) {
        (0, internal_1.assert)(!anchorNode.slots.has(exports.simpleTreeNodeSlot), 0x7f5 /* Cannot associate an flex node with multiple simple-tree nodes */);
        anchorNode.slots.set(exports.simpleTreeNodeSlot, this.node);
        return {
            anchorNode,
            offAnchorNode: new Set([
                anchorNode.events.on("afterDestroy", () => this.dispose()),
                // TODO: this should be triggered on change even for unhydrated nodes.
                anchorNode.events.on("childrenChanging", () => {
                    this.generationNumber += 1;
                }),
            ]),
        };
    }
    getStatus() {
        if (this.disposed) {
            return index_js_2.TreeStatus.Deleted;
        }
        if (!isHydrated(__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f"))) {
            return index_js_2.TreeStatus.New;
        }
        // TODO: Replace this check with the proper check against the cursor state when the cursor becomes part of the kernel
        const flex = __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").anchorNode.slots.get(index_js_2.flexTreeSlot);
        if (flex !== undefined) {
            (0, internal_1.assert)(flex instanceof index_js_2.LazyEntity, 0x9b4 /* Unexpected flex node implementation */);
            if (flex.isFreed()) {
                return index_js_2.TreeStatus.Deleted;
            }
        }
        return (0, index_js_2.treeStatusFromAnchorCache)(__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").anchorNode);
    }
    get events() {
        // Retrieve the correct events object based on whether this node is pre or post hydration.
        return isHydrated(__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f"))
            ? __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").anchorNode.events
            : __classPrivateFieldGet(this, _TreeNodeKernel_unhydratedEvents, "f").value;
    }
    dispose() {
        (0, internal_1.debugAssert)(() => !this.disposed || "Cannot dispose a disposed node");
        this.disposed = true;
        if (isHydrated(__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f"))) {
            for (const off of __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").offAnchorNode) {
                off();
            }
        }
        // TODO: go to the context and remove myself from withAnchors
    }
    isHydrated() {
        return isHydrated(__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f"));
    }
    get anchorNode() {
        return isHydrated(__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f")) ? __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").anchorNode : undefined;
    }
    /**
     * Retrieves the flex node associated with the given target.
     * @remarks
     * For {@link Unhydrated} nodes, this returns the MapTreeNode.
     *
     * For hydrated nodes it returns a FlexTreeNode backed by the forest.
     * Note that for "marinated" nodes, this FlexTreeNode exists and returns it: it does not return the MapTreeNode which is the current InnerNode.
     *
     * @throws A {@link @fluidframework/telemetry-utils#UsageError} if the node has been deleted.
     */
    getOrCreateInnerNode() {
        if (!isHydrated(__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f"))) {
            (0, internal_1.debugAssert)(() => __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode?.context.isDisposed() === false ||
                "Unhydrated node should never be disposed");
            return __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode; // Unhydrated case
        }
        if (this.disposed) {
            throw new internal_2.UsageError("Cannot access a deleted node.");
        }
        if (__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode === undefined) {
            // Marinated case -> cooked
            const anchorNode = __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").anchorNode;
            // This TreeNode is bound to an anchor node, but it may or may not have an actual flex node yet
            const flexNode = anchorNode.slots.get(index_js_2.flexTreeSlot);
            if (flexNode !== undefined) {
                // If the flex node already exists, use it...
                __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode = flexNode;
            }
            else {
                // ...otherwise, the flex node must be created
                const context = anchorNode.anchorSet.slots.get(index_js_2.ContextSlot) ?? (0, internal_1.fail)(0xb41 /* missing context */);
                const cursor = context.checkout.forest.allocateCursor("getFlexNode");
                context.checkout.forest.moveCursorToPath(anchorNode, cursor);
                __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode = (0, lazyNode_js_1.makeTree)(context, cursor);
                cursor.free();
                (0, index_js_2.assertFlexTreeEntityNotFreed)(__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode);
            }
        }
        return __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode;
    }
    /**
     * Retrieves the {@link UnhydratedFlexTreeNode} if unhydrated. otherwise undefined.
     */
    getInnerNodeIfUnhydrated() {
        if (isHydrated(__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f"))) {
            return undefined;
        }
        return __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode;
    }
}
exports.TreeNodeKernel = TreeNodeKernel;
_TreeNodeKernel_hydrationState = new WeakMap(), _TreeNodeKernel_unhydratedEvents = new WeakMap();
const kernelEvents = ["childrenChangedAfterBatch", "subtreeChangedAfterBatch"];
/**
 * Narrows innerNode to either {@link UnhydratedFlexTreeNode} or {@link HydratedFlexTreeNode}.
 */
function splitInnerNodeType(innerNode) {
    (0, internal_1.assert)(innerNode instanceof unhydratedFlexTree_js_1.UnhydratedFlexTreeNode || innerNode.isHydrated(), 0xbc8 /* Invalid inner node type */);
}
exports.splitInnerNodeType = splitInnerNodeType;
/**
 * An anchor slot which associates an anchor with its corresponding {@link TreeNode}, if there is one.
 * @remarks
 * For this to work, we have to require that there is at most a single view using a given AnchorSet.
 * FlexTree already has this assumption, and we also assume there is a single simple-tree per FlexTree, so this is valid.
 */
exports.simpleTreeNodeSlot = (0, index_js_1.anchorSlot)();
/**
 * Dispose a TreeNode (if any) for an existing anchor without disposing the anchor.
 */
function tryDisposeTreeNode(anchorNode) {
    const treeNode = anchorNode.slots.get(exports.simpleTreeNodeSlot);
    if (treeNode !== undefined) {
        const kernel = getKernel(treeNode);
        kernel.dispose();
        anchorNode.slots.delete(exports.simpleTreeNodeSlot);
    }
}
exports.tryDisposeTreeNode = tryDisposeTreeNode;
/**
 * Gets the {@link TreeNodeSchema} for the {@link InnerNode}.
 */
function getSimpleNodeSchemaFromInnerNode(innerNode) {
    const context = getSimpleContextFromInnerNode(innerNode);
    return context.schema.get(innerNode.type) ?? (0, internal_1.fail)(0xb3f /* missing schema from context */);
}
exports.getSimpleNodeSchemaFromInnerNode = getSimpleNodeSchemaFromInnerNode;
/**
 * Gets the {@link Context} for the {@link InnerNode}.
 */
function getSimpleContextFromInnerNode(innerNode) {
    splitInnerNodeType(innerNode);
    if (innerNode instanceof unhydratedFlexTree_js_1.UnhydratedFlexTreeNode) {
        return innerNode.simpleContext;
    }
    const context = innerNode.anchorNode.anchorSet.slots.get(context_js_1.SimpleContextSlot);
    (0, internal_1.assert)(context !== undefined, 0xa55 /* missing simple tree context */);
    return context;
}
exports.getSimpleContextFromInnerNode = getSimpleContextFromInnerNode;
/**
 * Retrieves the flex node associated with the given target.
 * @remarks
 * For {@link Unhydrated} nodes, this returns the MapTreeNode.
 *
 * For hydrated nodes it returns a FlexTreeNode backed by the forest.
 * Note that for "marinated" nodes, this FlexTreeNode exists and returns it: it does not return the MapTreeNode which is the current InnerNode.
 *
 * @throws A {@link @fluidframework/telemetry-utils#UsageError} if the node has been deleted.
 */
function getOrCreateInnerNode(treeNode) {
    const kernel = getKernel(treeNode);
    return kernel.getOrCreateInnerNode();
}
exports.getOrCreateInnerNode = getOrCreateInnerNode;
/**
 * Gets a flex node from an anchor node
 */
function flexNodeFromAnchor(anchorNode) {
    const flexNode = anchorNode.slots.get(index_js_2.flexTreeSlot);
    if (flexNode !== undefined) {
        return flexNode; // If it does have a flex node, return it...
    } // ...otherwise, the flex node must be created
    const context = anchorNode.anchorSet.slots.get(index_js_2.ContextSlot) ?? (0, internal_1.fail)(0xb45 /* missing context */);
    const cursor = context.checkout.forest.allocateCursor("getFlexNode");
    context.checkout.forest.moveCursorToPath(anchorNode, cursor);
    const newFlexNode = (0, lazyNode_js_1.makeTree)(context, cursor);
    cursor.free();
    return newFlexNode;
}
/**
 * Gets a tree node from an anchor node
 */
function treeNodeFromAnchor(anchorNode) {
    const cached = anchorNode.slots.get(exports.simpleTreeNodeSlot);
    if (cached !== undefined) {
        return cached;
    }
    const flexNode = flexNodeFromAnchor(anchorNode);
    return createTreeNodeFromInner(flexNode);
}
exports.treeNodeFromAnchor = treeNodeFromAnchor;
/**
 * Constructs a TreeNode from an InnerNode.
 * @remarks
 * This does not do caching or validation: caller must ensure duplicate nodes for a given inner node are not created, and that the inner node is valid.
 */
function createTreeNodeFromInner(innerNode) {
    const classSchema = getSimpleNodeSchemaFromInnerNode(innerNode);
    const internal = innerNode;
    return typeof classSchema === "function"
        ? new classSchema(internal)
        : classSchema.create(internal);
}
exports.createTreeNodeFromInner = createTreeNodeFromInner;
//# sourceMappingURL=treeNodeKernel.js.map