/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, unreachableCase, fail } from "@fluidframework/core-utils/internal";
import { forEachNode, } from "../../../core/index.js";
import { getOrCreate } from "../../../util/index.js";
import { Shape as ShapeGeneric, updateShapesAndIdentifiersEncoding, } from "./chunkEncodingGeneric.js";
import { SpecialField, supportsIncrementalEncoding, } from "./format/index.js";
/**
 * Encode data from `FieldBatch` into an `EncodedFieldBatch`.
 *
 * Optimized for encoded size and encoding performance.
 *
 * Most of the compression strategy comes from the policy provided via `context`.
 */
export function compressedEncode(fieldBatch, context) {
    const batchBuffer = [];
    // Populate buffer, including shape and identifier references
    for (const cursor of fieldBatch) {
        const buffer = [];
        anyFieldEncoder.encodeField(cursor, context, buffer);
        batchBuffer.push(buffer);
    }
    return updateShapesAndIdentifiersEncoding(context.version, batchBuffer);
}
/**
 * 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.
 */
export function asFieldEncoder(encoder) {
    return {
        encodeField(cursor, context, outputBuffer) {
            forEachNode(cursor, () => encoder.encodeNode(cursor, context, outputBuffer));
        },
        shape: encoder.shape,
    };
}
/**
 * Adapt a {@link NodeEncoder} to a {@link NodesEncoder} which invokes `encoder` once.
 */
export function asNodesEncoder(encoder) {
    return {
        encodeNodes(cursor, context, outputBuffer) {
            encoder.encodeNode(cursor, context, outputBuffer);
            cursor.nextNode();
        },
        shape: encoder.shape,
    };
}
/**
 * Encodes a chunk with {@link EncodedAnyShape} by prefixing the data with its shape.
 */
export class AnyShape extends ShapeGeneric {
    constructor() {
        super();
    }
    encodeShape(identifiers, shapes) {
        const encodedAnyShape = 0;
        return { d: encodedAnyShape };
    }
    countReferencedShapesAndIdentifiers(identifiers, shapeDiscovered) { }
    static encodeField(cursor, context, outputBuffer, encoder) {
        outputBuffer.push(encoder.shape);
        encoder.encodeField(cursor, context, outputBuffer);
    }
    static encodeNode(cursor, context, outputBuffer, encoder) {
        outputBuffer.push(encoder.shape);
        encoder.encodeNode(cursor, context, outputBuffer);
    }
    static encodeNodes(cursor, context, outputBuffer, encoder) {
        outputBuffer.push(encoder.shape);
        encoder.encodeNodes(cursor, context, outputBuffer);
    }
}
AnyShape.instance = new AnyShape();
/**
 * Encodes a single node polymorphically.
 */
export const anyNodeEncoder = {
    encodeNode(cursor, context, outputBuffer) {
        // TODO: Fast path uniform chunk content.
        const nodeEncoder = context.nodeEncoderFromSchema(cursor.type);
        AnyShape.encodeNode(cursor, context, outputBuffer, nodeEncoder);
    },
    shape: AnyShape.instance,
};
/**
 * Encodes a field polymorphically.
 */
