"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildForest = void 0;
const common_utils_1 = require("@fluidframework/common-utils");
const core_1 = require("../../core");
const util_1 = require("../../util");
const treeCursorUtils_1 = require("../treeCursorUtils");
const mapTreeCursor_1 = require("../mapTreeCursor");
const events_1 = require("../../events");
function makeRoot() {
    return {
        type: (0, util_1.brand)("above root placeholder"),
        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.
 */
class ObjectForest extends core_1.SimpleDependee {
    constructor(schema, anchors = new core_1.AnchorSet()) {
        super("object-forest.ObjectForest");
        this.schema = schema;
        this.anchors = anchors;
        this.dependent = new core_1.SimpleObservingDependent(() => this.invalidateDependents());
        this.roots = makeRoot();
        // All cursors that are in the "Current" state. Must be empty when editing.
        this.currentCursors = new Set();
        this.events = (0, events_1.createEmitter)();
        this.nextRange = 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) {
        const forest = new ObjectForest(schema, 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) => (0, mapTreeCursor_1.mapTreeFromCursor)((0, mapTreeCursor_1.singleMapTreeCursor)(v))));
        }
        return forest;
    }
    forgetAnchor(anchor) {
        this.anchors.forget(anchor);
    }
    applyDelta(delta) {
        this.events.emit("beforeDelta", delta);
        this.invalidateDependents();
        (0, common_utils_1.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 moves = new Map();
        const cursor = this.allocateCursor();
        cursor.setToAboveDetachedSequences();
        const moveIn = (index, toAttach) => {
            const detachedKey = (0, core_1.detachedFieldAsKey)(toAttach);
            const children = (0, core_1.getMapTreeField)(this.roots, detachedKey, false);
            this.roots.fields.delete(detachedKey);
            if (children.length === 0) {
                return 0; // Prevent creating 0 sized fields when inserting empty into empty.
            }
            const [parent, key] = cursor.getParent();
            const destinationField = (0, core_1.getMapTreeField)(parent, key, true);
            (0, util_1.assertValidIndex)(index, destinationField, true);
            // 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 range = this.add(content);
                moveIn(index, range);
            },
            onMoveOut: (index, count, id) => {
                const [parent, key] = cursor.getParent();
                const sourceField = (0, core_1.getMapTreeField)(parent, key, false);
                const startIndex = index;
                const endIndex = index + count;
                (0, util_1.assertValidIndex)(startIndex, sourceField, true);
                (0, util_1.assertValidIndex)(endIndex, sourceField, true);
                (0, common_utils_1.assert)(startIndex <= endIndex, 0x371 /* detached range's end must be after its start */);
                const newField = sourceField.splice(startIndex, endIndex - startIndex);
                const field = this.addFieldAsDetached(newField);
                if (id !== undefined) {
                    moves.set(id, field);
                }
                else {
                    this.delete(field);
                }
                if (sourceField.length === 0) {
                    parent.fields.delete(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, 0x369 /* counts must match */);
            },
            enterNode: (index) => cursor.enterNode(index),
            exitNode: (index) => cursor.exitNode(),
            enterField: (key) => cursor.enterField(key),
            exitField: (key) => cursor.exitField(),
        };
        (0, core_1.visitDelta)(delta, visitor);
        cursor.free();
        this.events.emit("afterDelta", delta);
    }
    newDetachedField() {
        const range = (0, util_1.brand)(String(this.nextRange));
        this.nextRange += 1;
        return range;
    }
    add(nodes) {
        const field = Array.from(nodes, mapTreeCursor_1.mapTreeFromCursor);
        return this.addFieldAsDetached(field);
    }
    addFieldAsDetached(field) {
        const detached = this.newDetachedField();
        const key = (0, core_1.detachedFieldAsKey)(detached);
        (0, common_utils_1.assert)(!this.roots.fields.has(key), 0x370 /* new range must not already exist */);
        if (field.length > 0) {
            this.roots.fields.set(key, field);
        }
        return detached;
    }
    delete(field) {
        this.roots.fields.delete((0, core_1.detachedFieldAsKey)(field));
    }
    allocateCursor() {
        return new Cursor(this);
    }
    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) {
        if (destination.parent === undefined) {
            this.moveCursorToPath(undefined, cursorToMove);
        }
        else {
            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, 0x337 /* ObjectForest must only be given its own Cursor type */);
        (0, common_utils_1.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;
    }
}
/**
 * Cursor implementation for ObjectForest.
 */
