"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 common_utils_1 = require("@fluidframework/common-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((0, util_1.brand)("above root placeholder"), 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;
        // Invalidate forest if schema change.
        (0, core_1.recordDependency)(this.dependent, this.schema);
    }
    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);
    }
    applyDelta(delta) {
        this.events.emit("beforeDelta", delta);
        this.invalidateDependents();
        const moves = new Map();
        if (this.roots.isShared()) {
            this.roots = this.roots.clone();
        }
        // 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.
        const mutableChunkStack = [];
        let mutableChunk = this.roots;
        const getParent = () => {
            (0, common_utils_1.assert)(mutableChunkStack.length > 0, 0x532 /* invalid access to root's parent */);
            return mutableChunkStack[mutableChunkStack.length - 1];
        };
        const moveIn = (index, toAttach) => {
            const detachedKey = (0, core_1.detachedFieldAsKey)(toAttach);
            const children = this.roots.fields.get(detachedKey) ?? [];
            this.roots.fields.delete(detachedKey);
            if (children.length === 0) {
                return 0; // Prevent creating 0 sized fields when inserting empty into empty.
            }
            const parent = 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(index, 0, ...children);
            return children.length;
        };
        const visitor = {
            onDelete: (index, count) => {
                visitor.onMoveOut(index, count);
            },
            onInsert: (index, content) => {
                const chunks = content.map((c) => (0, chunkTree_1.chunkTree)(c, this.chunker));
                const field = this.newDetachedField();
                this.roots.fields.set((0, core_1.detachedFieldAsKey)(field), chunks);
                moveIn(index, field);
            },
            onMoveOut: (index, count, id) => {
                const parent = getParent();
                const sourceField = parent.mutableChunk.fields.get(parent.key) ?? [];
                const newField = sourceField.splice(index, count);
                if (id !== undefined) {
                    const detached = this.newDetachedField();
                    const key = (0, core_1.detachedFieldAsKey)(detached);
                    if (newField.length > 0) {
                        this.roots.fields.set(key, newField);
                    }
                    moves.set(id, detached);
                }
                else {
                    for (const child of newField) {
                        child.referenceRemoved();
                    }
                }
                if (sourceField.length === 0) {
                    parent.mutableChunk.fields.delete(parent.key);
                }
            },
            onMoveIn: (index, count, id) => {
                const toAttach = moves.get(id) ?? (0, util_1.fail)("move in without move out");
                moves.delete(id);
                const countMoved = moveIn(index, toAttach);
                (0, common_utils_1.assert)(countMoved === count, 0x533 /* counts must match */);
            },
            enterNode: (index) => {
                (0, common_utils_1.assert)(mutableChunk === undefined, 0x535 /* should be in field */);
                const parent = 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.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, common_utils_1.assert)(found instanceof basicChunk_1.BasicChunk, 0x536 /* chunk should have been normalized */);
                if (found.isShared()) {
                    mutableChunk = chunks[indexOfChunk] = found.clone();
                    found.referenceRemoved();
                }
                else {
                    mutableChunk = found;
                }
            },
            exitNode: (index) => {
                (0, common_utils_1.assert)(mutableChunk !== undefined, 0x537 /* should be in node */);
                mutableChunk = undefined;
            },
            enterField: (key) => {
                (0, common_utils_1.assert)(mutableChunk !== undefined, 0x538 /* should be in node */);
                mutableChunkStack.push({ key, mutableChunk });
                mutableChunk = undefined;
            },
            exitField: (key) => {
                const top = mutableChunkStack.pop() ?? (0, util_1.fail)("should not be at root");
                (0, common_utils_1.assert)(mutableChunk === undefined, 0x539 /* should be in field */);
                mutableChunk = top.mutableChunk;
            },
        };
        (0, core_1.visitDelta)(delta, visitor);
        this.events.emit("afterDelta", delta);
    }
    newDetachedField() {
        const field = (0, util_1.brand)(String(this.nextDetachedFieldIdentifier));
        (0, common_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 /* NotFound */;
        }
        this.moveCursorToPath(path, cursorToMove);
        return 1 /* Ok */;
    }
    tryMoveCursorToField(destination, cursorToMove) {
        (0, common_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 /* Ok */;
        }
        const result = this.tryMoveCursorToNode(destination.parent, cursorToMove);
        if (result !== 1 /* Ok */) {
            return result;
        }
        cursorToMove.enterField(destination.fieldKey);
        return 1 /* Ok */;
    }
    /**
     * Set `cursorToMove` to location described by path.
     * This is NOT a relative move: current position is discarded.
     * Path must point to existing node.
     */
    moveCursorToPath(destination, cursorToMove) {
        (0, common_utils_1.assert)(cursorToMove instanceof Cursor, 0x53c /* ChunkedForest must only be given its own Cursor type */);
        (0, common_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());
        }
    }
}
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