"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.readStreamStream = exports.readStreamValue = exports.readStreamBoolean = exports.readStreamNumber = exports.readStream = exports.getChecked = exports.jsonMinimizingFilter = exports.Counter = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const index_js_1 = require("../../../util/index.js");
const valueUtilities_js_1 = require("../../valueUtilities.js");
/**
 * 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}.
 */
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])),
        };
    }
}
exports.Counter = Counter;
/**
 * 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.
 */
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;
}
exports.jsonMinimizingFilter = jsonMinimizingFilter;
/**
 * Read from an array, but error if index is not valid.
 */
function getChecked(data, index) {
    (0, index_js_1.assertValidIndex)(index, data);
    return data[index] ?? (0, internal_1.oob)();
}
exports.getChecked = getChecked;
/**
 * Read one item from the stream, advancing the stream offset.
 */
function readStream(stream) {
    const content = getChecked(stream.data, stream.offset);
    stream.offset++;
    return content;
}
exports.readStream = readStream;
/**
 * Read one number from the stream, advancing the stream offset.
 */
function readStreamNumber(stream) {
    const content = readStream(stream);
    (0, internal_1.assert)(typeof content === "number", 0x730 /* expected number in stream */);
    return content;
}
exports.readStreamNumber = readStreamNumber;
/**
 * Read one boolean from the stream, advancing the stream offset.
 */
function readStreamBoolean(stream) {
    const content = readStream(stream);
    (0, internal_1.assert)(typeof content === "boolean", 0x731 /* expected boolean in stream */);
    return content;
}
exports.readStreamBoolean = readStreamBoolean;
/**
 * Read one TreeValue from the stream, advancing the stream offset.
 */
function readStreamValue(stream) {
    const content = readStream(stream);
    (0, valueUtilities_js_1.assertAllowedValue)(content);
    return content;
}
exports.readStreamValue = readStreamValue;
/**
 * Read one nested array from the stream, advancing the stream offset.
 *
 * @returns the nested array as a stream.
 */
function readStreamStream(stream) {
    const content = readStream(stream);
    (0, internal_1.assert)(Array.isArray(content), 0x732 /* expected Array in stream */);
    return { data: content, offset: 0 };
}
exports.readStreamStream = readStreamStream;
//# sourceMappingURL=chunkCodecUtilities.js.map