/*!
 * 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;
import { createEmitter } from "@fluid-internal/client-utils";
import { assert, Lazy, fail, debugAssert } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { anchorSlot, } from "../../core/index.js";
import { getOrCreateHydratedFlexTreeNode } from "../../feature-libraries/index.js";
import { assertFlexTreeEntityNotFreed, ContextSlot, flexTreeSlot, LazyEntity, TreeStatus, treeStatusFromAnchorCache, } from "../../feature-libraries/index.js";
import { UnhydratedFlexTreeNode } from "./unhydratedFlexTree.js";
const treeNodeToKernel = new WeakMap();
export function getKernel(node) {
    const kernel = treeNodeToKernel.get(node);
    assert(kernel !== undefined, 0x9b1 /* Expected tree node to have kernel */);
    return kernel;
}
/**
 * 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.
 */
export function isTreeNode(candidate) {
    return treeNodeToKernel.has(candidate);
}
/**
 * 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}.
 */
export function tryGetTreeNodeSchema(value) {
    const kernel = treeNodeToKernel.get(value);
    return kernel?.schema;
}
/** 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.
 */
export class TreeNodeKernel {
    /**
     * Create a TreeNodeKernel which can be looked up with {@link getKernel}.
     *
     * @param initialContext - context from when this node was originally created. Only used when unhydrated.
     * @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 Lazy((createEmitter)));
        splitInnerNodeType(innerNode);
        assert(!treeNodeToKernel.has(node), 0xa1a /* only one kernel per node can be made */);
        treeNodeToKernel.set(node, this);
        if (innerNode instanceof UnhydratedFlexTreeNode) {
            // Unhydrated case
            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;
                        assert(parentNode === undefined || parentNode instanceof 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(SimpleContextSlot) ??
                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) {
        assert(!this.disposed, 0xa2a /* cannot hydrate a disposed node */);
        assert(!isHydrated(__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f")), 0xa2b /* hydration should only happen once */);
        const anchor = anchors.track(path);
        const anchorNode = anchors.locate(anchor) ?? 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) {
        assert(!anchorNode.slots.has(simpleTreeNodeSlot), 0x7f5 /* Cannot associate an flex node with multiple simple-tree nodes */);
        anchorNode.slots.set(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 TreeStatus.Deleted;
        }
        if (!isHydrated(__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f"))) {
            return 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(flexTreeSlot);
        if (flex !== undefined) {
            assert(flex instanceof LazyEntity, 0x9b4 /* Unexpected flex node implementation */);
            if (flex.isFreed()) {
                return TreeStatus.Deleted;
            }
        }
        return 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() {
        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"))) {
            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 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(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(ContextSlot) ?? fail(0xb41 /* missing context */);
                const cursor = context.checkout.forest.allocateCursor("getFlexNode");
                context.checkout.forest.moveCursorToPath(anchorNode, cursor);
                __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode = getOrCreateHydratedFlexTreeNode(context, cursor);
                cursor.free();
                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;
    }
}
_TreeNodeKernel_hydrationState = new WeakMap(), _TreeNodeKernel_unhydratedEvents = new WeakMap();
const kernelEvents = ["childrenChangedAfterBatch", "subtreeChangedAfterBatch"];
/**
 * Narrows innerNode to either {@link UnhydratedFlexTreeNode} or {@link HydratedFlexTreeNode}.
 */
export function splitInnerNodeType(innerNode) {
    assert(innerNode instanceof UnhydratedFlexTreeNode || innerNode.isHydrated(), 0xbc8 /* Invalid inner node type */);
}
/**
 * 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.
 */
export const simpleTreeNodeSlot = anchorSlot();
/**
 * Dispose a TreeNode (if any) for an existing anchor without disposing the anchor.
 */
export function tryDisposeTreeNode(anchorNode) {
    const treeNode = anchorNode.slots.get(simpleTreeNodeSlot);
    if (treeNode !== undefined) {
        const kernel = getKernel(treeNode);
        kernel.dispose();
        anchorNode.slots.delete(simpleTreeNodeSlot);
    }
}
/**
 * Gets the {@link TreeNodeSchema} for the {@link InnerNode}.
 */
export function getSimpleNodeSchemaFromInnerNode(innerNode) {
    const context = getSimpleContextFromInnerNode(innerNode);
    return context.schema.get(innerNode.type) ?? fail(0xb3f /* missing schema from context */);
}
/**
 * Gets the {@link Context} for the {@link InnerNode}.
 */
export function getSimpleContextFromInnerNode(innerNode) {
    splitInnerNodeType(innerNode);
    if (innerNode instanceof UnhydratedFlexTreeNode) {
        return innerNode.simpleContext;
    }
    const context = innerNode.anchorNode.anchorSet.slots.get(SimpleContextSlot);
    assert(context !== undefined, 0xa55 /* missing simple tree context */);
    return context;
}
/**
 * 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.
 */
export function getOrCreateInnerNode(treeNode) {
    const kernel = getKernel(treeNode);
    return kernel.getOrCreateInnerNode();
}
/**
 * Gets a flex node from an anchor node
 */
function flexNodeFromAnchor(anchorNode) {
    const flexNode = anchorNode.slots.get(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(ContextSlot) ?? fail(0xb45 /* missing context */);
    const cursor = context.checkout.forest.allocateCursor("getFlexNode");
    context.checkout.forest.moveCursorToPath(anchorNode, cursor);
    const newFlexNode = getOrCreateHydratedFlexTreeNode(context, cursor);
    cursor.free();
    return newFlexNode;
}
/**
 * Gets a tree node from an anchor node
 */
export function treeNodeFromAnchor(anchorNode) {
    const cached = anchorNode.slots.get(simpleTreeNodeSlot);
    if (cached !== undefined) {
        return cached;
    }
    const flexNode = flexNodeFromAnchor(anchorNode);
    return createTreeNodeFromInner(flexNode);
}
/**
 * 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.
 */
export function createTreeNodeFromInner(innerNode) {
    const classSchema = getSimpleNodeSchemaFromInnerNode(innerNode);
    const internal = innerNode;
    return typeof classSchema === "function"
        ? new classSchema(internal)
        : classSchema.create(internal);
}
/**
 * Creating multiple simple tree contexts for the same branch, and thus with the same underlying AnchorSet does not work due to how TreeNode caching works.
 * This slot is used to detect if one already exists and error if creating a second.
 * @remarks
 * See also {@link ContextSlot} in which the flex-tree context is stored.
 */
export const SimpleContextSlot = anchorSlot();
//# sourceMappingURL=treeNodeKernel.js.map