"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.BasicChunkCursor = exports.BasicChunk = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const index_js_1 = require("../../util/index.js");
const treeCursorUtils_js_1 = require("../treeCursorUtils.js");
const chunk_js_1 = require("./chunk.js");
/**
 * General purpose one node chunk.
 */
class BasicChunk extends index_js_1.ReferenceCountedBase {
    /**
     * Create a tree chunk with ref count 1.
     *
     * @param fields - provides exclusive deep ownership of this map to this object (which might mutate it in the future).
     * The caller must have already accounted for this reference to the children in this map (via `referenceAdded`),
     * and any edits to this must update child reference counts.
     * @param value - the value on this node, if any.
     */
    constructor(type, fields, value) {
        super();
        this.type = type;
        this.fields = fields;
        this.value = value;
        this.topLevelLength = 1;
    }
    clone() {
        const fields = new Map();
        for (const [k, v] of this.fields) {
            const field = v.map((child) => {
                child.referenceAdded();
                return child;
            });
            fields.set(k, field);
        }
        return new BasicChunk(this.type, fields, this.value);
    }
    cursor() {
        return new BasicChunkCursor([this], [], [], [], [], [chunk_js_1.dummyRoot], 0, 0, 0, undefined);
    }
    onUnreferenced() {
        for (const v of this.fields.values()) {
            for (const child of v) {
                child.referenceRemoved();
            }
        }
    }
}
exports.BasicChunk = BasicChunk;
/**
 * Cursor over basic chunks.
 *
 * @remarks This implementation is similar to StackCursor, however it is distinct because:
 * 1. The children are chunks, which might have a top level length that greater than 1.
 * 2. It needs to be able to delegate to cursors of other chunk formats it does not natively understand (See TODO below).
 *
 * TODO:
 * This cursor currently only handles child chunks which are BasicChunks:
 * BasicChunks should be an optimized fast path, and arbitrary chunk formats,
 * like UniformChunk, should be supported by delegating to their cursor implementations.
 */