export const anyFieldEncoder = {
    encodeField(cursor, context, outputBuffer) {
        // TODO: Fast path uniform chunks.
        if (cursor.getFieldLength() === 0) {
            const shape = InlineArrayEncoder.empty;
            AnyShape.encodeField(cursor, context, outputBuffer, shape);
        }
        else if (cursor.getFieldLength() === 1) {
            // Fast path chunk of size one size one at least: skip nested array.
            cursor.enterNode(0);
            anyNodeEncoder.encodeNode(cursor, context, 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 = context.nestedArrayEncoder(anyNodeEncoder);
            AnyShape.encodeField(cursor, context, outputBuffer, shape);
        }
    },
    shape: AnyShape.instance,
};
/**
 * Encodes a chunk using {@link EncodedInlineArrayShape}.
 * @remarks
 * The fact this is also a Shape is an implementation detail of the encoder: that allows the shape it uses to be itself,
 * which is an easy way to keep all the related code together without extra objects.
 */
export class InlineArrayEncoder extends ShapeGeneric {
    /**
     * @param length - number of invocations of `inner`.
     */
    constructor(length, inner) {
        super();
        this.length = length;
        this.inner = inner;
    }
    encodeNodes(cursor, context, 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, context, outputBuffer);
        }
    }
    encodeField(cursor, context, outputBuffer) {
        // Its possible individual items from this array encode multiple nodes, so don't assume === here.
        assert(cursor.getFieldLength() >= this.length, 0x73c /* unexpected length for fixed length array */);
        cursor.firstNode();
        this.encodeNodes(cursor, context, outputBuffer);
        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) ?? fail(0xb4e /* missing shape */),
            },
        };
    }
    countReferencedShapesAndIdentifiers(identifiers, shapeDiscovered) {
        shapeDiscovered(this.inner.shape);
    }
    get shape() {
        return this;
    }
}
InlineArrayEncoder.empty = new InlineArrayEncoder(0, {
    get shape() {
        // Not actually used, makes count work without adding an additional shape.
        return InlineArrayEncoder.empty;
    },
    encodeNodes(cursor, context, outputBuffer) {
        fail(0xb4d /* Empty array should not encode any nodes */);
    },
});
/**
 * Encodes the shape for a nested array as {@link EncodedNestedArrayShape} shape.
 */
export class NestedArrayShape extends ShapeGeneric {
    /**
     * @param innerShape - The shape of each item in this nested array.
     */
    constructor(innerShape) {
        super();
        this.innerShape = innerShape;
    }
    encodeShape(identifiers, shapes) {
        const shape = shapes.valueToIndex.get(this.innerShape) ??
            fail(0xb4f /* index for shape not found in table */);
        return {
            a: shape,
        };
    }
    countReferencedShapesAndIdentifiers(identifiers, shapeDiscovered) {
        shapeDiscovered(this.innerShape);
    }
}
/**
 * Encodes a field as a nested array with the {@link EncodedNestedArrayShape} shape.
 * @remarks
 * The fact this is also exposes a Shape is an implementation detail: it allows the shape it uses to be itself
 * which is an easy way to keep all the related code together without extra objects.
 */
