/*!
 * 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 { assert, Lazy } from "@fluidframework/core-utils/internal";
import { createEmitter } from "@fluid-internal/client-utils";
import { anchorSlot, } from "../../core/index.js";
import { assertFlexTreeEntityNotFreed, ContextSlot, flexTreeSlot, isFreedSymbol, LazyEntity, TreeStatus, treeStatusFromAnchorCache, } from "../../feature-libraries/index.js";
import { fail } from "../../util/index.js";
// TODO: decide how to deal with dependencies on flex-tree implementation.
// eslint-disable-next-line import/no-internal-modules
import { makeTree } from "../../feature-libraries/flex-tree/lazyNode.js";
import { SimpleContextSlot } from "./context.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 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 Lazy((createEmitter)));
        assert(!treeNodeToKernel.has(node), 0xa1a /* only one kernel per node can be made */);
        treeNodeToKernel.set(node, this);
        if (innerNode instanceof UnhydratedFlexTreeNode) {
            // Unhydrated case
            mapTreeNodeToProxy.set(innerNode, 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 n = innerNode;
                    while (n !== undefined) {
                        const treeNode = mapTreeNodeToProxy.get(n);
                        if (treeNode !== undefined) {
                            const kernel = getKernel(treeNode);
                            __classPrivateFieldGet(kernel, _TreeNodeKernel_unhydratedEvents, "f").value.emit("subtreeChangedAfterBatch");
                        }
                        // This cast is safe because the parent (if it exists) of an unhydrated flex node is always another unhydrated flex node.
                        n = n.parentField.parent.parent;
                    }
                }),
            }, "f");
        }
        else {
            // Hydrated case
            const { anchorNode } = innerNode;
            assert(!anchorNode.slots.has(proxySlot), 0x7f5 /* Cannot associate an flex node with multiple simple-tree nodes */);
            __classPrivateFieldSet(this, _TreeNodeKernel_hydrationState, this.createHydratedState(anchorNode), "f");
        }
    }
    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("missing simple-tree context"));
        }
        return this.initialContext;
    }
    /**
     * Transition from {@link Unhydrated} to hydrated.
     * Bi-directionally associates the given hydrated TreeNode to the given anchor node.
     * @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 the proxy has an anchor node.
     */
    hydrate(anchorNode) {
        assert(!this.disposed, 0xa2a /* cannot hydrate a disposed node */);
        assert(!isHydrated(__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f")), 0xa2b /* hydration should only happen once */);
        mapTreeNodeToProxy.delete(__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode);
        __classPrivateFieldSet(this, _TreeNodeKernel_hydrationState, this.createHydratedState(anchorNode), "f");
        // 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) {
        anchorNode.slots.set(proxySlot, 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[isFreedSymbol]()) {
                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() {
        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 via {@link setInnerNode}.
     * @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.
     */
    getOrCreateInnerNode(allowFreed = false) {
        if (!isHydrated(__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f"))) {
            return __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode; // Unhydrated case
        }
        if (__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode !== undefined) {
            return __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode; // Cooked case
        }
        // Marinated case -> cooked
        const anchorNode = __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").anchorNode;
        // The proxy 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("missing context");
            const cursor = context.checkout.forest.allocateCursor("getFlexNode");
            context.checkout.forest.moveCursorToPath(anchorNode, cursor);
            __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode = makeTree(context, cursor);
            cursor.free();
            // Calling this is a performance improvement, however, do this only after demand to avoid momentarily having no anchors to anchorNode
            anchorForgetters?.get(this.node)?.();
            if (!allowFreed) {
                assertFlexTreeEntityNotFreed(__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode);
            }
        }
        return __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode;
    }
    /**
     * Creates an anchor node and associates it with the given proxy.
     * @privateRemarks
     * Use `forgetters` to cleanup the anchor allocated by this function once the anchor is no longer needed.
     * In practice, this happens when either the anchor node is destroyed, or another anchor to the same node is created by a new flex node.
     *
     * The FlexTreeNode holds a reference to the same anchor, and has a lifetime at least as long as the simple-tree,
     * so this would be unnecessary except for the case of "marinated" nodes, which have an anchor,
     * but might not have a FlexTreeNode.
     * Handling this case is an optimization assuming that this extra anchor reference is cheaper than eagerly creating FlexTreeNodes.
     */
    anchorProxy(anchors, path) {
        assert(!anchorForgetters.has(this.node), 0x91c /* Proxy anchor should not be set twice */);
        const anchor = anchors.track(path);
        const anchorNode = anchors.locate(anchor) ?? fail("Expected anchor node to be present");
        this.hydrate(anchorNode);
        const forget = () => {
            if (anchors.locate(anchor)) {
                anchors.forget(anchor);
            }
            anchorForgetters.delete(this.node);
            off();
        };
        anchorForgetters.set(this.node, forget);
        const off = anchorNode.events.on("afterDestroy", forget);
        return anchorNode;
    }
    /**
     * Retrieves the InnerNode associated with the given target via {@link setInnerNode}, if any.
     * @remarks
     * If `target` is an unhydrated node, returns its UnhydratedFlexTreeNode.
     * If `target` is a cooked node (or marinated but a FlexTreeNode exists) returns the FlexTreeNode.
     * If the target is a marinated node with no FlexTreeNode for its anchor, returns undefined.
     */
    tryGetInnerNode() {
        if (isHydrated(__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f"))) {
            return (__classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode ??
                __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").anchorNode.slots.get(flexTreeSlot));
        }
        return __classPrivateFieldGet(this, _TreeNodeKernel_hydrationState, "f").innerNode;
    }
}
_TreeNodeKernel_hydrationState = new WeakMap(), _TreeNodeKernel_unhydratedEvents = new WeakMap();
/**
 * Used by {@link anchorProxy} as an optimization to ensure that only one anchor is remembered at a time for a given anchor node
 */
const anchorForgetters = new WeakMap();
const kernelEvents = ["childrenChangedAfterBatch", "subtreeChangedAfterBatch"];
/**
 * {@inheritdoc proxyToMapTreeNode}
 */
export const mapTreeNodeToProxy = new WeakMap();
/**
 * An anchor slot which associates an anchor with its corresponding 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 proxySlot = anchorSlot();
/**
 * Retrieves the node associated with the given MapTreeNode node if any.
 */
export function tryGetTreeNodeFromMapNode(flexNode) {
    return mapTreeNodeToProxy.get(flexNode);
}
export function tryDisposeTreeNode(anchorNode) {
    const treeNode = anchorNode.slots.get(proxySlot);
    if (treeNode !== undefined) {
        const kernel = getKernel(treeNode);
        kernel.dispose();
    }
}
/**
 * Lookup a TreeNodeSchema from a Hydrated FlexTreeNode.
 * @privateRemarks
 * This provides a way to access simple tree schema from the flex tree without depending on {@link FlexTreeSchema} which is in the process of being removed.
 * This is currently limited to hydrated nodes: this limitation will have to be fixed before {@link FlexTreeSchema} can be fully removed.
 */
export function getTreeNodeSchemaFromHydratedFlexNode(flexNode) {
    assert(flexNode.context.isHydrated(), 0xa56 /* getTreeNodeSchemaFromHydratedFlexNode only allows hydrated flex tree nodes */);
    const context = flexNode.anchorNode.anchorSet.slots.get(SimpleContextSlot) ??
        fail("Missing SimpleContextSlot");
    return context.schema.get(flexNode.schema) ?? fail("Missing schema");
}
/**
 * Retrieves the flex node associated with the given target via {@link setInnerNode}.
 * @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.
 */
export function getOrCreateInnerNode(treeNode, allowFreed = false) {
    const kernel = getKernel(treeNode);
    return kernel.getOrCreateInnerNode(allowFreed);
}
/**
 * Gets a flex node from an anchor node
 */
function flexNodeFromAnchor(anchorNode) {
    // the proxy 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) {
        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("missing context");
    const cursor = context.checkout.forest.allocateCursor("getFlexNode");
    context.checkout.forest.moveCursorToPath(anchorNode, cursor);
    const newFlexNode = makeTree(context, cursor);
    cursor.free();
    return newFlexNode;
}
/**
 * Gets a tree node from an anchor node
 */
export function treeNodeFromAnchor(anchorNode) {
    const cached = anchorNode.slots.get(proxySlot);
    if (cached !== undefined) {
        return cached;
    }
    const flexNode = flexNodeFromAnchor(anchorNode);
    const classSchema = getTreeNodeSchemaFromHydratedFlexNode(flexNode);
    return typeof classSchema === "function"
        ? new classSchema(flexNode)
        : classSchema.create(flexNode);
}
//# sourceMappingURL=treeNodeKernel.js.map