"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
const assert_1 = require("assert");
const benchmark_1 = require("@fluid-tools/benchmark");
const attributionCollection_1 = require("../attributionCollection.cjs");
const textSegment_1 = require("../textSegment.cjs");
const mergeTreeNodes_1 = require("../mergeTreeNodes.cjs");
const collections_1 = require("../collections/index.cjs");
function getCollectionSizes(ctor, baseSuiteType) {
    const singleKeyCollection = new ctor(5, { type: "op", seq: 42 });
    const tenKeyCollection = new ctor(2, { type: "op", seq: 0 });
    for (let i = 1; i < 10; i++) {
        tenKeyCollection.append(new ctor(3 * i, { type: "op", seq: i }));
    }
    const maxSizeCollection = new ctor(1, { type: "op", seq: 0 });
    for (let i = 1; i < textSegment_1.TextSegmentGranularity; i++) {
        maxSizeCollection.append(new ctor(1, { type: "op", seq: i }));
    }
    return [
        { name: "one key", collection: singleKeyCollection, type: benchmark_1.BenchmarkType.Diagnostic },
        { name: "ten keys", collection: tenKeyCollection, type: baseSuiteType },
        { name: "maximum keys", collection: maxSizeCollection, type: benchmark_1.BenchmarkType.Diagnostic },
    ];
}
function runAttributionCollectionSuite(ctor, suiteBaseType) {
    const collectionTestCases = getCollectionSizes(ctor, suiteBaseType);
    for (const { name, collection, type } of collectionTestCases) {
        describe(`using a collection with ${name}`, () => {
            const { length } = collection;
            (0, benchmark_1.benchmark)({
                title: "getAtOffset at the start",
                benchmarkFn: () => collection.getAtOffset(0),
                type,
            });
            (0, benchmark_1.benchmark)({
                title: "getAtOffset at the end",
                benchmarkFn: () => collection.getAtOffset(length - 1),
                type,
            });
            (0, benchmark_1.benchmark)({
                title: "getAtOffset in the middle",
                benchmarkFn: () => collection.getAtOffset(length / 2),
                type: benchmark_1.BenchmarkType.Diagnostic,
            });
            (0, benchmark_1.benchmark)({
                title: "getAll",
                benchmarkFn: () => collection.getAll(),
                type,
            });
            (0, benchmark_1.benchmark)({
                title: "clone",
                benchmarkFn: () => collection.clone(),
                type,
            });
            (0, benchmark_1.benchmark)({
                title: "split + append in the middle",
                benchmarkFn: () => {
                    const split = collection.splitAt(length / 2);
                    collection.append(split);
                },
                type,
            });
        });
    }
    (0, benchmark_1.benchmark)({
        title: "construction",
        benchmarkFn: () => new ctor(42, { type: "op", seq: 5 }),
        type: suiteBaseType,
    });
    const segmentsToSerialize = collectionTestCases.map(({ collection }) => ({
        attribution: collection,
        cachedLength: collection.length,
    }));
    (0, benchmark_1.benchmark)({
        title: "serializing",
        benchmarkFn: () => ctor.serializeAttributionCollections(segmentsToSerialize),
        type: suiteBaseType,
    });
    const summary = ctor.serializeAttributionCollections(segmentsToSerialize);
    const segments = Array.from({ length: 9 }, () => ({
        cachedLength: Math.floor(summary.length / 10),
    }));
    segments.push({ cachedLength: summary.length - 9 * Math.floor(summary.length / 10) });
    (0, benchmark_1.benchmark)({
        title: "deserialize into 10 segments",
        benchmarkFn: () => {
            ctor.populateAttributionCollections(segments, summary);
        },
        type: suiteBaseType,
    });
}
// Note: channel functionality is left unimplemented.
class TreeAttributionCollection {
    constructor(_length, 
    // eslint-disable-next-line @rushstack/no-new-null
    baseEntry) {
        this._length = _length;
        this.entries = new collections_1.RedBlackTree(mergeTreeNodes_1.compareNumbers);
        if (baseEntry !== undefined) {
            this.entries.put(0, baseEntry);
        }
    }
    get channelNames() {
        return [];
    }
    getAtOffset(offset) {
        (0, assert_1.strict)(offset >= 0 && offset < this._length, "Requested offset should be valid");
        const node = this.entries.floor(offset);
        (0, assert_1.strict)(node !== undefined, "Collection should have at least one entry");
        return node.data ?? undefined;
    }
    get length() {
        return this._length;
    }
    /**
     * Splits this attribution collection into two with entries for [0, pos) and [pos, length).
     */
    splitAt(pos) {
        const splitBaseEntry = this.getAtOffset(pos);
        const splitCollection = new TreeAttributionCollection(this.length - pos, splitBaseEntry);
        for (let current = this.entries.ceil(pos); current !== undefined; current = this.entries.ceil(pos)) {
            // If there happened to be an attribution change at exactly pos, it's already set in the base entry
            if (current.key !== pos) {
                splitCollection.entries.put(current.key - pos, current.data);
            }
            this.entries.remove(current.key);
        }
        this._length = pos;
        return splitCollection;
    }
    append(other) {
        const lastEntry = this.getAtOffset(this.length - 1);
        other.entries.map(({ key, data }) => {
            if (key !== 0 || !(0, attributionCollection_1.areEqualAttributionKeys)(lastEntry, data)) {
                this.entries.put(key + this.length, data);
            }
            return true;
        });
        this._length += other.length;
    }
    getAll() {
        const results = [];
        this.entries.map(({ key, data }) => {
            results.push({ offset: key, key: data });
            return true;
        });
        return { root: results, length: this.length };
    }
    clone() {
        const copy = new TreeAttributionCollection(this.length, this.getAtOffset(0));
        this.entries.map(({ key, data }) => {
            copy.entries.put(key, data);
            return true;
        });
        return copy;
    }
    /**
     * Rehydrates attribution information from its serialized form into the provided iterable of consecutive segments.
     */
    static populateAttributionCollections(segments, summary) {
        const { seqs, posBreakpoints } = summary;
        (0, assert_1.strict)(seqs.length === posBreakpoints.length && seqs.length > 0, "Invalid attribution summary blob provided");
        let curIndex = 0;
        let cumulativeSegPos = 0;
        let currentInfo = seqs[curIndex];
        const getCurrentKey = () => typeof currentInfo === "object"
            ? currentInfo
            : { type: "op", seq: currentInfo };
        for (const segment of segments) {
            const attribution = new TreeAttributionCollection(segment.cachedLength, getCurrentKey());
            while (posBreakpoints[curIndex] < cumulativeSegPos + segment.cachedLength) {
                currentInfo = seqs[curIndex];
                attribution.entries.put(posBreakpoints[curIndex] - cumulativeSegPos, getCurrentKey());
                curIndex++;
            }
            segment.attribution = attribution;
            cumulativeSegPos += segment.cachedLength;
        }
    }
    update() {
        throw new Error("unimplemented");
    }
    /**
     * Condenses attribution information on consecutive segments into a `SerializedAttributionCollection`
     */
    static serializeAttributionCollections(segments) {
        const posBreakpoints = [];
        const seqs = [];
        let mostRecentAttributionKey;
        let cumulativePos = 0;
        let segmentsWithAttribution = 0;
        let segmentsWithoutAttribution = 0;
        for (const segment of segments) {
            if (segment.attribution) {
                segmentsWithAttribution++;
                for (const { offset, key: info } of segment.attribution?.getAll()?.root ?? []) {
                    if (mostRecentAttributionKey === undefined ||
                        !(0, attributionCollection_1.areEqualAttributionKeys)(info, mostRecentAttributionKey)) {
                        posBreakpoints.push(offset + cumulativePos);
                        seqs.push(!info ? null : info.type === "op" ? info.seq : info);
                    }
                    mostRecentAttributionKey = info;
                }
            }
            else {
                segmentsWithoutAttribution++;
            }
            cumulativePos += segment.cachedLength;
        }
        (0, assert_1.strict)(segmentsWithAttribution === 0 || segmentsWithoutAttribution === 0, "Expected either all segments or no segments to have attribution information.");
        const blobContents = {
            seqs,
            posBreakpoints,
            length: cumulativePos,
        };
        return blobContents;
    }
}
describe("IAttributionCollection perf", () => {
    describe("tree implementation", () => {
        runAttributionCollectionSuite(TreeAttributionCollection, benchmark_1.BenchmarkType.Diagnostic);
    });
    describe("list-based implementation", () => {
        runAttributionCollectionSuite(attributionCollection_1.AttributionCollection, benchmark_1.BenchmarkType.Measurement);
    });
});
//# sourceMappingURL=attributionCollection.perf.spec.cjs.map