"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.visitDelta = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const index_js_1 = require("../../util/index.js");
const cursor_js_1 = require("./cursor.js");
const deltaUtil_js_1 = require("./deltaUtil.js");
/**
 * Implementation notes:
 *
 * The visit is organized into four phases:
 * 1. a detach pass
 * 2. root transfers
 * 3. an attach pass
 * 4. root destructions
 *
 * The core idea is that before content can be attached, it must first exist and be in a detached field.
 *
 * While the detach pass ensures that nodes to be attached are in a detached state, it does not guarantee that they
 * reside in the correct detached field. That is the responsibility of the root transfers phase.
 *
 * The attach phase carries out attaches.
 *
 * After the attach phase, roots destruction is carried out.
 * This needs to happen last to allow modifications to detached roots to be applied before they are destroyed.
 *
 * The details of the delta visit algorithm can impact how/when events are emitted by the objects that own the visitors.
 * For example, as of 2024-03-27, the subtreeChanged event of an AnchorNode is emitted when exiting a node during a
 * delta visit, and thus the two-pass nature of the algorithm means the event fires twice for any given change.
 */
/**
 * Crawls the given `delta`, calling `visitor`'s callback for each change encountered.
 * Each successive call to the visitor callbacks assumes that the change described by earlier calls have been applied
 * to the document tree. For example, for a change that removes the first and third node of a field, the visitor calls
 * will first call detach with a range from indices 0 to 1 then call detach with a range from indices 1 to 2.
 *
 * @param delta - The delta to be crawled.
 * @param visitor - The object to notify of the changes encountered.
 * @param detachedFieldIndex - Index responsible for keeping track of the existing detached fields.
 * @param latestRevision - The latest revision tag associated with this delta.
 */
function visitDelta(delta, visitor, detachedFieldIndex, latestRevision) {
    const detachPassRoots = new Map();
    const attachPassRoots = new Map();
    const rootTransfers = [];
    const rootDestructions = [];
    const refreshers = new Map();
    delta.refreshers?.forEach(({ id: { major, minor }, trees }) => {
        const treeCursors = nodeCursorsFromChunk(trees);
        for (let i = 0; i < trees.topLevelLength; i += 1) {
            const offsettedId = minor + i;
            (0, index_js_1.setInNestedMap)(refreshers, major, offsettedId, treeCursors[i]);
        }
    });
    const detachConfig = {
        func: detachPass,
        latestRevision,
        refreshers,
        detachedFieldIndex,
        detachPassRoots,
        attachPassRoots,
        rootTransfers,
        rootDestructions,
    };
    processBuilds(delta.build, detachConfig, visitor);
    processGlobal(delta.global, detachConfig, visitor);
    processRename(delta.rename, detachConfig);
    visitFieldMarks(delta.fields, visitor, detachConfig);
    fixedPointVisitOfRoots(visitor, detachPassRoots, detachConfig);
    transferRoots(rootTransfers, attachPassRoots, detachedFieldIndex, visitor, refreshers, latestRevision);
    const attachConfig = {
        func: attachPass,
        latestRevision,
        refreshers,
        detachedFieldIndex,
        detachPassRoots,
        attachPassRoots,
        rootTransfers,
        rootDestructions,
    };
    visitFieldMarks(delta.fields, visitor, attachConfig);
    fixedPointVisitOfRoots(visitor, attachPassRoots, attachConfig);
    collectDestroys(delta.destroy, attachConfig);
    for (const { id, count } of rootDestructions) {
        for (let i = 0; i < count; i += 1) {
            const offsetId = (0, deltaUtil_js_1.offsetDetachId)(id, i);
            const root = detachedFieldIndex.getEntry(offsetId);
            const field = detachedFieldIndex.toFieldKey(root);
            visitor.destroy(field, 1);
            detachedFieldIndex.deleteEntry(offsetId);
        }
    }
}
exports.visitDelta = visitDelta;
/**
 * Visits all nodes in `roots` until none are left.
 * This function tolerates entries being added to and removed from `roots` as part of visits.
 * @param visitor - The visitor to visit the roots with.
 * @param roots - The initial set of roots to visit.
 * Individual entries are removed prior to being visited.
 * @param config - The configuration to use for visits.
 */
