"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.uniformChunkFromCursor = exports.insertValues = exports.chunkRange = exports.basicOnlyChunkPolicy = exports.defaultChunkPolicy = exports.tryShapeFromFieldSchema = exports.tryShapeFromSchema = exports.shapesFromSchema = exports.makePolicy = exports.basicChunkTree = exports.chunkField = exports.chunkTree = exports.Chunker = exports.polymorphic = exports.Polymorphic = exports.makeTreeChunker = void 0;
const core_utils_1 = require("@fluidframework/core-utils");
const core_1 = require("../../core");
const util_1 = require("../../util");
const multiplicity_1 = require("../multiplicity");
const chunk_1 = require("./chunk");
const basicChunk_1 = require("./basicChunk");
const uniformChunk_1 = require("./uniformChunk");
const sequenceChunk_1 = require("./sequenceChunk");
/**
 * Creates a ChunkPolicy which responds to schema changes.
 */
function makeTreeChunker(schema, policy) {
    return new Chunker(schema, policy, exports.defaultChunkPolicy.sequenceChunkInlineThreshold, exports.defaultChunkPolicy.sequenceChunkInlineThreshold, exports.defaultChunkPolicy.uniformChunkNodeCount, tryShapeFromSchema);
}
exports.makeTreeChunker = makeTreeChunker;
/**
 * Indicates that there are multiple possible `TreeShape` trees with a given type can have.
 *
 * @remarks
 * For example, a schema transitively containing a sequence field, optional field, or allowing multiple child types will be Polymorphic.
 * See `tryShapeFromSchema` for how to tell if a type is Polymorphic.
 *
 * TODO: cache some of the possible shapes here.
 */
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
class Polymorphic {
}
exports.Polymorphic = Polymorphic;
/**
 * See `Polymorphic`.
 * For now Polymorphic is stateless, so just use a singleton.
 */
exports.polymorphic = new Polymorphic();
class Chunker {
    constructor(schema, policy, sequenceChunkSplitThreshold, sequenceChunkInlineThreshold, uniformChunkNodeCount, 
    // eslint-disable-next-line @typescript-eslint/no-shadow
    tryShapeFromSchema) {
        this.schema = schema;
        this.policy = policy;
        this.sequenceChunkSplitThreshold = sequenceChunkSplitThreshold;
        this.sequenceChunkInlineThreshold = sequenceChunkInlineThreshold;
        this.uniformChunkNodeCount = uniformChunkNodeCount;
        this.tryShapeFromSchema = tryShapeFromSchema;
        /**
         * Cache for information about possible shapes for types.
         * Corresponds to the version of the schema in `schema`.
         * Cleared when `schema` changes.
         */
        this.typeShapes = new Map();
    }
    clone(schema) {
        // This does not preserve the cache.
        // This is probably fine, but is a potential way it could be optimized in the future (with care to ensure invalidation work properly).
        return new Chunker(schema, this.policy, this.sequenceChunkSplitThreshold, this.sequenceChunkInlineThreshold, this.uniformChunkNodeCount, this.tryShapeFromSchema);
    }
    shapeFromSchema(schema) {
        const cached = this.typeShapes.get(schema);
        if (cached !== undefined) {
            return cached;
        }
        this.unregisterSchemaCallback = this.schema.on("afterSchemaChange", () => this.schemaChanged());
        return this.tryShapeFromSchema(this.schema, this.policy, schema, this.typeShapes);
    }
    dispose() {
        // Remove subscription for changes via dependent.
        this.schemaChanged();
    }
    schemaChanged() {
        this.typeShapes.clear();
        if (this.unregisterSchemaCallback) {
            this.unregisterSchemaCallback();
            this.unregisterSchemaCallback = undefined;
        }
    }
}
exports.Chunker = Chunker;
/**
 * Get a TreeChunk for the current node (and its children) of cursor.
 * This will copy if needed, but add refs to existing chunks which hold the data.
 */
function chunkTree(cursor, policy) {
    return chunkRange(cursor, policy, 1, true)[0];
}
exports.chunkTree = chunkTree;
/**
 * Get a TreeChunk[] for the current field (and its children) of cursor.
 * This will copy if needed, but add refs to existing chunks which hold the data.
 */
function chunkField(cursor, policy) {
    const length = cursor.getFieldLength();
    const started = cursor.firstNode();
    (0, core_utils_1.assert)(started, 0x57c /* field to chunk should have at least one node */);
    return chunkRange(cursor, policy, length, false);
}
exports.chunkField = chunkField;
/**
 * Get a BasicChunk for the current node (and its children) of cursor.
 * This will copy if needed, and add refs to existing chunks which hold the data.
 */
