"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChunkShape = exports.TreeShape = exports.UniformChunk = exports.uniformChunk = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const index_js_1 = require("../../core/index.js");
const index_js_2 = require("../../util/index.js");
const treeCursorUtils_js_1 = require("../treeCursorUtils.js");
/**
 * Create a tree chunk with ref count 1.
 *
 * @param shape - describes the semantics and layout of `values`.
 * @param values - provides exclusive ownership of this array to this object (which might mutate it in the future).
 */
function uniformChunk(shape, values) {
    return new UniformChunk(shape, values);
}
exports.uniformChunk = uniformChunk;
/**
 * Chunk which handles a sequence of trees with identical "shape" (see `TreeShape`).
 *
 * Separates shape from content,
 * allowing deduplication of shape information and storing of content as a flat sequence of values.
 */
class UniformChunk extends index_js_2.ReferenceCountedBase {
    /**
     * Create a tree chunk with ref count 1.
     *
     * @param shape - describes the semantics and layout of `values`.
     * @param values - provides exclusive ownership of this array to this object (which might mutate it in the future).
     */
    constructor(shape, values, idCompressor) {
        super();
        this.shape = shape;
        this.values = values;
        this.idCompressor = shape.treeShape.mayContainCompressedIds ? idCompressor : undefined;
        (0, internal_1.assert)(shape.treeShape.valuesPerTopLevelNode * shape.topLevelLength === values.length, 0x4c3 /* invalid number of values for shape */);
    }
    get topLevelLength() {
        return this.shape.topLevelLength;
    }
    clone() {
        return new UniformChunk(this.shape, [...this.values]);
    }
    cursor() {
        return new Cursor(this);
    }
    onUnreferenced() { }
}
exports.UniformChunk = UniformChunk;
/**
 * Maximum topLevelLength value (exclusive) for which {@link TreeShape.withTopLevelLength}
 * caches the resulting {@link ChunkShape}. Values at or above this threshold always
 * create a new instance to prevent unbounded cache growth.
 *
 * @remarks
 * This value is an estimation of the general size needed to cover current workflows,
 * not a researched constant, and is safe to tune as workloads change.
 *
 * Raising this value captures more chunk sizes in the cache, at the cost of
 * each `TreeShape` retaining up to `chunkShapeCacheLimit - 1` cached entries for the
 * lifetime of the shape. Lowering it reduces memory held per `TreeShape` but forces
 * small chunks, where the relative cost of rebuilding `positions` is highest, to pay
 * the construction cost on every call.
 */
const chunkShapeCacheLimit = 8;
/**
 * The "shape" of a tree.
 * Does not contain the actual values from  the tree, but describes everything else,
 * including where the values would be found in a flat values array.
 *
 * Note that since this requires fields to have uniform shapes (see `FieldShape`),
 * not all trees can have their shape described using this type.
 */
