"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.AttributionCollection = exports.areEqualAttributionKeys = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
// note: treats null and undefined as equivalent
function areEqualAttributionKeys(
// eslint-disable-next-line @rushstack/no-new-null
a, 
// eslint-disable-next-line @rushstack/no-new-null
b) {
    if (!a && !b) {
        return true;
    }
    if (!a || !b) {
        return false;
    }
    if (a.type !== b.type) {
        return false;
    }
    // Note: TS can't narrow the type of b inside this switch statement, hence the need for casting.
    switch (a.type) {
        case "op": {
            return a.seq === b.seq;
        }
        case "detached": {
            return a.id === b.id;
        }
        case "local": {
            return true;
        }
        default: {
            (0, internal_1.unreachableCase)(a, "Unhandled AttributionKey type");
        }
    }
}
exports.areEqualAttributionKeys = areEqualAttributionKeys;
class AttributionCollection {
    get channelEntries() {
        return Object.entries(this.channels ?? {});
    }
    constructor(_length, 
    // eslint-disable-next-line @rushstack/no-new-null
    baseEntry) {
        this._length = _length;
        this.offsets = [];
        this.keys = [];
        if (baseEntry !== undefined) {
            this.offsets.push(0);
            this.keys.push(baseEntry);
        }
    }
    get channelNames() {
        return Object.keys(this.channels ?? {});
    }
    getAtOffset(offset, channel) {
        if (channel !== undefined) {
            const subCollection = this.channels?.[channel];
            return subCollection?.getAtOffset(offset);
        }
        (0, internal_1.assert)(offset >= 0 && offset < this._length, 0x443 /* Requested offset should be valid */);
        return this.get(this.findIndex(offset));
    }
    getKeysInOffsetRange(startOffset, endOffset, channel) {
        if (startOffset < 0 || startOffset >= this._length) {
            throw new internal_2.UsageError("startOffset should be valid and in range");
        }
        if (endOffset !== undefined &&
            (endOffset < 0 || endOffset >= this._length || startOffset > endOffset)) {
            throw new internal_2.UsageError("endOffset should be valid and in range");
        }
        if (channel !== undefined) {
            const subCollection = this.channels?.[channel];
            return subCollection?.getKeysInOffsetRange(startOffset, endOffset);
        }
        const result = [];
        let index = this.findIndex(startOffset);
        let attributionKey = this.get(index);
        if (attributionKey !== undefined) {
            result.push({ offset: this.offsets[index], key: attributionKey });
        }
        index++;
        const endOffsetVal = endOffset ?? Number.MAX_SAFE_INTEGER;
        while (index < this.offsets.length && endOffsetVal >= this.offsets[index]) {
            attributionKey = this.get(index);
            if (attributionKey !== undefined) {
                result.push({ offset: this.offsets[index], key: attributionKey });
            }
            index++;
        }
        return result;
    }
    findIndex(offset) {
        // Note: maximum length here is 256 for text segments. Perf testing shows that linear scan beats binary search
        // for attribution collections with under ~64 entries, and even at maximum size (which would require a maximum
        // length segment with every offset having different attribution), getAtOffset is on the order of 100ns.
        let i = 0;
        while (i < this.offsets.length && offset > this.offsets[i]) {
            i++;
        }
        return this.offsets[i] === offset ? i : i - 1;
    }
    get(index) {
        const key = this.keys[index];
        return key ?? undefined;
    }
    get length() {
        return this._length;
    }
    /**
     * Splits this attribution collection into two with entries for [0, pos) and [pos, length).
     */
    splitAt(pos) {
        const splitIndex = this.findIndex(pos);
        const splitCollection = new AttributionCollection(this.length - pos);
        for (let i = splitIndex; i < this.keys.length; i++) {
            splitCollection.offsets.push(Math.max(this.offsets[i] - pos, 0));
            splitCollection.keys.push(this.keys[i]);
        }
        if (this.channels) {
            splitCollection.channels = {};
            for (const [key, collection] of this.channelEntries) {
                splitCollection.channels[key] = collection.splitAt(pos);
            }
        }
        const spliceIndex = this.offsets[splitIndex] === pos ? splitIndex : splitIndex + 1;
        this.keys.splice(spliceIndex);
        this.offsets.splice(spliceIndex);
        this._length = pos;
        return splitCollection;
    }
    append(other) {
        const lastEntry = this.keys[this.keys.length - 1];
        for (let i = 0; i < other.keys.length; i++) {
            if (i !== 0 || !areEqualAttributionKeys(lastEntry, other.keys[i])) {
                this.offsets.push(other.offsets[i] + this.length);
                this.keys.push(other.keys[i]);
            }
        }
        if (other.channels !== undefined || this.channels !== undefined) {
            this.channels ??= {};
            for (const [key, collection] of other.channelEntries) {
                const thisCollection = (this.channels[key] ??= new AttributionCollection(this.length, 
                // Null is needed as null and undefined have different meanings in the context of attribution collections.
                // eslint-disable-next-line unicorn/no-null
                null));
                thisCollection.append(collection);
            }
            for (const [key, collection] of this.channelEntries) {
                if (other.channels?.[key] === undefined) {
                    // eslint-disable-next-line unicorn/no-null
                    collection.append(new AttributionCollection(other.length, null));
                }
            }
        }
        this._length += other.length;
    }
    getAll() {
        const root = Array.from({ length: this.keys.length });
        for (let i = 0; i < this.keys.length; i++) {
            root[i] = { offset: this.offsets[i], key: this.keys[i] };
        }
        const result = {
            root,
            length: this.length,
        };
        if (this.channels !== undefined) {
            result.channels = {};
            for (const [key, collection] of this.channelEntries) {
                result.channels[key] = collection.getAll().root;
            }
        }
        return result;
    }
    clone() {
        const copy = new AttributionCollection(this.length);
        copy.keys = [...this.keys];
        copy.offsets = [...this.offsets];
        if (this.channels !== undefined) {
            const channelsCopy = {};
            for (const [key, collection] of this.channelEntries) {
                channelsCopy[key] = collection.clone();
            }
            copy.channels = channelsCopy;
        }
        return copy;
    }
    update(name, channel) {
        (0, internal_1.assert)(channel.length === this.length, 0x5c0 /* AttributionCollection channel update should have consistent segment length */);
        if (name === undefined) {
            this.offsets = [...channel.offsets];
            this.keys = [...channel.keys];
        }
        else {
            this.channels ??= {};
            if (this.channels[name] === undefined) {
                this.channels[name] = channel;
            }
            else {
                this.channels[name]?.update(undefined, channel);
            }
        }
    }
    /**
     * Rehydrates attribution information from its serialized form into the provided iterable of consecutive segments.
     */
    static populateAttributionCollections(segments, summary) {
        const { channels } = summary;
        (0, internal_1.assert)(
        // Destructuring here would require renaming the variables, since seqs is declared below
        // eslint-disable-next-line unicorn/consistent-destructuring
        summary.seqs.length === summary.posBreakpoints.length, 0x445 /* Invalid attribution summary blob provided */);
        const extractOntoSegments = ({ seqs, posBreakpoints }, assignToSegment) => {
            if (seqs.length === 0) {
                (0, internal_1.assert)(posBreakpoints.length === 0, 0x9e1 /* seqs and posBreakpoints length should match */);
                return;
            }
            let curIndex = 0;
            let cumulativeSegPos = 0;
            for (const segment of segments) {
                const attribution = new AttributionCollection(segment.cachedLength);
                // This function is defined here to allow for the creation of a new collection for each segment.
                // eslint-disable-next-line unicorn/consistent-function-scoping
                const pushEntry = (offset, seq) => {
                    attribution.offsets.push(offset);
                    attribution.keys.push(
                    // eslint-disable-next-line unicorn/no-null
                    seq === null ? null : typeof seq === "object" ? seq : { type: "op", seq });
                };
                if (posBreakpoints[curIndex] > cumulativeSegPos) {
                    curIndex--;
                }
                while (posBreakpoints[curIndex] < cumulativeSegPos + segment.cachedLength) {
                    const nextOffset = Math.max(posBreakpoints[curIndex] - cumulativeSegPos, 0);
                    pushEntry(nextOffset, seqs[curIndex]);
                    curIndex++;
                }
                if (attribution.offsets.length === 0) {
                    pushEntry(0, seqs[curIndex - 1]);
                }
                assignToSegment(attribution, segment);
                cumulativeSegPos += segment.cachedLength;
            }
        };
        extractOntoSegments(summary, (collection, segment) => {
            segment.attribution = collection;
        });
        if (channels) {
            for (const [name, collectionSpec] of Object.entries(channels)) {
                extractOntoSegments(collectionSpec, (collection, segment) => {
                    if (segment.attribution !== undefined) {
                        // Cast is valid as we just assigned this field above
                        (segment.attribution.channels ??= {})[name] =
                            collection;
                    }
                });
            }
        }
    }
    /**
     * Condenses attribution information on consecutive segments into a `SerializedAttributionCollection`
     *
     * Note: this operates on segments rather than attribution collections directly so that it can handle cases
     * where only some segments have attribution defined.
     */
    static serializeAttributionCollections(segments) {
        const allCollectionSpecs = [];
        const allChannelNames = new Set();
        for (const segment of segments) {
            const collection = 
            // eslint-disable-next-line unicorn/no-null
            segment.attribution ?? new AttributionCollection(segment.cachedLength, null);
            const spec = collection.getAll();
            allCollectionSpecs.push(spec);
            if (spec.channels) {
                for (const name of Object.keys(spec.channels)) {
                    allChannelNames.add(name);
                }
            }
        }
        const extractSequenceOffsets = (getSpecEntries) => {
            const posBreakpoints = [];
            const seqs = [];
            let mostRecentAttributionKey;
            let cumulativePos = 0;
            for (const spec of allCollectionSpecs) {
                for (const { offset, key } of getSpecEntries(spec)) {
                    (0, internal_1.assert)(key?.type !== "local", 0x5c1 /* local attribution keys should never be put in summaries */);
                    if (mostRecentAttributionKey === undefined ||
                        !areEqualAttributionKeys(key, mostRecentAttributionKey)) {
                        posBreakpoints.push(offset + cumulativePos);
                        // eslint-disable-next-line unicorn/no-null
                        seqs.push(key ? (key.type === "op" ? key.seq : key) : null);
                    }
                    mostRecentAttributionKey = key;
                }
                cumulativePos += spec.length;
            }
            return { seqs, posBreakpoints, length: cumulativePos };
        };
        const blobContents = extractSequenceOffsets((spec) => spec.root);
        if (allChannelNames.size > 0) {
            const channels = {};
            for (const name of allChannelNames) {
                const { posBreakpoints, seqs } = extractSequenceOffsets(
                // eslint-disable-next-line unicorn/no-null
                (spec) => spec.channels?.[name] ?? [{ offset: 0, key: null }]);
                channels[name] = { posBreakpoints, seqs };
            }
            blobContents.channels = channels;
        }
        return blobContents;
    }
}
exports.AttributionCollection = AttributionCollection;
//# sourceMappingURL=attributionCollection.js.map