function fixedPointVisitOfRoots(visitor, roots, config) {
    while (roots.size > 0) {
        for (const [root, modifications] of roots) {
            roots.delete(root);
            const field = config.detachedFieldIndex.toFieldKey(root);
            visitor.enterField(field);
            // Note: each visit may lead to `roots` being populated with new entries or having some entries removed.
            visitNode(0, modifications, visitor, config);
            visitor.exitField(field);
        }
    }
}
/**
 * Transfers roots from one detached field to another.
 * This occurs in the following circumstances:
 * - A changeset moves then removes a node
 * - A changeset restores then moves a node
 * - A changeset restores then removes a node
 * TODO#5481: update the DetachedFieldIndex instead of moving the nodes around.
 *
 * @param rootTransfers - The transfers to perform.
 * @param mapToUpdate - A map to update based on the transfers being performed.
 * @param detachedFieldIndex - The index to update based on the transfers being performed.
 * @param visitor - The visitor to inform of the transfers being performed.
 */
function transferRoots(rootTransfers, mapToUpdate, detachedFieldIndex, visitor, refreshers, revision) {
    let nextBatch = rootTransfers.flatMap(({ oldId, newId, count }) => {
        const atomized = [];
        // It's possible for a detached node to be revived transiently such that it ends up back in the same detached field.
        // Making such a transfer wouldn't just be inefficient, it would lead us to mistakenly think we have moved all content
        // out of the source detached field, and would lead us to delete the tree index entry for that source detached field.
        // This would effectively result in the tree index missing an entry for the detached field.
        // This if statement prevents that from happening.
        if (!(0, deltaUtil_js_1.areDetachedNodeIdsEqual)(oldId, newId)) {
            for (let i = 0; i < count; i += 1) {
                atomized.push({ oldId: (0, deltaUtil_js_1.offsetDetachId)(oldId, i), newId: (0, deltaUtil_js_1.offsetDetachId)(newId, i) });
            }
        }
        return atomized;
    });
    while (nextBatch.length > 0) {
        const delayed = [];
        const priorSize = nextBatch.length;
        for (const { oldId, newId } of nextBatch) {
            let oldRootId = detachedFieldIndex.tryGetEntry(oldId);
            if (oldRootId === undefined) {
                const tree = (0, index_js_1.tryGetFromNestedMap)(refreshers, oldId.major, oldId.minor);
                if (tree !== undefined) {
                    buildTrees(oldId, [tree], detachedFieldIndex, revision, visitor);
                    oldRootId = detachedFieldIndex.getEntry(oldId);
                }
            }
            if (oldRootId === undefined) {
                // The source field is not populated.
                // This can happen when another rename needs to be performed first.
                delayed.push({ oldId, newId });
                continue;
            }
            let newRootId = detachedFieldIndex.tryGetEntry(newId);
            if (newRootId !== undefined) {
                // The destination field is already occupied.
                // This can happen when another rename needs to be performed first.
                delayed.push({ oldId, newId });
                continue;
            }
            newRootId = detachedFieldIndex.createEntry(newId, revision);
            const fields = mapToUpdate.get(oldRootId);
            if (fields !== undefined) {
                mapToUpdate.delete(oldRootId);
                mapToUpdate.set(newRootId, fields);
            }
            const oldField = detachedFieldIndex.toFieldKey(oldRootId);
            const newField = detachedFieldIndex.toFieldKey(newRootId);
            visitor.enterField(oldField);
            visitor.detach({ start: 0, end: 1 }, newField, newId, false);
            visitor.exitField(oldField);
            detachedFieldIndex.deleteEntry(oldId);
        }
        (0, internal_1.assert)(delayed.length < priorSize, 0x7cf /* transferRoots should make progress */);
        nextBatch = delayed;
    }
}
function visitFieldMarks(fields, visitor, config) {
    if (fields !== undefined) {
        for (const [key, field] of fields) {
            visitor.enterField(key);
            config.func(field, visitor, config);
            visitor.exitField(key);
        }
    }
}
function visitNode(index, fields, visitor, config) {
    if (fields !== undefined) {
        visitor.enterNode(index);
        visitFieldMarks(fields, visitor, config);
        visitor.exitNode(index);
    }
}
/**
 * Performs the following:
 * - Collects all roots that may need an attach pass
 * - Executes detaches (bottom-up)
 */