function basicChunkTree(cursor, policy) {
    // symbol based fast path to check for BasicChunk:
    // return existing chunk with a increased ref count if possible.
    const chunk = (0, chunk_1.tryGetChunk)(cursor);
    if (chunk instanceof basicChunk_1.BasicChunk) {
        chunk.referenceAdded();
        return chunk;
    }
    return newBasicChunkTree(cursor, policy);
}
exports.basicChunkTree = basicChunkTree;
function makePolicy(policy) {
    const withDefaults = { ...exports.defaultChunkPolicy, ...policy };
    // TODO: move this to a top level policy validation
    (0, core_utils_1.assert)(withDefaults.sequenceChunkSplitThreshold >= 2, 0x57d /* sequenceChunkThreshold must be at least 2 */);
    return withDefaults;
}
exports.makePolicy = makePolicy;
function shapesFromSchema(schema, policy) {
    const shapes = new Map();
    for (const identifier of schema.nodeSchema.keys()) {
        tryShapeFromSchema(schema, policy, identifier, shapes);
    }
    return shapes;
}
exports.shapesFromSchema = shapesFromSchema;
/**
 * If `schema` has only one shape, return it.
 *
 * Note that this does not tolerate optional or sequence fields, nor does it optimize for patterns of specific values.
 */
function tryShapeFromSchema(schema, policy, type, shapes) {
    const cached = shapes.get(type);
    if (cached) {
        return cached;
    }
    const treeSchema = schema.nodeSchema.get(type) ?? (0, util_1.fail)("missing schema");
    if (treeSchema.mapFields !== undefined) {
        return exports.polymorphic;
    }
    const fieldsArray = [];
    for (const [key, field] of treeSchema.objectNodeFields) {
        const fieldShape = tryShapeFromFieldSchema(schema, policy, field, key, shapes);
        if (fieldShape === undefined) {
            return exports.polymorphic;
        }
        fieldsArray.push(fieldShape);
    }
    const shape = new uniformChunk_1.TreeShape(type, treeSchema.leafValue !== undefined, fieldsArray);
    shapes.set(type, shape);
    return shape;
}
exports.tryShapeFromSchema = tryShapeFromSchema;
/**
 * If `schema` has only one shape, return it.
 *
 * Note that this does not tolerate optional or sequence fields, nor does it optimize for patterns of specific values.
 */
function tryShapeFromFieldSchema(schema, policy, type, key, shapes) {
    const kind = policy.fieldKinds.get(type.kind.identifier) ?? (0, util_1.fail)("missing FieldKind");
    if (kind.multiplicity !== multiplicity_1.Multiplicity.Single) {
        return undefined;
    }
    if (type.types?.size !== 1) {
        return undefined;
    }
    const childType = [...type.types][0];
    const childShape = tryShapeFromSchema(schema, policy, childType, shapes);
    if (childShape instanceof Polymorphic) {
        return undefined;
    }
    return [key, childShape, 1];
}
exports.tryShapeFromFieldSchema = tryShapeFromFieldSchema;
/**
 * Default settings for use for {@link ChunkPolicy}.
 * Use `makeTreeChunker` to create a policy with the defaults, but leverages to schema.
 */
