"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.EncoderCache = exports.encodeValue = exports.NestedArrayShape = exports.InlineArrayShape = exports.anyFieldEncoder = exports.anyNodeEncoder = exports.AnyShape = exports.asNodesEncoder = exports.asFieldEncoder = exports.compressedEncode = 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 chunkEncodingGeneric_js_1 = require("./chunkEncodingGeneric.js");
const format_js_1 = require("./format.js");
/**
 * Encode data from `FieldBatch` into an `EncodedChunk`.
 *
 * Optimized for encoded size and encoding performance.
 *
 * Most of the compression strategy comes from the policy provided via `cache`.
 */
function compressedEncode(fieldBatch, cache) {
    const batchBuffer = [];
    // Populate buffer, including shape and identifier references
    for (const cursor of fieldBatch) {
        const buffer = [];
        exports.anyFieldEncoder.encodeField(cursor, cache, buffer);
        batchBuffer.push(buffer);
    }
    return (0, chunkEncodingGeneric_js_1.updateShapesAndIdentifiersEncoding)(format_js_1.version, batchBuffer);
}
exports.compressedEncode = compressedEncode;
/**
 * Makes a {@link FieldEncoder} which runs `encoder` on every node in the field.
 * This does not encode the number nodes: the user of this may need to encode that elsewhere.
 */
function asFieldEncoder(encoder) {
    return {
        encodeField(cursor, shapes, outputBuffer) {
            (0, index_js_1.forEachNode)(cursor, () => encoder.encodeNode(cursor, shapes, outputBuffer));
        },
        shape: encoder.shape,
    };
}
exports.asFieldEncoder = asFieldEncoder;
/**
 * Adapt a {@link NodeEncoder} to a {@link NodesEncoder} which invokes `encoder` once.
 */
function asNodesEncoder(encoder) {
    return {
        encodeNodes(cursor, shapes, outputBuffer) {
            encoder.encodeNode(cursor, shapes, outputBuffer);
            cursor.nextNode();
        },
        shape: encoder.shape,
    };
}
exports.asNodesEncoder = asNodesEncoder;
/**
 * Encodes a chunk with {@link EncodedAnyShape} by prefixing the data with its shape.
 */
class AnyShape extends chunkEncodingGeneric_js_1.Shape {
    constructor() {
        super();
    }
    encodeShape(identifiers, shapes) {
        const encodedAnyShape = 0;
        return { d: encodedAnyShape };
    }
    countReferencedShapesAndIdentifiers(identifiers, shapeDiscovered) { }
    static encodeField(cursor, cache, outputBuffer, shape) {
        outputBuffer.push(shape.shape);
        shape.encodeField(cursor, cache, outputBuffer);
    }
    static encodeNode(cursor, cache, outputBuffer, shape) {
        outputBuffer.push(shape.shape);
        shape.encodeNode(cursor, cache, outputBuffer);
    }
    static encodeNodes(cursor, cache, outputBuffer, shape) {
        outputBuffer.push(shape.shape);
        shape.encodeNodes(cursor, cache, outputBuffer);
    }
}
exports.AnyShape = AnyShape;
AnyShape.instance = new AnyShape();
/**
 * Encodes a single node polymorphically.
 */
exports.anyNodeEncoder = {
    encodeNode(cursor, cache, outputBuffer) {
        // TODO: Fast path uniform chunk content.
        const shape = cache.shapeFromTree(cursor.type);
        AnyShape.encodeNode(cursor, cache, outputBuffer, shape);
    },
    shape: AnyShape.instance,
};
/**
 * Encodes a field polymorphically.
 */
exports.anyFieldEncoder = {
    encodeField(cursor, cache, outputBuffer) {
        // TODO: Fast path uniform chunks.
        if (cursor.getFieldLength() === 0) {
            const shape = InlineArrayShape.empty;
            AnyShape.encodeField(cursor, cache, outputBuffer, shape);
        }
        else if (cursor.getFieldLength() === 1) {
            // Fast path chunk of size one size one at least: skip nested array.
            cursor.enterNode(0);
            exports.anyNodeEncoder.encodeNode(cursor, cache, outputBuffer);
            cursor.exitNode();
        }
        else {
            // TODO: more efficient encoding for common cases.
            // Could try to find more specific shape compatible with all children than `anyNodeEncoder`.
            const shape = cache.nestedArray(exports.anyNodeEncoder);
            AnyShape.encodeField(cursor, cache, outputBuffer, shape);
        }
    },
    shape: AnyShape.instance,
};
/**
 * Encodes a chunk using {@link EncodedInlineArray}.
 */