function detachPass(fieldChanges, visitor, config) {
    let index = 0;
    for (const mark of fieldChanges) {
        if (mark.fields !== undefined) {
            (0, internal_1.assert)(mark.attach === undefined || mark.detach !== undefined, 0x7d0 /* Invalid nested changes on an additive mark */);
            visitNode(index, mark.fields, visitor, config);
        }
        if (mark.detach !== undefined) {
            for (let i = 0; i < mark.count; i += 1) {
                const id = (0, deltaUtil_js_1.offsetDetachId)(mark.detach, i);
                const root = config.detachedFieldIndex.createEntry(id, config.latestRevision);
                if (mark.fields !== undefined) {
                    config.attachPassRoots.set(root, mark.fields);
                }
                const field = config.detachedFieldIndex.toFieldKey(root);
                visitor.detach({ start: index, end: index + 1 }, field, id, mark.attach !== undefined);
            }
        }
        if (mark.detach === undefined && mark.attach === undefined) {
            index += mark.count;
        }
    }
}
function buildTrees(id, trees, detachedFieldIndex, latestRevision, visitor) {
    for (const [i, tree] of trees.entries()) {
        const offsettedId = (0, deltaUtil_js_1.offsetDetachId)(id, i);
        let root = detachedFieldIndex.tryGetEntry(offsettedId);
        (0, internal_1.assert)(root === undefined, 0x929 /* Unable to build tree that already exists */);
        root = detachedFieldIndex.createEntry(offsettedId, latestRevision);
        const field = detachedFieldIndex.toFieldKey(root);
        visitor.create([tree], field);
    }
}
function processBuilds(builds, config, visitor) {
    if (builds !== undefined) {
        for (const { id, trees } of builds) {
            buildTrees(id, nodeCursorsFromChunk(trees), config.detachedFieldIndex, config.latestRevision, visitor);
        }
    }
}
function processGlobal(global, config, visitor) {
    if (global !== undefined) {
        for (const { id, fields } of global) {
            let root = config.detachedFieldIndex.tryGetEntry(id);
            if (root === undefined) {
                const tree = (0, index_js_1.tryGetFromNestedMap)(config.refreshers, id.major, id.minor);
                (0, internal_1.assert)(tree !== undefined, 0x928 /* refresher data not found */);
                buildTrees(id, [tree], config.detachedFieldIndex, config.latestRevision, visitor);
                root = config.detachedFieldIndex.getEntry(id);
            }
            // the revision is updated for any refresher data included in the delta that is used
            config.detachedFieldIndex.updateLatestRevision(id, config.latestRevision);
            config.detachPassRoots.set(root, fields);
            config.attachPassRoots.set(root, fields);
        }
    }
}
function processRename(rename, config) {
    if (rename !== undefined) {
        config.rootTransfers.push(...rename);
    }
}
function collectDestroys(destroys, config) {
    if (destroys !== undefined) {
        config.rootDestructions.push(...destroys);
    }
}
/**
 * Preforms the following:
 * - Executes attaches (top-down) applying nested changes on the attached nodes
 */
function attachPass(fieldChanges, visitor, config) {
    let index = 0;
    for (const mark of fieldChanges) {
        if (mark.attach !== undefined) {
            for (let i = 0; i < mark.count; i += 1) {
                const offsetAttachId = (0, deltaUtil_js_1.offsetDetachId)(mark.attach, i);
                let sourceRoot = config.detachedFieldIndex.tryGetEntry(offsetAttachId);
                if (sourceRoot === undefined) {
                    const tree = (0, index_js_1.tryGetFromNestedMap)(config.refreshers, offsetAttachId.major, offsetAttachId.minor);
                    (0, internal_1.assert)(tree !== undefined, 0x92a /* refresher data not found */);
                    buildTrees(offsetAttachId, [tree], config.detachedFieldIndex, config.latestRevision, visitor);
                    sourceRoot = config.detachedFieldIndex.getEntry(offsetAttachId);
                }
                const sourceField = config.detachedFieldIndex.toFieldKey(sourceRoot);
                const offsetIndex = index + i;
                visitor.attach(sourceField, 1, offsetIndex);
                config.detachedFieldIndex.deleteEntry(offsetAttachId);
                const fields = config.attachPassRoots.get(sourceRoot);
                if (fields !== undefined) {
                    config.attachPassRoots.delete(sourceRoot);
                    visitNode(offsetIndex, fields, visitor, config);
                }
            }
        }
        if (mark.detach === undefined && mark.fields !== undefined) {
            visitNode(index, mark.fields, visitor, config);
        }
        if (mark.detach === undefined || mark.attach !== undefined) {
            index += mark.count;
        }
    }
}
/**
 * Converts a chunk of trees into an array of cursors.
 *
 * TODO: Update the visitDelta logic and downstream APIs to avoid splitting up sequences into individual nodes.
 */
function nodeCursorsFromChunk(trees) {
    return (0, cursor_js_1.mapCursorField)(trees.cursor(), (c) => c.fork());
}
//# sourceMappingURL=visitDelta.js.map