/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/core-utils";
import { ITreeSubscriptionCursorState, AnchorSet, getMapTreeField, aboveRootPlaceholder, } from "../../core/index.js";
import { brand, fail, assertValidIndex, assertValidRange, assertNonNegativeSafeInteger, } from "../../util/index.js";
import { SynchronousCursor } from "../treeCursorUtils.js";
import { mapTreeFromCursor, cursorForMapTreeNode } from "../mapTreeCursor.js";
import { createEmitter } from "../../events/index.js";
function makeRoot() {
    return {
        type: aboveRootPlaceholder,
        fields: new Map(),
    };
}
/**
 * Reference implementation of IEditableForest.
 *
 * This implementation focuses on correctness and simplicity, not performance.
 * It does not use compressed chunks: instead nodes are implemented using objects.
 */
export class ObjectForest {
    constructor(anchors = new AnchorSet()) {
        this.anchors = anchors;
        this.roots = makeRoot();
        // All cursors that are in the "Current" state. Must be empty when editing.
        this.currentCursors = new Set();
        this.events = createEmitter();
        this.nextRange = 0;
    }
    get isEmpty() {
        return this.roots.fields.size === 0;
    }
    on(eventName, listener) {
        return this.events.on(eventName, listener);
    }
    clone(_, anchors) {
        const forest = new ObjectForest(anchors);
        // Deep copy the trees.
        for (const [key, value] of this.roots.fields) {
            // TODO: this references the existing TreeValues instead of copying them:
            // they are assumed to be copy on write. See TODO on NodeData.
            forest.roots.fields.set(key, value.map((v) => mapTreeFromCursor(cursorForMapTreeNode(v))));
        }
        return forest;
    }
    forgetAnchor(anchor) {
        this.anchors.forget(anchor);
    }
    acquireVisitor() {
        assert(this.activeVisitor === undefined, 0x76c /* Must release existing visitor before acquiring another */);
        this.events.emit("beforeChange");
        assert(this.currentCursors.size === 0, 0x374 /* No cursors can be current when modifying forest */);
        // Note: This code uses cursors, however it also modifies the tree.
        // In general this is not safe, but this code happens to only modify the tree below the current cursor location,
        // which happens to work.
        // This pattern could be generalized/formalized with a concept of an exclusive cursor,
        // which can edit, but is the only cursor allowed at the time.
        const cursor = this.allocateCursor();
        cursor.setToAboveDetachedSequences();
        const visitor = {
            forest: this,
            cursor,
            free() {
                this.cursor.free();
                assert(this.forest.activeVisitor !== undefined, 0x76d /* Multiple free calls for same visitor */);
                this.forest.activeVisitor = undefined;
                this.forest.events.emit("afterChange");
            },
            destroy(detachedField, count) {
                this.forest.delete(detachedField);
            },
            create(content, destination) {
                this.forest.add(content, destination);
            },
            attach(source, count, destination) {
                this.attachEdit(source, count, destination);
            },
            detach(source, destination) {
                this.detachEdit(source, destination);
            },
            /**
             * Attaches the nodes from the given source field into the current field.
             * Does not invalidate dependents.
             * @param source - The the range to be attached.
             * @param count - The number of nodes being attached.
             * Expected to match the number of nodes in the source detached field.
             * @param destination - The index in the current field at which to attach the content.
             */
            attachEdit(source, count, destination) {
                assertNonNegativeSafeInteger(count);
                if (count === 0) {
                    return;
                }
                const [parent, key] = cursor.getParent();
                assert(parent !== this.forest.roots || key !== source, 0x7b6 /* Attach source field must be different from current field */);
                const currentField = getMapTreeField(parent, key, true);
                assertValidIndex(destination, currentField, true);
                const sourceField = getMapTreeField(this.forest.roots, source, false);
                assert(sourceField !== undefined, 0x7b7 /* Attach source field must exist */);
                assert(sourceField.length === count, 0x7b8 /* Attach must consume all nodes in source field */);
                // TODO: this will fail for very large insertions due to argument limits.
                currentField.splice(destination, 0, ...sourceField);
                this.forest.delete(source);
            },
            /**
             * 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, key] = cursor.getParent();
                assert(destination === undefined ||
                    parent !== this.forest.roots ||
                    key !== destination, 0x7b9 /* Detach destination field must be different from current field */);
                const currentField = getMapTreeField(parent, key, true);
                assertValidRange(source, currentField);
                const content = currentField.splice(source.start, source.end - source.start);
                if (destination !== undefined) {
                    this.forest.addFieldAsDetached(content, destination);
                }
                // 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 (currentField.length === 0) {
                    parent.fields.delete(key);
                }
            },
            replace(newContentSource, range, oldContentDestination) {
                assert(newContentSource !== oldContentDestination, 0x7ba /* Replace detached source field and detached destination field must be different */);
                this.detachEdit(range, oldContentDestination);
                this.attachEdit(newContentSource, range.end - range.start, range.start);
            },
            enterNode(index) {
                this.cursor.enterNode(index);
            },
            exitNode(index) {
                this.cursor.exitNode();
            },
            enterField(key) {
                this.cursor.enterField(key);
            },
            exitField(key) {
                this.cursor.exitField();
            },
        };
        this.activeVisitor = visitor;
        return visitor;
    }
    newDetachedField() {
        const range = brand(String(this.nextRange));
        this.nextRange += 1;
        return range;
    }
    add(nodes, key) {
        const field = Array.from(nodes, mapTreeFromCursor);
        this.addFieldAsDetached(field, key);
    }
    addFieldAsDetached(field, key) {
        assert(!this.roots.fields.has(key), 0x370 /* new range must not already exist */);
        if (field.length > 0) {
            this.roots.fields.set(key, field);
        }
    }
    delete(field) {
        this.roots.fields.delete(field);
    }
    allocateCursor() {
        return new Cursor(this);
    }
    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) {
        if (destination.parent === undefined) {
            this.moveCursorToPath(undefined, cursorToMove);
        }
        else {
            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) {
        assert(cursorToMove instanceof Cursor, 0x337 /* ObjectForest must only be given its own Cursor type */);
        assert(cursorToMove.forest === this, 0x338 /* ObjectForest 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.setToAboveDetachedSequences();
        while (keyStack.length > 0) {
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            cursorToMove.enterField(keyStack.pop());
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            cursorToMove.enterNode(indexStack.pop());
        }
        return;
    }
    getCursorAboveDetachedFields() {
        return cursorForMapTreeNode(this.roots);
    }
}
/**
 * Cursor implementation for ObjectForest.
 */
class Cursor extends SynchronousCursor {
    /**
     * @param forest - forest this cursor navigates
     * @param innerCursor - underlying cursor implementation this wraps. `undefined` when state is not `Current`
     */
    constructor(forest, innerCursor) {
        super();
        this.forest = forest;
        this.innerCursor = innerCursor;
        if (innerCursor === undefined) {
            this.state = ITreeSubscriptionCursorState.Cleared;
        }
        else {
            this.state = ITreeSubscriptionCursorState.Current;
            this.forest.currentCursors.add(this);
        }
    }
    buildFieldAnchor() {
        const path = this.getFieldPath();
        const anchor = path.parent === undefined ? undefined : this.forest.anchors.track(path.parent);
        return { parent: anchor, fieldKey: path.field };
    }
    getFieldPath(prefix) {
        assert(this.innerCursor !== undefined, 0x45f /* Cursor must be current to be used */);
        return this.innerCursor.getFieldPath(prefix);
    }
    get mode() {
        assert(this.innerCursor !== undefined, 0x42e /* Cursor must be current to be used */);
        return this.innerCursor.mode;
    }
    nextField() {
        assert(this.innerCursor !== undefined, 0x42f /* Cursor must be current to be used */);
        return this.innerCursor.nextField();
    }
    exitField() {
        assert(this.innerCursor !== undefined, 0x430 /* Cursor must be current to be used */);
        return this.innerCursor.exitField();
    }
    skipPendingFields() {
        assert(this.innerCursor !== undefined, 0x431 /* Cursor must be current to be used */);
        return this.innerCursor.skipPendingFields();
    }
    getFieldKey() {
        assert(this.innerCursor !== undefined, 0x432 /* Cursor must be current to be used */);
        return this.innerCursor.getFieldKey();
    }
    getFieldLength() {
        assert(this.innerCursor !== undefined, 0x433 /* Cursor must be current to be used */);
        return this.innerCursor.getFieldLength();
    }
    firstNode() {
        assert(this.innerCursor !== undefined, 0x434 /* Cursor must be current to be used */);
        return this.innerCursor.firstNode();
    }
    enterNode(childIndex) {
        assert(this.innerCursor !== undefined, 0x435 /* Cursor must be current to be used */);
        return this.innerCursor.enterNode(childIndex);
    }
    getPath(prefix) {
        assert(this.innerCursor !== undefined, 0x436 /* Cursor must be current to be used */);
        return this.innerCursor.getPath(prefix) ?? fail("no path when at root");
    }
    get fieldIndex() {
        assert(this.innerCursor !== undefined, 0x437 /* Cursor must be current to be used */);
        return this.innerCursor.fieldIndex;
    }
    get chunkStart() {
        assert(this.innerCursor !== undefined, 0x438 /* Cursor must be current to be used */);
        return this.innerCursor.chunkStart;
    }
    get chunkLength() {
        assert(this.innerCursor !== undefined, 0x439 /* Cursor must be current to be used */);
        return this.innerCursor.chunkLength;
    }
    seekNodes(offset) {
        assert(this.innerCursor !== undefined, 0x43a /* Cursor must be current to be used */);
        return this.innerCursor.seekNodes(offset);
    }
    nextNode() {
        assert(this.innerCursor !== undefined, 0x43b /* Cursor must be current to be used */);
        return this.innerCursor.nextNode();
    }
    exitNode() {
        assert(this.innerCursor !== undefined, 0x43c /* Cursor must be current to be used */);
        return this.innerCursor.exitNode();
    }
    firstField() {
        assert(this.innerCursor !== undefined, 0x43d /* Cursor must be current to be used */);
        return this.innerCursor.firstField();
    }
    enterField(key) {
        assert(this.innerCursor !== undefined, 0x43e /* Cursor must be current to be used */);
        return this.innerCursor.enterField(key);
    }
    get type() {
        assert(this.innerCursor !== undefined, 0x43f /* Cursor must be current to be used */);
        return this.innerCursor.type;
    }
    get value() {
        assert(this.innerCursor !== undefined, 0x440 /* Cursor must be current to be used */);
        return this.innerCursor.value;
    }
    // TODO: tests for clear when not at root.
    clear() {
        assert(this.state !== ITreeSubscriptionCursorState.Freed, 0x33b /* Cursor must not be freed */);
        this.state = ITreeSubscriptionCursorState.Cleared;
        this.innerCursor = undefined;
        this.forest.currentCursors.delete(this);
    }
    /**
     * Move this cursor to the special dummy node above the detached sequences.
     * Can be used when cleared (but not freed).
     */
    setToAboveDetachedSequences() {
        assert(this.state !== ITreeSubscriptionCursorState.Freed, 0x33c /* Cursor must not be freed */);
        this.clear();
        this.state = ITreeSubscriptionCursorState.Current;
        this.innerCursor = cursorForMapTreeNode(this.forest.roots);
        this.forest.currentCursors.add(this);
    }
    getNode() {
        assert(this.innerCursor !== undefined, 0x33e /* Cursor must be current to be used */);
        return this.innerCursor.getNode();
    }
    getParent() {
        assert(this.innerCursor !== undefined, 0x441 /* Cursor must be current to be used */);
        // This could be optimized to skip moving it accessing internals of cursor.
        const key = this.innerCursor.getFieldKey();
        this.innerCursor.exitField();
        const node = this.innerCursor.getNode();
        this.innerCursor.enterField(key);
        return [node, key];
    }
    fork() {
        assert(this.innerCursor !== undefined, 0x460 /* Cursor must be current to be used */);
        return new Cursor(this.forest, this.innerCursor.fork());
    }
    free() {
        assert(this.state !== ITreeSubscriptionCursorState.Freed, 0x33f /* Cursor must not be double freed */);
        this.forest.currentCursors.delete(this);
        this.state = ITreeSubscriptionCursorState.Freed;
    }
    buildAnchor() {
        assert(this.state === ITreeSubscriptionCursorState.Current, 0x37a /* Cursor must be current to be used */);
        return this.forest.anchors.track(this.getPath());
    }
}
// This function is the folder level export for objectForest, and hides all the implementation types.
// When other forest implementations are created (ex: optimized ones),
// this function should likely be moved and updated to (at least conditionally) use them.
/**
 * @returns an implementation of {@link IEditableForest} with no data or schema.
 */
export function buildForest(anchors) {
    return new ObjectForest(anchors);
}
//# sourceMappingURL=objectForest.js.map