class TreeShape {
    /**
     * @param type - {@link TreeNodeSchemaIdentifier} used to compare shapes.
     * @param hasValue - whether or not the TreeShape has a value.
     * @param fieldsArray - an array of {@link FieldShape} values, which contains a TreeShape for each FieldKey.
     *
     * @param maybeCompressedIdLeaf - whether the value may have been compressed by the {@link UniformChunk.idCompressor}.
     * Can only be explicitly set to `true` on string leaf nodes; otherwise this constructor asserts.
     * For non-leaf nodes, {@link TreeShape.mayContainCompressedIds} is automatically derived from child shapes.
     */
    constructor(type, hasValue, fieldsArray, maybeCompressedIdLeaf = false) {
        this.type = type;
        this.hasValue = hasValue;
        this.fieldsArray = fieldsArray;
        /**
         * Cache for ChunkShape instances created by {@link withTopLevelLength}.
         * `topLevelLength` is always a positive integer (enforced by the {@link ChunkShape} constructor),
         * so the cache only ever holds entries for values in `1..chunkShapeCacheLimit - 1` to prevent unbounded growth.
         */
        this.chunkShapeCache = new Map();
        (0, internal_1.assert)(hasValue === false || fieldsArray.length === 0, 0xcef /* only non-leaf can have fields */);
        if (maybeCompressedIdLeaf) {
            (0, internal_1.assert)(hasValue && type === "com.fluidframework.leaf.string", 0xcf0 /* only strings can opt into maybeCompressedIdLeaf */);
        }
        // For non-leaf nodes, derive from whether any child shapes contain compressed ids.
        this.mayContainCompressedIds =
            maybeCompressedIdLeaf || fieldsArray.some(([, shape]) => shape.mayContainCompressedIds);
        const fields = new Map();
        let numberOfValues = hasValue ? 1 : 0;
        const infos = [
            new NodePositionInfo(undefined, index_js_1.dummyRoot, 0, undefined, undefined, this, 1, 0),
        ];
        let fieldIndex = 0;
        for (const [k, f, length] of fieldsArray) {
            (0, internal_1.assert)(!fields.has(k), 0x4c5 /* no duplicate keys */);
            const offset = new OffsetShape(f, length, infos.length, k, fieldIndex);
            fields.set(k, offset);
            clonePositions(0, [k, f, length], fieldIndex, numberOfValues, infos);
            numberOfValues += f.valuesPerTopLevelNode * length;
            fieldIndex++;
        }
        this.fields = fields;
        this.valuesPerTopLevelNode = numberOfValues;
        this.positions = infos;
        this.fieldsOffsetArray = [...fields.values()];
    }
    equals(other) {
        // TODO: either dedupe instances and/or store a collision resistant hash for fast compare.
        if (!(0, internal_1.compareArrays)(this.fieldsArray, other.fieldsArray, ([k, f, l], [k2, f2, l2]) => k === k2 && l === l2 && f.equals(f2))) {
            return false;
        }
        return (this.type === other.type &&
            this.hasValue === other.hasValue &&
            this.mayContainCompressedIds === other.mayContainCompressedIds);
    }
    withTopLevelLength(topLevelLength) {
        if (topLevelLength < chunkShapeCacheLimit) {
            return (0, index_js_2.getOrCreate)(this.chunkShapeCache, topLevelLength, () => new ChunkShape(this, topLevelLength));
        }
        return new ChunkShape(this, topLevelLength);
    }
}
exports.TreeShape = TreeShape;
function clonePositions(indexOfParentInOutput, [key, shape, copies], indexOfParentField, valueOffset, outputInto) {
    const offset = outputInto.length;
    for (let index = 0; index < copies; index++) {
        for (const inner of shape.positions) {
            const wasRoot = inner.indexOfParentPosition === undefined;
            const parentPositionIndex = wasRoot
                ? indexOfParentInOutput
                : inner.indexOfParentPosition + index * shape.positions.length + offset;
            outputInto.push(new NodePositionInfo(parentPositionIndex === undefined ? undefined : outputInto[parentPositionIndex], inner.parentField === index_js_1.dummyRoot ? key : inner.parentField, wasRoot ? index : inner.parentIndex, inner.indexOfParentField ?? indexOfParentField, parentPositionIndex, inner.shape, wasRoot ? copies : inner.topLevelLength, inner.valueOffset + valueOffset + shape.valuesPerTopLevelNode * index));
        }
    }
}
/**
 * The shape (see `TreeShape`) of a sequence of trees, all with the same shape (like `FieldShape`, but without a field key).
 *
 * @remarks
 * Paired with a value array, this lets a {@link UniformChunk} be traversed like a tree by an
 * {@link ITreeCursorSynchronous}. The {@link Cursor} derives each node's position info from the
 * shared {@link TreeShape.positions} plus the node's top-level index.
 *
 * TODO: consider storing shape information in WASM
 */