class InlineArrayShape extends chunkEncodingGeneric_js_1.Shape {
    /**
     * @param length - number of invocations of `inner`.
     */
    constructor(length, inner) {
        super();
        this.length = length;
        this.inner = inner;
    }
    encodeNodes(cursor, shapes, outputBuffer) {
        // Linter is wrong about this loop being for-of compatible.
        // eslint-disable-next-line @typescript-eslint/prefer-for-of
        for (let index = 0; index < this.length; index++) {
            this.inner.encodeNodes(cursor, shapes, outputBuffer);
        }
    }
    encodeField(cursor, shapes, outputBuffer) {
        // Its possible individual items from this array encode multiple nodes, so don't assume === here.
        (0, internal_1.assert)(cursor.getFieldLength() >= this.length, 0x73c /* unexpected length for fixed length array */);
        cursor.firstNode();
        this.encodeNodes(cursor, shapes, outputBuffer);
        (0, internal_1.assert)(cursor.mode === 1 /* CursorLocationType.Fields */, 0x73d /* should return to fields mode when finished encoding */);
    }
    encodeShape(identifiers, shapes) {
        return {
            b: {
                length: this.length,
                shape: shapes.valueToIndex.get(this.inner.shape) ?? (0, internal_1.fail)(0xb4e /* missing shape */),
            },
        };
    }
    countReferencedShapesAndIdentifiers(identifiers, shapeDiscovered) {
        shapeDiscovered(this.inner.shape);
    }
    get shape() {
        return this;
    }
}
exports.InlineArrayShape = InlineArrayShape;
InlineArrayShape.empty = new InlineArrayShape(0, {
    get shape() {
        // Not actually used, makes count work without adding an additional shape.
        return InlineArrayShape.empty;
    },
    encodeNodes(cursor, shapes, outputBuffer) {
        (0, internal_1.fail)(0xb4d /* Empty array should not encode any nodes */);
    },
});
/**
 * Encodes a field as a nested array with the {@link EncodedNestedArray} shape.
 */
class NestedArrayShape extends chunkEncodingGeneric_js_1.Shape {
    constructor(inner) {
        super();
        this.inner = inner;
        this.shape = this;
    }
    encodeField(cursor, cache, outputBuffer) {
        const buffer = [];
        let allNonZeroSize = true;
        const length = cursor.getFieldLength();
        (0, index_js_1.forEachNode)(cursor, () => {
            const before = buffer.length;
            this.inner.encodeNode(cursor, cache, buffer);
            allNonZeroSize &&= buffer.length - before !== 0;
        });
        if (buffer.length === 0) {
            // This relies on the number of inner chunks being the same as the number of nodes.
            // If making inner a `NodesEncoder`, this code will have to be adjusted accordingly.
            outputBuffer.push(length);
        }
        else {
            (0, internal_1.assert)(allNonZeroSize, 0x73e /* either all or none of the members of a nested array must be 0 sized, or there is no way the decoder could process the content correctly. */);
            outputBuffer.push(buffer);
        }
    }
    encodeShape(identifiers, shapes) {
        const shape = shapes.valueToIndex.get(this.inner.shape) ??
            (0, internal_1.fail)(0xb4f /* index for shape not found in table */);
        return {
            a: shape,
        };
    }
    countReferencedShapesAndIdentifiers(identifiers, shapeDiscovered) {
        shapeDiscovered(this.inner.shape);
    }
}
exports.NestedArrayShape = NestedArrayShape;
/**
 * Encode `value` with `shape` into `outputBuffer`.
 *
 * Requires that `value` is compatible with `shape`.
 */
function encodeValue(value, shape, outputBuffer) {
    if (shape === undefined) {
        if (value !== undefined) {
            outputBuffer.push(true, value);
        }
        else {
            outputBuffer.push(false);
        }
    }
    else {
        if (shape === true) {
            (0, internal_1.assert)(value !== undefined, 0x78d /* required value must not be missing */);
            outputBuffer.push(value);
        }
        else if (shape === false) {
            (0, internal_1.assert)(value === undefined, 0x73f /* incompatible value shape: expected no value */);
        }
        else if (Array.isArray(shape)) {
            (0, internal_1.assert)(shape.length === 1, 0x740 /* expected a single constant for value */);
        }
        else if (shape === format_js_1.SpecialField.Identifier) {
            // This case is a special case handling the encoding of identifier fields.
            (0, internal_1.assert)(value !== undefined, 0x998 /* required value must not be missing */);
            outputBuffer.push(value);
        }
        else {
            // EncodedCounter case:
            (0, internal_1.unreachableCase)(shape, "Encoding values as deltas is not yet supported");
        }
    }
}
exports.encodeValue = encodeValue;
class EncoderCache {
    constructor(treeEncoder, fieldEncoder, fieldShapes, idCompressor) {
        this.treeEncoder = treeEncoder;
        this.fieldEncoder = fieldEncoder;
        this.fieldShapes = fieldShapes;
        this.idCompressor = idCompressor;
        this.shapesFromSchema = new Map();
        this.nestedArrays = new Map();
    }
    shapeFromTree(schemaName) {
        return (0, index_js_2.getOrCreate)(this.shapesFromSchema, schemaName, () => this.treeEncoder(this, schemaName));
    }
    nestedArray(inner) {
        return (0, index_js_2.getOrCreate)(this.nestedArrays, inner, () => new NestedArrayShape(inner));
    }
    shapeFromField(field) {
        return new LazyFieldEncoder(this, field, this.fieldEncoder);
    }
}
exports.EncoderCache = EncoderCache;
class LazyFieldEncoder {
    constructor(cache, field, fieldEncoder) {
        this.cache = cache;
        this.field = field;
        this.fieldEncoder = fieldEncoder;
    }
    encodeField(cursor, cache, outputBuffer) {
        this.encoder.encodeField(cursor, cache, outputBuffer);
    }
    get encoder() {
        if (this.encoderLazy === undefined) {
            this.encoderLazy = this.fieldEncoder(this.cache, this.field);
        }
        return this.encoderLazy;
    }
    get shape() {
        return this.encoder.shape;
    }
}
//# sourceMappingURL=compressedEncode.js.map