/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/core-utils";
import { assertValidIndex, fail, objectToMap } from "../../../util";
import { assertAllowedValue } from "../../contextuallyTyped";
/**
 * Utilities related to chunk encoding and decoding that do not depend on specific chunk types or formats.
 */
/**
 * Counts usages of some `T`, then generates tables for replacing those `T` values with small numbers.
 *
 * Can be used to deduplicate objects when encoding via {@link https://go-compression.github.io/algorithms/dictionary/ | Dictionary Coding}.
 */
export class Counter {
    constructor() {
        this.counts = new Map();
    }
    add(t, count = 1) {
        const old = this.counts.get(t) ?? 0;
        this.counts.set(t, old + count);
    }
    /**
     * Generate a table which can be used to deduplicate the items added via `add`.
     * Table is sorted from most used to least used so that more commonly added items will get smaller indexes.
     *
     * @param filter - determines which items should be included in the table.
     */
    buildTable(filter = () => true) {
        const data = [...this.counts.keys()];
        // Sort in descending order by count, giving priority (smaller indexes) to more commonly used values.
        data.sort((a, b) => (this.counts.get(b) ?? 0) - (this.counts.get(a) ?? 0));
        // Since the index needed is the output index not the input one, data.filter doesn't quite work here.
        const filtered = [];
        for (const t of data) {
            const include = filter(t, filtered.length, this.counts.get(t) ?? 0);
            if (include) {
                filtered.push(t);
            }
        }
        return {
            indexToValue: filtered,
            valueToIndex: new Map(filtered.map((t, index) => [t, index])),
        };
    }
}
/**
 * Filter for use with Counter.buildTable that minimizes the size of the produced JSON.
 *
 * @remarks
 * This minimizes the number of UTF-16 code units in the produced JSON.
 * This is not ideal: minimizing UTF-8 bytes would be better.
 * For ascii text these are the same, and in worst cases they correlate pretty well (accurate to within a small constant factor).
 * This is thus good enough for a heuristic.
 *
 * Also the use of this filter doesn't take into account the length cost of keeping a value in the table possibly
 * making some of the entries at higher indexes in the table get a larger number.
 * For example, if keeping an entry at index 9 saves one character this will keep it,
 * even if that means the next entry (at index 10) loses more than one character due to 10 being two digits instead of the one digit it would have been at index 9.
 * This means that this filter is not guaranteed to be optimal, but it should always be quite close.
 */
export function jsonMinimizingFilter(s, value, count) {
    // The most practical way to compute how long s will be with quoting and escaping
    // is to actually quote and escape it with JSON.stringify:
    const quotedAndEscaped = JSON.stringify(s);
    // Account for count instances of value, and one instance of a `,s` which would go in the table.
    const lengthUsingTable = String(value).length * count + quotedAndEscaped.length + 1;
    // Account for count instances of s.
    const lengthWithoutTable = quotedAndEscaped.length * count;
    // Break ties to not use the table to avoid needing a lookup,
    // and save table entries for other uses.
    return lengthUsingTable < lengthWithoutTable;
}
/**
 * Read from an array, but error if index is not valid.
 */
export function getChecked(data, index) {
    assertValidIndex(index, data);
    return data[index];
}
/**
 * Read one item from the stream, advancing the stream offset.
 */
export function readStream(stream) {
    const content = getChecked(stream.data, stream.offset);
    stream.offset++;
    return content;
}
/**
 * Read one number from the stream, advancing the stream offset.
 */
export function readStreamNumber(stream) {
    const content = readStream(stream);
    assert(typeof content === "number", 0x730 /* expected number in stream */);
    return content;
}
/**
 * Read one boolean from the stream, advancing the stream offset.
 */
export function readStreamBoolean(stream) {
    const content = readStream(stream);
    assert(typeof content === "boolean", 0x731 /* expected boolean in stream */);
    return content;
}
/**
 * Read one TreeValue from the stream, advancing the stream offset.
 */
export function readStreamValue(stream) {
    const content = readStream(stream);
    assertAllowedValue(content);
    return content;
}
/**
 * Read one nested array from the stream, advancing the stream offset.
 *
 * @returns the nested array as a stream.
 */
export function readStreamStream(stream) {
    const content = readStream(stream);
    assert(Array.isArray(content), 0x732 /* expected Array in stream */);
    return { data: content, offset: 0 };
}
/**
 * Applies a function to the content of a [discriminated union](https://en.wikipedia.org/wiki/Tagged_union)
 * where the function to apply depends on the which value from the union it holds.
 *
 * This uses a rather non-standard encoding of the union where it is an object with many differently named optional fields,
 * and which of the fields is populated determines the content type.
 * This union encoding has the advantage that schema validation (such as that implemented by TypeBox) can validate the data.
 * Other encodings, such as an untagged-union, then tagging the content types with a marker enum require the schema validator to disambiguate the union members,
 * inferring that the enum can be use to differentiate them.
 */
export class DiscriminatedUnionDispatcher {
    constructor(library) {
        this.library = objectToMap(library);
    }
    dispatch(union, ...args) {
        const keys = Reflect.ownKeys(union);
        assert(keys.length === 1, 0x733 /* discriminated union type should have exactly one member */);
        const key = keys[0];
        const value = union[key];
        const factory = this.library.get(key) ?? fail("missing function for union member");
        const result = factory(value, ...args);
        return result;
    }
}
//# sourceMappingURL=chunkCodecUtilities.js.map