class ChunkShape {
    constructor(treeShape, topLevelLength) {
        this.treeShape = treeShape;
        this.topLevelLength = topLevelLength;
        (0, internal_1.assert)(topLevelLength > 0, 0x4c6 /* topLevelLength must be greater than 0 */);
    }
    equals(other) {
        // TODO: either dedup instances and/or store a collision resistant hash for fast compare.
        return this.topLevelLength === other.topLevelLength && this.treeShape === other.treeShape;
    }
}
exports.ChunkShape = ChunkShape;
/**
 * Shape of a field (like `FieldShape`) but with information about how it would be offset within a chunk because of its parents.
 */
class OffsetShape {
    /**
     * @param shape - the shape of each child in this field
     * @param topLevelLength - number of top level nodes in this sequence chunk (either field within a chunk, or top level chunk)
     * @param offset - number of nodes before this in the parent's subtree. The nodes are considered in depth first pre order
     * traversal, so a parent is the first node in its subtree (before its children) with offset 0
     * @param key - field key
     * @param indexOfParentField - index to this shape in the parent's array of fields
     */
    constructor(shape, topLevelLength, offset, key, indexOfParentField) {
        this.shape = shape;
        this.topLevelLength = topLevelLength;
        this.offset = offset;
        this.key = key;
        this.indexOfParentField = indexOfParentField;
    }
}
/**
 * Information about a node at a specific position within one top-level tree of a {@link TreeShape}.
 */
class NodePositionInfo {
    /**
     * @param parent - The parent node's {@link NodePositionInfo} or `undefined` for a root.
     * @param parentField - The {@link FieldKey} of the field this node occupies within its parent.
     * @param parentIndex - indexWithinParentField
     * @param indexOfParentField - Which field of the parent `parentIndex` is indexing into to locate this.
     * @param indexOfParentPosition - Index of this node's parent in {@link TreeShape.positions}
     * @param shape - Shape of the top level sequence this node is part of
     * @param topLevelLength - Number of siblings in this node's field. For a root this is unused
     * @param valueOffset - Offset of this node's value within one top-level tree's slice of the chunk's flat `values` array;
     * only valid when `shape.hasValue` (otherwise it's where the value would have gone, and should not be used to read a value).
     */
    constructor(parent, // TODO; general UpPath to allow prefixing here?
    parentField, parentIndex, indexOfParentField, indexOfParentPosition, shape, // Shape of sequence that contains this node (top level is parent of this node)
    topLevelLength, valueOffset) {
        this.parent = parent;
        this.parentField = parentField;
        this.parentIndex = parentIndex;
        this.indexOfParentField = indexOfParentField;
        this.indexOfParentPosition = indexOfParentPosition;
        this.shape = shape;
        this.topLevelLength = topLevelLength;
        this.valueOffset = valueOffset;
    }
}
/**
 * The cursor implementation for `UniformChunk`.
 *
 * @remarks
 * Tracks a flat `positionIndex` and derives each node's position info from the shape's shared
 * {@link TreeShape.positions} plus the node's top-level index.
 */