export class NestedArrayEncoder {
    constructor(innerEncoder, shape = new NestedArrayShape(innerEncoder.shape)) {
        this.innerEncoder = innerEncoder;
        this.shape = shape;
    }
    encodeField(cursor, context, outputBuffer) {
        const buffer = [];
        let allNonZeroSize = true;
        const length = cursor.getFieldLength();
        forEachNode(cursor, () => {
            const before = buffer.length;
            this.innerEncoder.encodeNode(cursor, context, 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 {
            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);
        }
    }
}
/**
 * Encodes the shape for an incremental chunk as {@link EncodedIncrementalChunkShape} shape.
 */
export class IncrementalChunkShape extends ShapeGeneric {
    encodeShape(identifiers, shapes) {
        return {
            e: 0 /* EncodedIncrementalChunkShape */,
        };
    }
    countReferencedShapesAndIdentifiers(identifiers, shapeDiscovered) { }
    get shape() {
        return this;
    }
}
/**
 * Encodes an incremental field whose tree chunks are encoded separately and referenced by their {@link ChunkReferenceId}.
 * The shape of the content of this field is {@link NestedArrayShape}.
 * The inner items of the array have shape {@link IncrementalChunkShape} and are {@link ChunkReferenceId}s
 * of the encoded chunks.
 */
export const incrementalFieldEncoder = {
    encodeField(cursor, context, outputBuffer) {
        assert(context.incrementalEncoder !== undefined, 0xc88 /* incremental encoder must be defined to use incrementalFieldEncoder */);
        assert(supportsIncrementalEncoding(context.version), 0xca1 /* Unsupported FieldBatchFormatVersion for incremental encoding; must be v2 or higher */);
        const chunkReferenceIds = context.incrementalEncoder.encodeIncrementalField(cursor, (chunk) => compressedEncode([chunk.cursor()], context));
        outputBuffer.push(chunkReferenceIds);
    },
    shape: new NestedArrayShape(new IncrementalChunkShape() /* innerShape */),
};
/**
 * Encode `value` with `shape` into `outputBuffer`.
 *
 * Requires that `value` is compatible with `shape`.
 */
export function encodeValue(value, shape, outputBuffer) {
    if (shape === undefined) {
        if (value === undefined) {
            outputBuffer.push(false);
        }
        else {
            outputBuffer.push(true, value);
        }
    }
    else {
        if (shape === true) {
            assert(value !== undefined, 0x78d /* required value must not be missing */);
            outputBuffer.push(value);
        }
        else if (shape === false) {
            assert(value === undefined, 0x73f /* incompatible value shape: expected no value */);
        }
        else if (Array.isArray(shape)) {
            assert(shape.length === 1, 0x740 /* expected a single constant for value */);
        }
        else if (shape === SpecialField.Identifier) {
            // This case is a special case handling the encoding of identifier fields.
            assert(value !== undefined, 0x998 /* required value must not be missing */);
            outputBuffer.push(value);
        }
        else {
            // EncodedCounter case:
            unreachableCase(shape, "Encoding values as deltas is not yet supported");
        }
    }
}
/**
 * Provides common contextual information during encoding, like schema and policy settings.
 * Also, provides a cache to avoid duplicating equivalent shapes during a batch of encode operations.
 * @remarks
 * To avoid Shape duplication, any Shapes used in the encoding should either be:
 * - Singletons defined in a static scope.
 * - Cached in this object for future reuse such that all equivalent Shapes are deduplicated.
 */
export class EncoderContext {
    constructor(nodeEncoderFromPolicy, fieldEncoderFromPolicy, fieldShapes, idCompressor, 
    /**
     * To be used to encode incremental chunks, if any.
     * @remarks
     * See {@link IncrementalEncoder} for more information.
     */
    incrementalEncoder, version) {
        this.nodeEncoderFromPolicy = nodeEncoderFromPolicy;
        this.fieldEncoderFromPolicy = fieldEncoderFromPolicy;
        this.fieldShapes = fieldShapes;
        this.idCompressor = idCompressor;
        this.incrementalEncoder = incrementalEncoder;
        this.version = version;
        this.nodeEncodersFromSchema = new Map();
        this.nestedArrayEncoders = new Map();
    }
    nodeEncoderFromSchema(schemaName) {
        return getOrCreate(this.nodeEncodersFromSchema, schemaName, () => this.nodeEncoderFromPolicy(this, schemaName));
    }
    fieldEncoderFromSchema(fieldSchema) {
        return new LazyFieldEncoder(this, fieldSchema, this.fieldEncoderFromPolicy);
    }
    nestedArrayEncoder(inner) {
        return getOrCreate(this.nestedArrayEncoders, inner, () => new NestedArrayEncoder(inner));
    }
}
class LazyFieldEncoder {
    constructor(nodeBuilder, fieldSchema, fieldEncoderFromPolicy) {
        this.nodeBuilder = nodeBuilder;
        this.fieldSchema = fieldSchema;
        this.fieldEncoderFromPolicy = fieldEncoderFromPolicy;
    }
    encodeField(cursor, context, outputBuffer) {
        this.encoder.encodeField(cursor, context, outputBuffer);
    }
    get encoder() {
        // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- using ??= could change behavior if value is falsy
        if (this.encoderLazy === undefined) {
            this.encoderLazy = this.fieldEncoderFromPolicy(this.nodeBuilder, this.fieldSchema);
        }
        return this.encoderLazy;
    }
    get shape() {
        return this.encoder.shape;
    }
}
//# sourceMappingURL=compressedEncode.js.map