class BasicChunkCursor extends treeCursorUtils_js_1.SynchronousCursor {
    /**
     * Starts at root field which might be a detached sequence.
     *
     * @param root - sequence of BasicChunk which make up the contents of the root sequence.
     * Since this cursor starts in `Fields` mode at the root, the siblings array when in fields mode is just the field keys,
     * this is needed to get the actual root nodes when entering nodes of the root field.
     * @param siblingStack - Stack of collections of siblings along the path through the tree:
     * does not include current level (which is stored in `siblings`).
     * Even levels in the stack (starting from 0) are keys and odd levels are sequences of nodes.
     * @param indexStack - Stack of indices into the corresponding levels in `siblingStack`.
     * @param indexOfChunkStack - Index of chunk in array of chunks. Only for Node levels.
     * @param indexWithinChunkStack - Index within chunk selected by indexOfChunkStack. Only for Node levels.
     * @param siblings - Siblings at the current level (not included in `siblingStack`).
     * @param index - Index into `siblings`.
     * @param indexOfChunk - Index of chunk in array of chunks. Only for Nodes mode.
     * @param indexWithinChunk - Index within chunk selected by indexOfChunkStack. Only for Nodes mode.
     * @param nestedCursor - When the outer cursor (this `BasicChunkCursor` cursor)
     * navigates into a chunk it does not natively understand (currently anything other than `BasicChunk`s)
     * it creates the `nestedCursor` over that chunk, and delegates all operations to it.
     */
    constructor(root, siblingStack, indexStack, indexOfChunkStack, 
    // TODO: Currently only BasicChunks are supported, and the currently always have a top level length of 1.
    // That makes this stack unneeded. When BasicChunkCursor is more feature complete, this stack should be reevaluated, and removed if possible.
    indexWithinChunkStack, siblings, index, indexOfChunk, indexWithinChunk, nestedCursor) {
        super();
        this.root = root;
        this.siblingStack = siblingStack;
        this.indexStack = indexStack;
        this.indexOfChunkStack = indexOfChunkStack;
        this.indexWithinChunkStack = indexWithinChunkStack;
        this.siblings = siblings;
        this.index = index;
        this.indexOfChunk = indexOfChunk;
        this.indexWithinChunk = indexWithinChunk;
        this.nestedCursor = nestedCursor;
    }
    get [chunk_js_1.cursorChunk]() {
        if (this.nestedCursor !== undefined) {
            return this.nestedCursor[chunk_js_1.cursorChunk];
        }
        (0, internal_1.assert)(this.mode === 0 /* CursorLocationType.Nodes */, 0x57a /* must be in nodes mode */);
        return this.siblings[this.indexOfChunk];
    }
    atChunkRoot() {
        return (this.siblingStack.length < 2 &&
            (this.nestedCursor === undefined || this.nestedCursor.atChunkRoot()));
    }
    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 BasicChunkCursor(this.root, [...this.siblingStack], [...this.indexStack], [...this.indexOfChunkStack], [...this.indexWithinChunkStack], this.siblings, this.index, this.indexOfChunk, this.indexWithinChunk, this.nestedCursor?.fork());
    }
    get mode() {
        if (this.nestedCursor !== undefined) {
            return this.nestedCursor.mode;
        }
        // Compute the number of nodes deep the current depth is.
        // We want the floor of the result, which can computed using a bitwise shift assuming the depth is less than 2^31, which seems safe.
        // eslint-disable-next-line no-bitwise
        const halfHeight = (this.siblingStack.length + 1) >> 1;
        (0, internal_1.assert)(this.indexOfChunkStack.length === halfHeight, 0x51c /* unexpected indexOfChunkStack */);
        (0, internal_1.assert)(this.indexWithinChunkStack.length === halfHeight, 0x51d /* unexpected indexWithinChunkStack */);
        return this.siblingStack.length % 2 === 0
            ? 1 /* CursorLocationType.Fields */
            : 0 /* CursorLocationType.Nodes */;
    }
    getFieldKey() {
        if (this.nestedCursor !== undefined) {
            return this.nestedCursor.getFieldKey();
        }
        (0, internal_1.assert)(this.mode === 1 /* CursorLocationType.Fields */, 0x51e /* must be in fields mode */);
        return this.siblings[this.index];
    }
    getStackedFieldKey(height) {
        (0, internal_1.assert)(height % 2 === 0, 0x51f /* must field height */);
        return this.siblingStack[height][this.indexStack[height]];
    }
    getStackedNodeIndex(height) {
        (0, internal_1.assert)(height % 2 === 1, 0x520 /* must be node height */);
        (0, internal_1.assert)(height >= 0, 0x521 /* must not be above root */);
        return this.indexStack[height];
    }
    getStackedNode(height) {
        const index = this.getStackedNodeIndex(height);
        return this.siblingStack[height][index];
    }
    getFieldLength() {
        if (this.nestedCursor !== undefined) {
            return this.nestedCursor.getFieldLength();
        }
        (0, internal_1.assert)(this.mode === 1 /* CursorLocationType.Fields */, 0x522 /* must be in fields mode */);
        let total = 0;
        // TODO: optimize?
        for (const chunk of this.getField()) {
            total += chunk.topLevelLength;
        }
        return total;
    }
    enterNode(index) {
        if (this.nestedCursor !== undefined) {
            this.nestedCursor.enterNode(index);
            return;
        }
        const found = this.firstNode() && this.seekNodes(index);
        (0, internal_1.assert)(found, 0x523 /* child must exist at index */);
    }
    getPath(prefix) {
        if (this.nestedCursor !== undefined) {
            return (this.nestedCursor.getPath(this.nestedPathPrefix(prefix)) ??
                (0, index_js_1.fail)("nested cursors should not be root"));
        }
        (0, internal_1.assert)(this.mode === 0 /* CursorLocationType.Nodes */, 0x524 /* must be in nodes mode */);
        const path = this.getOffsetPath(0, prefix);
        (0, internal_1.assert)(path !== undefined, 0x55c /* field root cursor should never have undefined path */);
        return path;
    }
    nestedPathPrefix(prefix) {
        // This uses index offset for actual node, when it should use offset for start of chunk.
        // To compensate, subtract this.indexWithinChunk below.
        const rootPath = this.getOffsetPath(0, prefix) ?? (0, index_js_1.fail)("nested cursors should not be root");
        return {
            indexOffset: rootPath.parentIndex - this.indexWithinChunk,
            rootFieldOverride: rootPath.parentField,
            parent: rootPath.parent,
        };
    }
    getFieldPath(prefix) {
        if (this.nestedCursor !== undefined) {
            return this.nestedCursor.getFieldPath(this.nestedPathPrefix(prefix));
        }
        (0, internal_1.assert)(this.mode === 1 /* CursorLocationType.Fields */, 0x525 /* must be in fields mode */);
        return {
            field: this.indexStack.length === 1
                ? prefix?.rootFieldOverride ?? this.getFieldKey()
                : this.getFieldKey(),
            parent: this.getOffsetPath(1, prefix),
        };
    }
    getOffsetPath(offset, prefix) {
        // It is more efficient to handle prefix directly in here rather than delegating to PrefixedPath.
        const length = this.indexStack.length - offset;
        if (length === -1) {
            return prefix?.parent; // At root
        }
        (0, internal_1.assert)(length > 0, 0x526 /* invalid offset to above root */);
        (0, internal_1.assert)(length % 2 === 1, 0x527 /* offset path must point to node not field */);
        // Perf Note:
        // This is O(depth) in tree.
        // If many different anchors are created, this could be optimized to amortize the costs.
        // For example, the cursor could cache UpPaths from the anchorSet when creating an anchor,
        // then reuse them as a starting point when making another.
        // Could cache this at one depth, and remember the depth.
        // When navigating up, adjust cached anchor if present.
        let path;
        function updatePath(newPath) {
            path = path === undefined ? (0, treeCursorUtils_js_1.prefixPath)(prefix, newPath) : newPath;
        }
        // Skip top level, since root node in path is "undefined" and does not have a parent or index.
        for (let height = 1; height < length; height += 2) {
            const key = this.getStackedFieldKey(height - 1);
            updatePath({
                parent: path,
                parentIndex: this.getStackedNodeIndex(height),
                parentField: key,
            });
        }
        updatePath({
            parent: path,
            parentIndex: offset === 0 ? this.index : this.getStackedNodeIndex(length),
            parentField: this.getStackedFieldKey(length - 1),
        });
        return path;
    }
    enterField(key) {
        if (this.nestedCursor !== undefined) {
            this.nestedCursor.enterField(key);
            return;
        }
        (0, internal_1.assert)(this.mode === 0 /* CursorLocationType.Nodes */, 0x528 /* must be in nodes mode */);
        this.siblingStack.push(this.siblings);
        this.indexStack.push(this.index);
        // For fields, siblings are only used for key lookup and
        // nextField and which has arbitrary iteration order,
        // so making a array of just key here works.
        // This adds an allocation, so it's optimizing code simplicity and for the other use case (enumeration)
        // at the cost of an allocation here.
        this.index = 0;
        this.siblings = [key];
    }
    nextField() {
        if (this.nestedCursor !== undefined) {
            return this.nestedCursor.nextField();
        }
        this.index += 1;
        if (this.index === this.siblings.length) {
            this.exitField();
            return false;
        }
        return true;
    }
    firstField() {
        if (this.nestedCursor !== undefined) {
            return this.nestedCursor.firstField();
        }
        const fields = this.getNode().fields;
        if (fields.size === 0) {
            return false;
        }
        this.siblingStack.push(this.siblings);
        this.indexStack.push(this.index);
        this.index = 0;
        this.siblings = [...fields.keys()]; // TODO: avoid this copy
        return true;
    }
    seekNodes(offset) {
        if (this.nestedCursor !== undefined) {
            const atRoot = this.nestedCursor.atChunkRoot();
            const stillIn = this.nestedCursor.seekNodes(offset);
            if (!atRoot) {
                return stillIn;
            }
            if (!stillIn) {
                this.nestedCursor = undefined;
            }
        }
        (0, internal_1.assert)(this.mode === 0 /* CursorLocationType.Nodes */, 0x529 /* can only seekNodes when in Nodes */);
        (0, internal_1.assert)(this.indexOfChunk < this.siblings.length, 0x52a /* out of bounds indexOfChunk */);
        this.indexWithinChunk += offset;
        if (offset >= 0) {
            const chunks = this.siblings;
            while (this.indexWithinChunk >= chunks[this.indexOfChunk].topLevelLength) {
                this.indexWithinChunk -= chunks[this.indexOfChunk].topLevelLength;
                this.indexOfChunk++;
                if (this.indexOfChunk === chunks.length) {
                    this.exitNode();
                    return false;
                }
                (0, internal_1.assert)(this.indexOfChunk < this.siblings.length, 0x52b /* out of bounds indexOfChunk */);
            }
        }
        else {
            const chunks = this.siblings;
            while (this.indexWithinChunk < 0) {
                if (this.indexOfChunk === 0) {
                    this.exitNode();
                    return false;
                }
                this.indexOfChunk--;
                this.indexWithinChunk += chunks[this.indexOfChunk].topLevelLength;
            }
        }
        this.index += offset;
        this.initNestedCursor();
        return true;
    }
    firstNode() {
        if (this.nestedCursor !== undefined) {
            return this.nestedCursor.firstNode();
        }
        const siblings = this.getField();
        if (siblings.length === 0) {
            return false;
        }
        this.siblingStack.push(this.siblings);
        this.indexStack.push(this.index);
        this.indexOfChunkStack.push(this.indexOfChunk);
        this.indexWithinChunkStack.push(this.indexWithinChunk);
        this.index = 0;
        this.siblings = siblings;
        this.indexOfChunk = 0;
        this.indexWithinChunk = 0;
        this.initNestedCursor();
        return true;
    }
    nextNode() {
        if (this.nestedCursor !== undefined) {
            const atRoot = this.nestedCursor.atChunkRoot();
            const stillIn = this.nestedCursor.nextNode();
            if (!atRoot) {
                return stillIn;
            }
            if (!stillIn) {
                this.nestedCursor = undefined;
            }
        }
        (0, internal_1.assert)(this.mode === 0 /* CursorLocationType.Nodes */, 0x52c /* can only nextNode when in Nodes */);
        this.indexWithinChunk++;
        if (this.indexWithinChunk ===
            this.siblings[this.indexOfChunk].topLevelLength) {
            this.indexOfChunk++;
            if (this.indexOfChunk === this.siblings.length) {
                this.exitNode();
                return false;
            }
            this.indexWithinChunk = 0;
            this.initNestedCursor();
        }
        this.index++;
        return true;
    }
    initNestedCursor() {
        (0, internal_1.assert)(this.mode === 0 /* CursorLocationType.Nodes */, 0x55d /* can only initNestedCursor when in Nodes */);
        const chunk = this.siblings[this.indexOfChunk];
        this.nestedCursor = !(chunk instanceof BasicChunk) ? chunk.cursor() : undefined;
        this.nestedCursor?.enterNode(this.indexWithinChunk);
    }
    exitField() {
        if (this.nestedCursor !== undefined) {
            return this.nestedCursor.exitField();
        }
        (0, internal_1.assert)(this.mode === 1 /* CursorLocationType.Fields */, 0x52d /* can only navigate up from field when in field */);
        this.siblings = this.siblingStack.pop() ?? (0, index_js_1.fail)("Unexpected siblingStack.length");
        this.index = this.indexStack.pop() ?? (0, index_js_1.fail)("Unexpected indexStack.length");
    }
    exitNode() {
        if (this.nestedCursor !== undefined) {
            if (!this.nestedCursor.atChunkRoot()) {
                return this.nestedCursor.exitNode();
            }
            this.nestedCursor = undefined;
        }
        (0, internal_1.assert)(this.mode === 0 /* CursorLocationType.Nodes */, 0x52e /* can only navigate up from node when in node */);
        this.siblings = this.siblingStack.pop() ?? (0, index_js_1.fail)("Unexpected siblingStack.length");
        this.index = this.indexStack.pop() ?? (0, index_js_1.fail)("Unexpected indexStack.length");
        this.indexOfChunk =
            this.indexOfChunkStack.pop() ?? (0, index_js_1.fail)("Unexpected indexOfChunkStack.length");
        this.indexWithinChunk =
            this.indexWithinChunkStack.pop() ?? (0, index_js_1.fail)("Unexpected indexWithinChunkStack.length");
    }
    getNode() {
        (0, internal_1.assert)(this.mode === 0 /* CursorLocationType.Nodes */, 0x52f /* can only get node when in node */);
        return this.siblings[this.index];
    }
    getField() {
        if (this.siblingStack.length === 0) {
            return this.root;
        }
        (0, internal_1.assert)(this.mode === 1 /* CursorLocationType.Fields */, 0x530 /* can only get field when in fields */);
        const parent = this.getStackedNode(this.indexStack.length - 1);
        const key = this.getFieldKey();
        const field = parent.fields.get(key) ?? [];
        return field;
    }
    get value() {
        if (this.nestedCursor !== undefined) {
            return this.nestedCursor.value;
        }
        return this.getNode().value;
    }
    get type() {
        if (this.nestedCursor !== undefined) {
            return this.nestedCursor.type;
        }
        return this.getNode().type;
    }
    get fieldIndex() {
        (0, internal_1.assert)(this.mode === 0 /* CursorLocationType.Nodes */, 0x531 /* can only node's index when in node */);
        if (this.nestedCursor !== undefined) {
            if (this.nestedCursor.atChunkRoot()) {
                // TODO: this.index
                return this.nestedCursor.fieldIndex + this.nestedOffset();
            }
            return this.nestedCursor.fieldIndex;
        }
        return this.index;
    }
    /**
     * Within the field that `nestedCursor` is nested in:
     * returns the index within that field of the first node that is part of the chunk nestedCursor traverses.
     */
    nestedOffset() {
        (0, internal_1.assert)(this.nestedCursor !== undefined, 0x55e /* nested offset requires nested cursor */);
        (0, internal_1.assert)(!this.nestedCursor.atChunkRoot() ||
            this.indexWithinChunk === this.nestedCursor.fieldIndex, 0x55f /* indexes should match if at root */);
        return this.index - this.indexWithinChunk;
    }
    get chunkStart() {
        if (this.nestedCursor !== undefined) {
            if (this.nestedCursor.atChunkRoot()) {
                return this.nestedCursor.chunkStart + this.nestedOffset();
            }
            return this.nestedCursor.chunkStart;
        }
        return this.fieldIndex;
    }
    get chunkLength() {
        if (this.nestedCursor !== undefined) {
            return this.nestedCursor.chunkLength;
        }
        return 1;
    }
}
exports.BasicChunkCursor = BasicChunkCursor;
//# sourceMappingURL=basicChunk.js.map