"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildChunkedForest = void 0;
const core_utils_1 = require("@fluidframework/core-utils");
const core_1 = require("../../core");
const util_1 = require("../../util");
const events_1 = require("../../events");
const basicChunk_1 = require("./basicChunk");
const chunkTree_1 = require("./chunkTree");
function makeRoot() {
    return new basicChunk_1.BasicChunk(core_1.aboveRootPlaceholder, new Map());
}
/**
 * Implementation of IEditableForest based on copy on write chunks.
 *
 * This implementation focuses on performance.
 */
class ChunkedForest extends core_1.SimpleDependee {
    /**
     * @param roots - dummy node above the root under which detached fields are stored. All content of the forest is reachable from this.
     * @param schema - schema which all content in this forest is assumed to comply with.
     * @param chunker - Chunking policy. TODO: dispose of this when forest is disposed.
     * @param anchors - anchorSet used to track location in this forest across changes. Callers of applyDelta must ensure this is updated accordingly.
     */
    constructor(roots, schema, chunker, anchors = new core_1.AnchorSet()) {
        super("object-forest.ChunkedForest");
        this.roots = roots;
        this.schema = schema;
        this.chunker = chunker;
        this.anchors = anchors;
        this.dependent = new core_1.SimpleObservingDependent(() => this.invalidateDependents());
        this.events = (0, events_1.createEmitter)();
        this.nextDetachedFieldIdentifier = 0;
    }
    get isEmpty() {
        return this.roots.fields.size === 0;
    }
    on(eventName, listener) {
        return this.events.on(eventName, listener);
    }
    clone(schema, anchors) {
        this.roots.referenceAdded();
        return new ChunkedForest(this.roots, schema, this.chunker.clone(schema), anchors);
    }
    forgetAnchor(anchor) {
        this.anchors.forget(anchor);
    }
    acquireVisitor() {
        (0, core_utils_1.assert)(this.activeVisitor === undefined, 0x76a /* Must release existing visitor before acquiring another */);
        this.events.emit("beforeChange");
        if (this.roots.isShared()) {
            this.roots = this.roots.clone();
        }
        const visitor = {
            forest: this,
            // Current location in the tree, as a non-shared BasicChunk (TODO: support in-place modification of other chunk formats when possible).
            // Start above root detached sequences.
            mutableChunkStack: [],
            mutableChunk: this.roots,
            getParent() {
                (0, core_utils_1.assert)(this.mutableChunkStack.length > 0, 0x532 /* invalid access to root's parent */);
                return this.mutableChunkStack[this.mutableChunkStack.length - 1];
            },
            free() {
                this.mutableChunk = undefined;
                this.mutableChunkStack.length = 0;
                (0, core_utils_1.assert)(this.forest.activeVisitor !== undefined, 0x76b /* Multiple free calls for same visitor */);
                this.forest.activeVisitor = undefined;
                this.forest.events.emit("afterChange");
            },
            destroy(detachedField, count) {
                this.forest.invalidateDependents();
                this.forest.roots.fields.delete(detachedField);
            },
            create(content, destination) {
                this.forest.invalidateDependents();
                const chunks = content.map((c) => (0, chunkTree_1.chunkTree)(c, this.forest.chunker));
                this.forest.roots.fields.set(destination, chunks);
            },
            attach(source, count, destination) {
                this.forest.invalidateDependents();
                this.attachEdit(source, count, destination);
            },
            detach(source, destination) {
                this.forest.invalidateDependents();
                this.detachEdit(source, destination);
            },
            /**
             * Attaches the range into the current field by transferring it from the given source path.
             * Does not invalidate dependents.
             * @param source - The the range to be attached.
             * @param destination - The index in the current field at which to attach the content.
             */
            attachEdit(source, count, destination) {
                const sourceField = this.forest.roots.fields.get(source) ?? [];
                this.forest.roots.fields.delete(source);
                if (sourceField.length === 0) {
                    return; // Prevent creating 0 sized fields when inserting empty into empty.
                }
                const parent = this.getParent();
                const destinationField = (0, util_1.getOrAddEmptyToMap)(parent.mutableChunk.fields, parent.key);
                // TODO: this will fail for very large moves due to argument limits.
                destinationField.splice(destination, 0, ...sourceField);
            },
            /**
             * Detaches the range from the current field and transfers it to the given destination if any.
             * Does not invalidate dependents.
             * @param source - The bounds of the range to be detached from the current field.
             * @param destination - If specified, the destination to transfer the detached range to.
             * If not specified, the detached range is destroyed.
             */
            detachEdit(source, destination) {
                const parent = this.getParent();
                const sourceField = parent.mutableChunk.fields.get(parent.key) ?? [];
                (0, util_1.assertValidRange)(source, sourceField);
                const newField = sourceField.splice(source.start, source.end - source.start);
                if (destination !== undefined) {
                    (0, core_utils_1.assert)(!this.forest.roots.fields.has(destination), 0x7af /* Destination must be a new empty detached field */);
                    if (newField.length > 0) {
                        this.forest.roots.fields.set(destination, newField);
                    }
                }
                else {
                    for (const child of newField) {
                        child.referenceRemoved();
                    }
                }
                // This check is performed after the transfer to ensure that the field is not removed in scenarios
                // where the source and destination are the same.
                if (sourceField.length === 0) {
                    parent.mutableChunk.fields.delete(parent.key);
                }
            },
            replace(newContentSource, range, oldContentDestination) {
                (0, core_utils_1.assert)(newContentSource !== oldContentDestination, 0x7b0 /* Replace detached source field and detached destination field must be different */);
                this.forest.invalidateDependents();
                this.detachEdit(range, oldContentDestination);
                this.attachEdit(newContentSource, range.end - range.start, range.start);
            },
            enterNode(index) {
                (0, core_utils_1.assert)(this.mutableChunk === undefined, 0x535 /* should be in field */);
                const parent = this.getParent();
                const chunks = parent.mutableChunk.fields.get(parent.key) ?? (0, util_1.fail)("missing edited field");
                let indexWithinChunk = index;
                let indexOfChunk = 0;
                while (indexWithinChunk >= chunks[indexOfChunk].topLevelLength) {
                    indexWithinChunk -= chunks[indexOfChunk].topLevelLength;
                    indexOfChunk++;
                    if (indexOfChunk === chunks.length) {
                        (0, util_1.fail)("missing edited node");
                    }
                }
                let found = chunks[indexOfChunk];
                if (!(found instanceof basicChunk_1.BasicChunk)) {
                    // TODO:Perf: support in place editing of other chunk formats when possible:
                    // 1. Support updating values in uniform chunks.
                    // 2. Support traversing sequence chunks.
                    //
                    // Maybe build path when visitor navigates then lazily sync to chunk tree when editing?
                    const newChunks = (0, core_1.mapCursorField)(found.cursor(), (cursor) => (0, chunkTree_1.basicChunkTree)(cursor, this.forest.chunker));
                    // TODO: this could fail for really long chunks being split (due to argument count limits).
                    // Current implementations of chunks shouldn't ever be that long, but it could be an issue if they get bigger.
                    chunks.splice(indexOfChunk, 1, ...newChunks);
                    found.referenceRemoved();
                    found = newChunks[indexWithinChunk];
                }
                (0, core_utils_1.assert)(found instanceof basicChunk_1.BasicChunk, 0x536 /* chunk should have been normalized */);
                if (found.isShared()) {
                    this.mutableChunk = chunks[indexOfChunk] = found.clone();
                    found.referenceRemoved();
                }
                else {
                    this.mutableChunk = found;
                }
            },
            exitNode(index) {
                (0, core_utils_1.assert)(this.mutableChunk !== undefined, 0x537 /* should be in node */);
                this.mutableChunk = undefined;
            },
            enterField(key) {
                (0, core_utils_1.assert)(this.mutableChunk !== undefined, 0x538 /* should be in node */);
                this.mutableChunkStack.push({ key, mutableChunk: this.mutableChunk });
                this.mutableChunk = undefined;
            },
            exitField(key) {
                const top = this.mutableChunkStack.pop() ?? (0, util_1.fail)("should not be at root");
                (0, core_utils_1.assert)(this.mutableChunk === undefined, 0x539 /* should be in field */);
                this.mutableChunk = top.mutableChunk;
            },
        };
        this.activeVisitor = visitor;
        return visitor;
    }
    newDetachedField() {
        const field = (0, util_1.brand)(String(this.nextDetachedFieldIdentifier));
        (0, core_utils_1.assert)(!this.roots.fields.has((0, core_1.detachedFieldAsKey)(field)), 0x53a /* new field must not already exist */);
        this.nextDetachedFieldIdentifier += 1;
        return field;
    }
    allocateCursor() {
        return new Cursor(this, core_1.ITreeSubscriptionCursorState.Cleared, [], [], [], [], [], [], 0, 0, 0, undefined);
    }
    tryMoveCursorToNode(destination, cursorToMove) {
        const path = this.anchors.locate(destination);
        if (path === undefined) {
            return -1 /* TreeNavigationResult.NotFound */;
        }
        this.moveCursorToPath(path, cursorToMove);
        return 1 /* TreeNavigationResult.Ok */;
    }
    tryMoveCursorToField(destination, cursorToMove) {
        (0, core_utils_1.assert)(cursorToMove instanceof Cursor, 0x53b /* ChunkedForest must only be given its own Cursor type */);
        if (destination.parent === undefined) {
            cursorToMove.setToDetachedSequence(destination.fieldKey);
            return 1 /* TreeNavigationResult.Ok */;
        }
        const result = this.tryMoveCursorToNode(destination.parent, cursorToMove);
        if (result !== 1 /* TreeNavigationResult.Ok */) {
            return result;
        }
        cursorToMove.enterField(destination.fieldKey);
        return 1 /* TreeNavigationResult.Ok */;
    }
    moveCursorToPath(destination, cursorToMove) {
        (0, core_utils_1.assert)(cursorToMove instanceof Cursor, 0x53c /* ChunkedForest must only be given its own Cursor type */);
        (0, core_utils_1.assert)(cursorToMove.forest === this, 0x53d /* ChunkedForest must only be given its own Cursor */);
        const indexStack = [];
        const keyStack = [];
        let path = destination;
        while (path !== undefined) {
            indexStack.push(path.parentIndex);
            keyStack.push(path.parentField);
            path = path.parent;
        }
        cursorToMove.clear();
        while (keyStack.length > 0) {
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            const key = keyStack.pop();
            if (cursorToMove.state === core_1.ITreeSubscriptionCursorState.Cleared) {
                cursorToMove.setToDetachedSequence(key);
                cursorToMove.state = core_1.ITreeSubscriptionCursorState.Current;
            }
            else {
                cursorToMove.enterField(key);
            }
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            cursorToMove.enterNode(indexStack.pop());
        }
    }
    getCursorAboveDetachedFields() {
        const rootCursor = this.roots.cursor();
        rootCursor.enterNode(0);
        return rootCursor;
    }
}
class Cursor extends basicChunk_1.BasicChunkCursor {
    constructor(forest, state, root, siblingStack, indexStack, indexOfChunkStack, indexWithinChunkStack, siblings, index, indexOfChunk, indexWithinChunk, nestedCursor) {
        super(root, siblingStack, indexStack, indexOfChunkStack, indexWithinChunkStack, siblings, index, indexOfChunk, indexWithinChunk, nestedCursor);
        this.forest = forest;
        this.state = state;
    }
    setToDetachedSequence(key) {
        this.root = (this.forest.roots.fields.get(key) ?? []);
        this.siblingStack.length = 0;
        this.indexStack.length = 0;
        this.indexOfChunkStack.length = 0;
        this.indexWithinChunkStack.length = 0;
        this.siblings = [key];
        this.index = 0;
        this.indexOfChunk = 0;
        this.indexWithinChunk = 0;
        this.nestedCursor = undefined;
    }
    fork() {
        // Siblings arrays are not modified during navigation and do not need be be copied.
        // This allows this copy to be shallow, and `this.siblings` below to not be copied as all.
        return new Cursor(this.forest, this.state, this.root, [...this.siblingStack], [...this.indexStack], [...this.indexOfChunkStack], [...this.indexWithinChunkStack], this.siblings, this.index, this.indexOfChunk, this.indexWithinChunk, this.nestedCursor?.fork());
    }
    buildFieldAnchor() {
        const path = this.getFieldPath();
        const anchor = path.parent === undefined ? undefined : this.forest.anchors.track(path.parent);
        return { parent: anchor, fieldKey: path.field };
    }
    free() {
        this.state = core_1.ITreeSubscriptionCursorState.Freed;
    }
    buildAnchor() {
        return this.forest.anchors.track(this.getPath());
    }
    clear() {
        this.state = core_1.ITreeSubscriptionCursorState.Cleared;
        this.setToDetachedSequence(core_1.rootFieldKey);
    }
}
/**
 * @returns an implementation of {@link IEditableForest} with no data or schema.
 */
function buildChunkedForest(chunker, anchors) {
    return new ChunkedForest(makeRoot(), chunker.schema, chunker, anchors);
}
exports.buildChunkedForest = buildChunkedForest;
//# sourceMappingURL=chunkedForest.js.map