"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.visitDelta = void 0;
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
const core_utils_1 = require("@fluidframework/core-utils");
const deltaUtil_1 = require("./deltaUtil");
/**
 * 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.
 * The detach pass is therefore responsible for making sure that all roots that needs to be attached during the
 * attach pass are detached.
 * In practice, this means the detach pass must:
 * - Create all subtrees that need to be created
 * - Detach all moved nodes
 *
 * In addition to that, the detach pass also detaches nodes that need removing, with the exception of nodes that get
 * replaced. The reason for this exception is that we need to be able to communicate replaces as atomic operations.
 * In order to do that, we need to wait until we are sure that the content to attach is available as a detached root.
 * Replaces are therefore handled during the attach pass.
 * Note that this could theoretically lead to a situation where, in the attach pass, one replace wants to attach
 * a node that has yet to be detached by another replace. This does not occur in practice because we do not support
 * editing operations that would lead to this situation.
 *
 * 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 detach field. That is the responsibility of the root transfers phase.
 *
 * The attach phase carries out attaches and replaces.
 *
 * 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.
 */
/**
 * 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.
 */
function visitDelta(delta, visitor, detachedFieldIndex) {
    const detachPassRoots = new Map();
    const attachPassRoots = new Map();
    const rootTransfers = [];
    const rootDestructions = [];
    const detachConfig = {
        func: detachPass,
        detachedFieldIndex,
        detachPassRoots,
        attachPassRoots,
        rootTransfers,
        rootDestructions,
    };
    processBuilds(delta.build, detachConfig, visitor);
    visitFieldMarks(delta.fields, visitor, detachConfig);
    fixedPointVisitOfRoots(visitor, detachPassRoots, detachConfig);
    transferRoots(rootTransfers, attachPassRoots, detachedFieldIndex, visitor);
    const attachConfig = {
        func: attachPass,
        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_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.
 * 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) {
    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_1.areDetachedNodeIdsEqual)(oldId, newId)) {
            for (let i = 0; i < count; i += 1) {
                atomized.push({ oldId: (0, deltaUtil_1.offsetDetachId)(oldId, i), newId: (0, deltaUtil_1.offsetDetachId)(newId, i) });
            }
        }
        return atomized;
    });
    while (nextBatch.length > 0) {
        const delayed = [];
        const priorSize = nextBatch.length;
        for (const { oldId, newId } of nextBatch) {
            const oldRootId = detachedFieldIndex.tryGetEntry(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);
            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);
            visitor.exitField(oldField);
            detachedFieldIndex.deleteEntry(oldId);
        }
        (0, core_utils_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:
 * - Performs all root creations
 * - Collects all roots that may need a detach pass
 * - Collects all roots that may need an attach pass
 * - Collects all relocates
 * - Collects all destructions
 * - Executes detaches (bottom-up) provided they are not part of a replace
 * (because we want to wait until we are sure content to attach is available as a root)
 */
function detachPass(delta, visitor, config) {
    processBuilds(delta.build, config, visitor);
    collectDestroys(delta.destroy, config);
    if (delta.global !== undefined) {
        for (const { id, fields } of delta.global) {
            const root = config.detachedFieldIndex.getEntry(id);
            config.detachPassRoots.set(root, fields);
            config.attachPassRoots.set(root, fields);
        }
    }
    if (delta.rename !== undefined) {
        config.rootTransfers.push(...delta.rename);
    }
    if (delta.local !== undefined) {
        let index = 0;
        for (const mark of delta.local) {
            if (mark.fields !== undefined) {
                (0, core_utils_1.assert)(mark.attach === undefined || mark.detach !== undefined, 0x7d0 /* Invalid nested changes on an additive mark */);
                visitNode(index, mark.fields, visitor, config);
            }
            if ((0, deltaUtil_1.isDetachMark)(mark)) {
                for (let i = 0; i < mark.count; i += 1) {
                    const root = config.detachedFieldIndex.createEntry(
                    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                    (0, deltaUtil_1.offsetDetachId)(mark.detach, i));
                    if (mark.fields !== undefined) {
                        config.attachPassRoots.set(root, mark.fields);
                    }
                    const field = config.detachedFieldIndex.toFieldKey(root);
                    visitor.detach({ start: index, end: index + 1 }, field);
                }
            }
            else if (!(0, deltaUtil_1.isAttachMark)(mark)) {
                index += mark.count;
            }
        }
    }
}
function processBuilds(builds, config, visitor) {
    if (builds !== undefined) {
        for (const { id, trees } of builds) {
            for (let i = 0; i < trees.length; i += 1) {
                const offsettedId = (0, deltaUtil_1.offsetDetachId)(id, i);
                let root = config.detachedFieldIndex.tryGetEntry(offsettedId);
                // Tree building is idempotent. We can therefore ignore build instructions for trees that already exist.
                // The idempotence is leveraged by undo/redo as well as sandwich rebasing.
                if (root === undefined) {
                    root = config.detachedFieldIndex.createEntry(offsettedId);
                    const field = config.detachedFieldIndex.toFieldKey(root);
                    visitor.create([trees[i]], field);
                }
            }
        }
    }
}
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
 * - Executes replaces (top-down) applying nested changes on the attached nodes
 * - Collects detached roots (from replaces) that need an attach pass
 */
function attachPass(delta, visitor, config) {
    if (delta.local !== undefined) {
        let index = 0;
        for (const mark of delta.local) {
            if ((0, deltaUtil_1.isAttachMark)(mark) || (0, deltaUtil_1.isReplaceMark)(mark)) {
                for (let i = 0; i < mark.count; i += 1) {
                    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                    const offsetAttachId = (0, deltaUtil_1.offsetDetachId)(mark.attach, i);
                    const sourceRoot = config.detachedFieldIndex.getEntry(offsetAttachId);
                    const sourceField = config.detachedFieldIndex.toFieldKey(sourceRoot);
                    if ((0, deltaUtil_1.isReplaceMark)(mark)) {
                        const rootDestination = config.detachedFieldIndex.createEntry(
                        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                        (0, deltaUtil_1.offsetDetachId)(mark.detach, i));
                        const destinationField = config.detachedFieldIndex.toFieldKey(rootDestination);
                        visitor.replace(sourceField, { start: index, end: index + 1 }, destinationField);
                        // We may need to do a second pass on the detached nodes
                        if (mark.fields !== undefined) {
                            config.attachPassRoots.set(rootDestination, mark.fields);
                        }
                    }
                    else {
                        // This a simple attach
                        visitor.attach(sourceField, 1, index + i);
                    }
                    config.detachedFieldIndex.deleteEntry(offsetAttachId);
                    const fields = config.attachPassRoots.get(sourceRoot);
                    if (fields !== undefined) {
                        config.attachPassRoots.delete(sourceRoot);
                        visitNode(index, fields, visitor, config);
                    }
                }
            }
            else if (!(0, deltaUtil_1.isDetachMark)(mark) && mark.fields !== undefined) {
                visitNode(index, mark.fields, visitor, config);
            }
            if (!(0, deltaUtil_1.isDetachMark)(mark)) {
                index += mark.count;
            }
        }
    }
}
//# sourceMappingURL=visitDelta.js.map