exports.defaultChunkPolicy = {
    // Currently ChunkedForest and BasicTreeCursor don't handle SequenceChunks very efficiently:
    // they likely add more overhead than they save for now, so don't create them.
    sequenceChunkSplitThreshold: Number.POSITIVE_INFINITY,
    sequenceChunkInlineThreshold: Number.POSITIVE_INFINITY,
    // Current UniformChunk handling doesn't scale well to large chunks, so set a modest size limit:
    uniformChunkNodeCount: 400,
    // Without knowing what the schema is, all shapes are possible.
    // Use `makeTreeChunker` to do better.
    shapeFromSchema: () => exports.polymorphic,
};
exports.basicOnlyChunkPolicy = {
    sequenceChunkSplitThreshold: Number.POSITIVE_INFINITY,
    sequenceChunkInlineThreshold: Number.POSITIVE_INFINITY,
    uniformChunkNodeCount: 0,
    shapeFromSchema: () => exports.polymorphic,
};
function newBasicChunkTree(cursor, policy) {
    return new basicChunk_1.BasicChunk(cursor.type, new Map((0, core_1.mapCursorFields)(cursor, () => [cursor.getFieldKey(), chunkField(cursor, policy)])), cursor.value);
}
function chunkRange(cursor, policy, length, skipLastNavigation) {
    (0, core_utils_1.assert)(cursor.mode === 0 /* CursorLocationType.Nodes */, 0x57e /* should be in nodes */);
    let output = [];
    let remaining = length;
    while (remaining > 0) {
        (0, core_utils_1.assert)(cursor.mode === 0 /* CursorLocationType.Nodes */, 0x57f /* should be in nodes */);
        const start = cursor.chunkStart;
        let reusedChunk = false;
        // symbol based fast path to check for chunk:
        // return existing chunk with a increased ref count if possible.
        if (start === cursor.fieldIndex) {
            const chunkLength = cursor.chunkLength;
            if (chunkLength <= remaining) {
                const chunk = (0, chunk_1.tryGetChunk)(cursor);
                if (chunk !== undefined) {
                    if (chunk instanceof sequenceChunk_1.SequenceChunk &&
                        chunk.subChunks.length <= policy.sequenceChunkInlineThreshold) {
                        // If sequence chunk, and its very short, inline it.
                        // Note that this is not recursive: there may be short sequences nested below this which are not inlined.
                        for (const subChunk of chunk.subChunks) {
                            subChunk.referenceAdded();
                            output.push(subChunk);
                        }
                    }
                    chunk.referenceAdded();
                    output.push(chunk);
                    remaining -= chunkLength;
                    reusedChunk = true;
                    let seek = chunkLength;
                    if (skipLastNavigation && remaining === 0) {
                        seek -= 1;
                    }
                    cursor.seekNodes(seek);
                }
            }
        }
        if (!reusedChunk) {
            (0, core_utils_1.assert)(cursor.mode === 0 /* CursorLocationType.Nodes */, 0x580 /* should be in nodes */);
            // TODO: if provided, use schema to consider using UniformChunks
            const type = cursor.type;
            const shape = policy.shapeFromSchema(type);
            if (shape instanceof uniformChunk_1.TreeShape) {
                const nodesPerTopLevelNode = shape.positions.length;
                const maxTopLevelLength = Math.ceil(nodesPerTopLevelNode / policy.uniformChunkNodeCount);
                const maxLength = Math.min(maxTopLevelLength, remaining);
                const newChunk = uniformChunkFromCursor(cursor, shape, maxLength, maxLength === remaining && skipLastNavigation);
                remaining -= newChunk.topLevelLength;
                output.push(newChunk);
            }
            else {
                // Slow path: copy tree into new basic chunk
                output.push(newBasicChunkTree(cursor, policy));
                remaining -= 1;
                if (!skipLastNavigation || remaining !== 0) {
                    cursor.nextNode();
                }
            }
        }
    }
    // TODO: maybe make a pass over output to coalesce UniformChunks and/or convert other formats to UniformChunks where possible.
    // If output is large, group it into a tree of sequence chunks.
    while (output.length > policy.sequenceChunkSplitThreshold) {
        const chunkCount = Math.ceil(output.length / policy.sequenceChunkSplitThreshold);
        const newOutput = [];
        // Rounding down, and add an extra item to some of the chunks.
        const chunkSize = Math.floor(output.length / chunkCount);
        // number of chunks to add an extra item to to make total line up.
        const extra = output.length % chunkCount;
        let previousEnd = 0;
        for (let index = 0; index < chunkCount; index++) {
            // If we are in the first `extra` items, add an extra to this chunk.
            const end = previousEnd + chunkSize + (index < extra ? 1 : 0);
            newOutput.push(new sequenceChunk_1.SequenceChunk(output.slice(previousEnd, end)));
            previousEnd = end;
        }
        (0, core_utils_1.assert)(previousEnd === output.length, 0x581 /* chunks should add up to total */);
        output = newOutput;
    }
    return output;
}
exports.chunkRange = chunkRange;
function insertValues(cursor, shape, values) {
    (0, core_utils_1.assert)(shape.type === cursor.type, 0x582 /* shape and type must match */);
    // TODO:Perf:
    // Fast path for already part of a uniform chunk with matching shape
    // Slow path: walk shape and cursor together, inserting values.
    if (shape.hasValue) {
        values.push(cursor.value);
    }
    for (const [key, childShape, length] of shape.fieldsArray) {
        cursor.enterField(key);
        let count = 0;
        for (let inNodes = cursor.firstNode(); inNodes; inNodes = cursor.nextNode()) {
            insertValues(cursor, childShape, values);
            count++;
        }
        cursor.exitField();
        (0, core_utils_1.assert)(length === count, 0x583 /* unexpected field length */);
    }
}
exports.insertValues = insertValues;
/**
 * Read up to `maxTopLevelLength` nodes from `cursor`, stopping when limit is hit or type of node changes.
 *
 * This requires that the all trees with matching type match the provided shape.
 * This cannot be used if other shapes are possible for this type.
 *
 * If this stops early due to the type changing, `skipLastNavigation` is not involved:
 * `skipLastNavigation` only determines if the cursor will be left on the node after the last one (possibly exiting the field)
 * if the full length is used.
 */
function uniformChunkFromCursor(cursor, shape, maxTopLevelLength, skipLastNavigation) {
    // TODO:
    // This could have a fast path for consuming already uniformly chunked data with matching shape.
    const values = [];
    let topLevelLength = 1;
    while (topLevelLength <= maxTopLevelLength) {
        insertValues(cursor, shape, values);
        if (topLevelLength === maxTopLevelLength) {
            if (!skipLastNavigation) {
                cursor.nextNode();
            }
            break;
        }
        cursor.nextNode();
        if (cursor.type !== shape.type) {
            break;
        }
        topLevelLength += 1;
    }
    return new uniformChunk_1.UniformChunk(shape.withTopLevelLength(topLevelLength), values);
}
exports.uniformChunkFromCursor = uniformChunkFromCursor;
//# sourceMappingURL=chunkTree.js.map