/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/core-utils/internal";
import { createAnnouncedVisitor, } from "../../core/index.js";
import { TreeNode, treeNodeFromAnchor } from "../core/index.js";
/**
 * Registers a visitor on the view's forest that tracks which nodes are dirty.
 * @param view - The view to track dirty nodes on
 * @param dirty - A {@link DirtyTreeMap | map} that will be updated over time to reflect the {@link DirtyTreeStatus | status} of nodes as they change.
 * Nodes that have not changed will not be inserted/updated in the map.
 * @returns a cleanup function that should be called when the tracking is no longer needed.
 * @example
 * ```typescript
 * const dirty = new Map<TreeNode, DirtyTreeStatus>();
 * const stopTracking = trackDirtyNodes(view, dirty);
 * // ... make changes to the view ...
 * console.log(`The root of the tree is ${dirty.get(view.root) ?? "unchanged"}`);
 * stopTracking();
 * ```
 * @privateRemarks
 * This provides an opportunity for user code ("set" on dirty) to run during forest changes, in the middle of processing an edit.
 * We have generally avoided exposing such times to users and instead flush events after the edit has finished and user facing invariants are known to be restored.
 * Before stabilizing, how this event relates to other events time/ordering wise should be clarified, and if any APIs have been provided to defer events, that mentioned as well.
 * Maybe updates should be deferred?
 *
 * This is also documented in terms of "forest" which is not a concept in the public API, and thus needs to be redefined in terms of public concepts.
 * @alpha
 */
export function trackDirtyNodes(view, dirty) {
    const forest = view.checkout.forest;
    const announcedVisitor = () => createDirtyVisitor(forest, dirty);
    forest.registerAnnouncedVisitor(announcedVisitor);
    return () => {
        forest.deregisterAnnouncedVisitor(announcedVisitor);
    };
}
function createDirtyVisitor(forest, dirty) {
    // When cursor is in Fields mode, `parentField` is the field and `parent` is the parent node above that field (if any).
    // When cursor is in Nodes mode, `parent` is the current node and `parentField` is undefined.
    let parentField;
    let parent;
    return createAnnouncedVisitor({
        beforeDetach: (src) => {
            assert(parent !== undefined, 0xc48 /* Expected node */);
            assert(parentField !== undefined, 0xc49 /* Expected field */);
            for (let parentIndex = src.start; parentIndex < src.end; parentIndex++) {
                const path = {
                    parent,
                    parentField,
                    parentIndex,
                };
                // The only way a detached node can be re-attached (and become visible/usable again) is via a move, so mark it as moved ahead of time.
                const node = getNodeAtPath(forest, path);
                if (node !== undefined && dirty.get(node) === undefined) {
                    // Only mark the node as moved if it is not already marked as something else.
                    dirty.set(node, "moved");
                }
                // Mark the parent as changed unless it is already marked as new.
                const parentNode = getNodeAtPath(forest, parent);
                if (parentNode !== undefined && dirty.get(parentNode) !== "new") {
                    dirty.set(parentNode, "changed");
                }
            }
        },
        afterAttach: (_, dst) => {
            assert(parent !== undefined, 0xc4a /* Expected node */);
            assert(parentField !== undefined, 0xc4b /* Expected field */);
            for (let parentIndex = dst.start; parentIndex < dst.end; parentIndex++) {
                const path = {
                    parent,
                    parentField,
                    parentIndex,
                };
                const node = getNodeAtPath(forest, path);
                if (node !== undefined && dirty.get(node) === undefined) {
                    // Only mark the node as new if it is not already marked as something else - this ensures that a moved node is not marked as new (since nodes are marked move when detached).
                    dirty.set(node, "new");
                }
                // Mark the parent as changed unless it is already marked as new.
                const parentNode = getNodeAtPath(forest, parent);
                if (parentNode !== undefined && dirty.get(parentNode) !== "new") {
                    dirty.set(parentNode, "changed");
                }
            }
        },
        enterNode(index) {
            assert(parentField !== undefined, 0xc4c /* Expected field */);
            parent = {
                parent,
                parentField,
                parentIndex: index,
            };
            parentField = undefined;
        },
        exitNode() {
            assert(parent !== undefined, 0xc4d /* Expected node */);
            parentField = parent.parentField;
            parent = parent.parent;
        },
        enterField: (key) => {
            parentField = key;
        },
        exitField() {
            parentField = undefined;
        },
    });
}
function getNodeAtPath(forest, path) {
    const cursor = forest.allocateCursor();
    forest.moveCursorToPath(path, cursor);
    assert(cursor.mode === 0 /* CursorLocationType.Nodes */, 0xc4e /* attach should happen in a node */);
    const anchor = cursor.buildAnchor();
    const anchorNode = forest.anchors.locate(anchor);
    cursor.free();
    if (anchorNode !== undefined) {
        const node = treeNodeFromAnchor(anchorNode);
        if (node instanceof TreeNode) {
            return node;
        }
    }
    return undefined;
}
//# sourceMappingURL=dirtyIndex.js.map