class Cursor extends treeCursorUtils_js_1.SynchronousCursor {
    // TODO: support prefix (path above root, including index offset of chunk in its containing field)
    constructor(chunk) {
        super();
        this.chunk = chunk;
        /** Which top-level node of the chunk the current position is within. Valid when nodePositionInfo !== undefined. */
        this.topLevelIndex = 0;
        this.mode = 1 /* CursorLocationType.Fields */;
        // Valid only in fields mode. Can be past end for empty fields.
        // This is redundant with fieldKey above (but might be worth keeping for perf), and could be removed.
        this.indexOfField = 0;
        this.chunkStart = 0;
        this.shape = this.chunk.shape;
        this.treeShape = this.shape.treeShape;
        this.nodeLength = this.treeShape.positions.length;
        this.stride = this.treeShape.valuesPerTopLevelNode;
        this.fieldKey = index_js_1.dummyRoot;
        this.moveToPosition(0);
    }
    get [index_js_1.cursorChunk]() {
        return this.atChunkRoot() ? this.chunk : undefined;
    }
    atChunkRoot() {
        (0, internal_1.assert)((this.fieldKey === undefined) === (this.mode === 0 /* CursorLocationType.Nodes */), 0x560 /* expect valid field key */);
        return (this.nodePositionInfo === undefined ||
            (this.nodePositionInfo.parent === undefined && this.fieldKey === undefined));
    }
    fork() {
        const cursor = new Cursor(this.chunk);
        cursor.mode = this.mode;
        cursor.fieldKey = this.fieldKey;
        cursor.indexOfField = this.indexOfField;
        cursor.moveToPosition(this.positionIndex);
        return cursor;
    }
    /**
     * Change the current node within the chunk.
     *
     * @param positionIndex - flat position index of the newly selected node. This is NOT an index
     * within a field, and is not bounds checked.
     *
     * @remarks
     * Decomposes the index into {@link Cursor.topLevelIndex} and {@link Cursor.nodePositionInfo}.
     * See `nodeInfo` for getting data about the current node.
     *
     */
    moveToPosition(positionIndex) {
        this.positionIndex = positionIndex;
        if (positionIndex === 0) {
            this.nodePositionInfo = undefined;
            (0, internal_1.assert)(this.mode === 1 /* CursorLocationType.Fields */, 0x562 /* expected root to be a field */);
            return;
        }
        const decoded = this.decodePosition(positionIndex);
        this.topLevelIndex = decoded.topLevelIndex;
        this.nodePositionInfo = decoded.info;
    }
    /**
     * Decode a flat `positionIndex` into its components.
     *
     * @param positionIndex - flat position index of the node to decode. Must be greater than 0;
     * @returns the node's index within {@link TreeShape.positions} (`withinTree`), which top-level
     * tree holds it (`topLevelIndex`), and the corresponding shared {@link NodePositionInfo} (`info`).
     */
    decodePosition(positionIndex) {
        const offset = positionIndex - 1;
        // Find the node's index within treeShape.positions, then which top-level tree holds it.
        const withinTree = offset % this.nodeLength; // remainder
        const topLevelIndex = (offset - withinTree) / this.nodeLength; // quotient
        const info = this.treeShape.positions[withinTree] ?? (0, internal_1.oob)();
        return { withinTree, topLevelIndex, info };
    }
    /**
     * Build a standalone {@link UpPath} for the node at `positionIndex`. O(depth) allocation.
     *
     * @remarks
     * walks the shared per-tree {@link TreeShape.positions} and applies the top-level index
     * at each level. Mirrors how the `BasicChunk` cursor allocates paths.
     */
    materializePath(positionIndex) {
        if (positionIndex === 0) {
            return undefined;
        }
        const { withinTree, topLevelIndex, info } = this.decodePosition(positionIndex);
        if (info.parent === undefined) {
            // Top-level node: its parent is the (prefixed) chunk root.
            return { parent: undefined, parentField: info.parentField, parentIndex: topLevelIndex };
        }
        return {
            parent: this.materializePath(positionIndex - withinTree + (info.indexOfParentPosition ?? (0, internal_1.oob)())),
            parentField: info.parentField,
            parentIndex: info.parentIndex,
        };
    }
    /**
     * Gets information about the current node.
     *
     * When in Nodes mode, this means the node this cursor is current at.
     * When if fields mode, this means the node which is the parent of the current field.
     * This cursor is in Nodes mode at the root, so there is no case where a fields mode does not have a parent.
     *
     * @param requiredMode - asserts that the mode matches this. Since the semantics of this function are somewhat mode dependent,
     * providing this ensures that the caller knows what the results will mean.
     */
    nodeInfo(requiredMode) {
        (0, internal_1.assert)(this.mode === requiredMode, 0x4c8 /* tried to access cursor when in wrong mode */);
        (0, internal_1.assert)(this.nodePositionInfo !== undefined, 0x53e /* can not access nodeInfo in root field */);
        return this.nodePositionInfo;
    }
    nextField() {
        this.indexOfField++;
        const fields = this.nodeInfo(1 /* CursorLocationType.Fields */).shape.fieldsArray;
        if (this.indexOfField < fields.length) {
            const fieldArr = fields[this.indexOfField] ?? (0, internal_1.oob)();
            this.fieldKey = fieldArr[0];
            return true;
        }
        this.exitField();
        return false;
    }
    exitField() {
        (0, internal_1.assert)(this.mode === 1 /* CursorLocationType.Fields */, 0x4c9 /* exitField when in wrong mode */);
        (0, internal_1.assert)(this.nodePositionInfo !== undefined, 0x563 /* can not exit root field */);
        this.fieldKey = undefined;
        this.mode = 0 /* CursorLocationType.Nodes */;
    }
    getFieldKey() {
        return this.fieldKey ?? (0, internal_1.fail)(0xb09 /* not in a field */);
    }
    getFieldLength() {
        (0, internal_1.assert)(this.mode === 1 /* CursorLocationType.Fields */, 0x53f /* tried to access cursor when in wrong mode */);
        if (this.nodePositionInfo === undefined) {
            return this.shape.topLevelLength;
        }
        const fieldInfo = this.nodePositionInfo.shape.fieldsArray[this.indexOfField];
        if (fieldInfo === undefined) {
            return 0;
        }
        return fieldInfo[2];
    }
    firstNode() {
        (0, internal_1.assert)(this.mode === 1 /* CursorLocationType.Fields */, 0x540 /* tried to access cursor when in wrong mode */);
        if (this.nodePositionInfo === undefined) {
            // Root field is not allowed to be empty, so we can skip handling that case.
            this.enterRootNodeInner(0);
            return true;
        }
        else {
            return this.enterNodeInner(this.nodePositionInfo, 0);
        }
    }
    enterNode(childIndex) {
        (0, internal_1.assert)(this.mode === 1 /* CursorLocationType.Fields */, 0x541 /* tried to access cursor when in wrong mode */);
        (0, internal_1.assert)(childIndex >= 0, 0x4ca /* index must be positive */);
        if (this.nodePositionInfo === undefined) {
            (0, internal_1.assert)(childIndex < this.shape.topLevelLength, 0x542 /* index must not be past the end of the field */);
            this.enterRootNodeInner(childIndex);
        }
        else {
            const moved = this.enterNodeInner(this.nodePositionInfo, childIndex);
            (0, internal_1.assert)(moved, 0x4cb /* index must not be past the end of the field */);
        }
    }
    /**
     * Enter the current field, at `childIndex`.
     * @param childIndex - index into current field to navigate to. Must be non-negative integer.
     */
    enterNodeInner(currentPosition, childIndex) {
        const shape = currentPosition.shape;
        const fields = shape.fieldsOffsetArray;
        if (this.indexOfField >= fields.length) {
            return false; // Handle empty field (indexed by key into empty field)
        }
        const f = shape.fieldsOffsetArray[this.indexOfField] ?? (0, internal_1.oob)();
        if (childIndex >= f.topLevelLength) {
            return false;
        }
        this.mode = 0 /* CursorLocationType.Nodes */;
        this.fieldKey = undefined;
        this.moveToPosition(this.positionIndex + f.offset + childIndex * f.shape.positions.length);
        (0, internal_1.assert)(this.fieldIndex === childIndex, 0x4cc /* should be at selected child */);
        return true;
    }
    enterRootNodeInner(childIndex) {
        this.mode = 0 /* CursorLocationType.Nodes */;
        this.fieldKey = undefined;
        // 1 for the "undefined" root-field marker at position 0, then stride by one top-level tree (nodeLength).
        this.moveToPosition(1 + childIndex * this.nodeLength);
        (0, internal_1.assert)(this.fieldIndex === childIndex, 0x543 /* should be at selected child */);
    }
    getFieldPath(prefix) {
        return (0, treeCursorUtils_js_1.prefixFieldPath)(prefix, {
            field: this.getFieldKey(),
            parent: this.materializePath(this.positionIndex),
        });
    }
    getPath(prefix) {
        this.nodeInfo(0 /* CursorLocationType.Nodes */); // assert: in nodes mode at a node
        return (0, treeCursorUtils_js_1.prefixPath)(prefix, this.materializePath(this.positionIndex));
    }
    get fieldIndex() {
        const info = this.nodeInfo(0 /* CursorLocationType.Nodes */);
        return info.parent === undefined ? this.topLevelIndex : info.parentIndex;
    }
    /**
     * Number of nodes in `info`'s field including `info` itself.
     *
     * @remarks
     * For top-level nodes this is the chunk's `topLevelLength`, read from the chunk
     * rather than the node, so the shared per-tree {@link TreeShape.positions} stays independent of
     * chunk length; the root entry's own `topLevelLength` field is unused. Nested nodes use the
     * field length stored on the node.
     */
    siblingCount(info) {
        return info.parent === undefined ? this.shape.topLevelLength : info.topLevelLength;
    }
    get chunkLength() {
        return this.siblingCount(this.nodeInfo(0 /* CursorLocationType.Nodes */));
    }
    seekNodes(offset) {
        const info = this.nodeInfo(0 /* CursorLocationType.Nodes */);
        const index = offset + this.fieldIndex;
        if (index >= 0 && index < this.siblingCount(info)) {
            this.moveToPosition(this.positionIndex + offset * info.shape.positions.length);
            return true;
        }
        this.exitNode();
        return false;
    }
    nextNode() {
        // This is the same as `return this.seekNodes(1);` but slightly faster.
        const info = this.nodeInfo(0 /* CursorLocationType.Nodes */);
        const index = this.fieldIndex + 1;
        if (index === this.siblingCount(info)) {
            this.exitNode();
            return false;
        }
        this.moveToPosition(this.positionIndex + info.shape.positions.length);
        return true;
    }
    exitNode() {
        const info = this.nodeInfo(0 /* CursorLocationType.Nodes */);
        const withinTree = this.positionIndex - 1 - this.topLevelIndex * this.nodeLength;
        // Top-level nodes (no parent) exit to the root field at position 0;
        // nested nodes' parent is `indexOfParentPosition` within the same top-level instance.
        this.indexOfField = info.indexOfParentField ?? 0;
        this.fieldKey = info.parentField;
        this.mode = 1 /* CursorLocationType.Fields */;
        this.moveToPosition(info.indexOfParentPosition === undefined
            ? 0
            : this.positionIndex - withinTree + info.indexOfParentPosition);
    }
    firstField() {
        const fieldsArray = this.nodeInfo(0 /* CursorLocationType.Nodes */).shape.fieldsArray;
        if (!(0, index_js_2.hasSome)(fieldsArray)) {
            return false;
        }
        this.indexOfField = 0;
        this.mode = 1 /* CursorLocationType.Fields */;
        const fields = fieldsArray[0];
        this.fieldKey = fields[0];
        return true;
    }
    enterField(key) {
        const fieldMap = this.nodeInfo(0 /* CursorLocationType.Nodes */).shape.fields;
        const fieldInfo = fieldMap.get(key);
        this.indexOfField =
            fieldInfo === undefined
                ? fieldMap.size
                : (fieldInfo.indexOfParentField ?? (0, internal_1.fail)(0xb0c /* children should have parents */));
        this.fieldKey = key;
        this.mode = 1 /* CursorLocationType.Fields */;
    }
    get type() {
        return this.nodeInfo(0 /* CursorLocationType.Nodes */).shape.type;
    }
    get value() {
        const info = this.nodeInfo(0 /* CursorLocationType.Nodes */);
        if (info.shape.hasValue) {
            const value = this.chunk.values[info.valueOffset + this.topLevelIndex * this.stride];
            // If mayContainCompressedIds is set, check if the value is a number (i.e. a compressed ID that needs decompression).
            if (info.shape.mayContainCompressedIds && typeof value === "number") {
                const idCompressor = this.chunk.idCompressor;
                (0, internal_1.assert)(idCompressor !== undefined, 0xcf1 /* chunk required idCompressor but did not provide it */);
                return idCompressor.decompress(value);
            }
            return value;
        }
        return undefined;
    }
}
//# sourceMappingURL=uniformChunk.js.map