class Cursor extends treeCursorUtils_1.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 = core_1.ITreeSubscriptionCursorState.Cleared;
        }
        else {
            this.state = core_1.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) {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x45f /* Cursor must be current to be used */);
        return this.innerCursor.getFieldPath(prefix);
    }
    get mode() {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x42e /* Cursor must be current to be used */);
        return this.innerCursor.mode;
    }
    nextField() {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x42f /* Cursor must be current to be used */);
        return this.innerCursor.nextField();
    }
    exitField() {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x430 /* Cursor must be current to be used */);
        return this.innerCursor.exitField();
    }
    skipPendingFields() {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x431 /* Cursor must be current to be used */);
        return this.innerCursor.skipPendingFields();
    }
    getFieldKey() {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x432 /* Cursor must be current to be used */);
        return this.innerCursor.getFieldKey();
    }
    getFieldLength() {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x433 /* Cursor must be current to be used */);
        return this.innerCursor.getFieldLength();
    }
    firstNode() {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x434 /* Cursor must be current to be used */);
        return this.innerCursor.firstNode();
    }
    enterNode(childIndex) {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x435 /* Cursor must be current to be used */);
        return this.innerCursor.enterNode(childIndex);
    }
    getPath(prefix) {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x436 /* Cursor must be current to be used */);
        return this.innerCursor.getPath(prefix) ?? (0, util_1.fail)("no path when at root");
    }
    get fieldIndex() {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x437 /* Cursor must be current to be used */);
        return this.innerCursor.fieldIndex;
    }
    get chunkStart() {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x438 /* Cursor must be current to be used */);
        return this.innerCursor.chunkStart;
    }
    get chunkLength() {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x439 /* Cursor must be current to be used */);
        return this.innerCursor.chunkLength;
    }
    seekNodes(offset) {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x43a /* Cursor must be current to be used */);
        return this.innerCursor.seekNodes(offset);
    }
    nextNode() {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x43b /* Cursor must be current to be used */);
        return this.innerCursor.nextNode();
    }
    exitNode() {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x43c /* Cursor must be current to be used */);
        return this.innerCursor.exitNode();
    }
    firstField() {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x43d /* Cursor must be current to be used */);
        return this.innerCursor.firstField();
    }
    enterField(key) {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x43e /* Cursor must be current to be used */);
        return this.innerCursor.enterField(key);
    }
    get type() {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x43f /* Cursor must be current to be used */);
        return this.innerCursor.type;
    }
    get value() {
        (0, common_utils_1.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() {
        (0, common_utils_1.assert)(this.state !== core_1.ITreeSubscriptionCursorState.Freed, 0x33b /* Cursor must not be freed */);
        this.state = core_1.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() {
        (0, common_utils_1.assert)(this.state !== core_1.ITreeSubscriptionCursorState.Freed, 0x33c /* Cursor must not be freed */);
        this.clear();
        this.state = core_1.ITreeSubscriptionCursorState.Current;
        this.innerCursor = (0, mapTreeCursor_1.singleMapTreeCursor)(this.forest.roots);
        this.forest.currentCursors.add(this);
    }
    getNode() {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x33e /* Cursor must be current to be used */);
        return this.innerCursor.getNode();
    }
    getParent() {
        (0, common_utils_1.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() {
        (0, common_utils_1.assert)(this.innerCursor !== undefined, 0x460 /* Cursor must be current to be used */);
        return new Cursor(this.forest, this.innerCursor.fork());
    }
    free() {
        (0, common_utils_1.assert)(this.state !== core_1.ITreeSubscriptionCursorState.Freed, 0x33f /* Cursor must not be double freed */);
        this.forest.currentCursors.delete(this);
        this.state = core_1.ITreeSubscriptionCursorState.Freed;
    }
    buildAnchor() {
        (0, common_utils_1.assert)(this.state === core_1.ITreeSubscriptionCursorState.Current, 0x37a /* Cursor must be current to be used */);
        return this.forest.anchors.track(this.getPath());
    }
}
// This function is the only package 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.
 * @alpha
 */
function buildForest(schema, anchors) {
    return new ObjectForest(schema, anchors);
}
exports.buildForest = buildForest;
//# sourceMappingURL=objectForest.js.map