/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, unreachableCase } from "@fluidframework/core-utils";
import { forEachNode, } from "../../../core";
import { fail, getOrCreate } from "../../../util";
import { Shape as ShapeGeneric, handleShapesAndIdentifiers, } from "./chunkEncodingGeneric";
import { version, } from "./format";
/**
 * Encode data from `cursor` in into an `EncodedChunk`.
 *
 * Optimized for encoded size and encoding performance.
 *
 * Most of the compression strategy comes from the policy provided via `cache`.
 */
export function compressedEncode(cursor, cache) {
    const buffer = [];
    // Populate buffer, including shape and identifier references
    anyFieldEncoder.encodeField(cursor, cache, buffer);
    return handleShapesAndIdentifiers(version, buffer);
}
/**
 * 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, shapes, outputBuffer) {
            forEachNode(cursor, () => encoder.encodeNode(cursor, shapes, outputBuffer));
        },
        shape: encoder.shape,
    };
}
/**
 * Adapt a {@link NodeEncoder} to a {@link NodesEncoder} which invokes `encoder` once.
 */
export function asNodesEncoder(encoder) {
    return {
        encodeNodes(cursor, shapes, outputBuffer) {
            encoder.encodeNode(cursor, shapes, 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 };
    }
    count(identifiers, shapes) { }
    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);
    }
}
AnyShape.instance = new AnyShape();
/**
 * Encodes a single node polymorphically.
 */
export const 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.
 */
export const 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);
            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(anyNodeEncoder);
            AnyShape.encodeField(cursor, cache, outputBuffer, shape);
        }
    },
    shape: AnyShape.instance,
};
/**
 * Encodes a chunk using {@link EncodedInlineArray}.
 */
export class InlineArrayShape extends ShapeGeneric {
    /**
     * @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.
        assert(cursor.getFieldLength() >= this.length, 0x73c /* unexpected length for fixed length array */);
        cursor.firstNode();
        this.encodeNodes(cursor, shapes, 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("missing shape"),
            },
        };
    }
    count(identifiers, shapes) {
        shapes(this.inner.shape);
    }
    get shape() {
        return this;
    }
}
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) {
        fail("Empty array should not encode any nodes");
    },
});
/**
 * Encodes a field as a nested array with the {@link EncodedNestedArray} shape.
 */
export class NestedArrayShape extends ShapeGeneric {
    constructor(inner) {
        super();
        this.inner = inner;
        this.shape = this;
    }
    encodeField(cursor, cache, outputBuffer) {
        const buffer = [];
        let allNonZeroSize = true;
        const length = cursor.getFieldLength();
        forEachNode(cursor, () => {
            const before = buffer.length;
            this.inner.encodeNode(cursor, cache, buffer);
            allNonZeroSize && (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);
        }
    }
    encodeShape(identifiers, shapes) {
        const shape = shapes.valueToIndex.get(this.inner.shape) ?? fail("index for shape not found in table");
        return {
            a: shape,
        };
    }
    count(identifiers, shapes) {
        shapes(this.inner.shape);
    }
}
/**
 * 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(true, value);
        }
        else {
            outputBuffer.push(false);
        }
    }
    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 {
            // EncodedCounter case:
            unreachableCase(shape, "Encoding values as deltas is not yet supported");
        }
    }
}
export class EncoderCache {
    constructor(treeEncoder, fieldEncoder, fieldShapes) {
        this.treeEncoder = treeEncoder;
        this.fieldEncoder = fieldEncoder;
        this.fieldShapes = fieldShapes;
        this.shapesFromSchema = new Map();
        this.nestedArrays = new Map();
    }
    shapeFromTree(schemaName) {
        return getOrCreate(this.shapesFromSchema, schemaName, () => this.treeEncoder(this, schemaName));
    }
    nestedArray(inner) {
        return getOrCreate(this.nestedArrays, inner, () => new NestedArrayShape(inner));
    }
    shapeFromField(field) {
        return new LazyFieldEncoder(this, field, this.fieldEncoder);
    }
}
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