"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 core_utils_1 = require("@fluidframework/core-utils");
// 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, core_utils_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, core_utils_1.assert)(offset >= 0 && offset < this._length, 0x443 /* Requested offset should be valid */);
        return this.get(this.findIndex(offset));
    }
    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) {
        var _a;
        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 ?? (this.channels = {});
            for (const [key, collection] of other.channelEntries) {
                const thisCollection = ((_a = this.channels)[key] ?? (_a[key] = new AttributionCollection(this.length, null)));
                thisCollection.append(collection);
            }
            for (const [key, collection] of this.channelEntries) {
                if (other.channels?.[key] === undefined) {
                    collection.append(new AttributionCollection(other.length, null));
                }
            }
        }
        this._length += other.length;
    }
    getAll() {
        const root = new Array(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.slice();
        copy.offsets = this.offsets.slice();
        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, core_utils_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 ?? (this.channels = {});
            if (this.channels[name] !== undefined) {
                this.channels[name].update(undefined, channel);
            }
            else {
                this.channels[name] = channel;
            }
        }
    }
    /**
     * Rehydrates attribution information from its serialized form into the provided iterable of consecutive segments.
     */
    static populateAttributionCollections(segments, summary) {
        const { channels } = summary;
        (0, core_utils_1.assert)(summary.seqs.length === summary.posBreakpoints.length, 0x445 /* Invalid attribution summary blob provided */);
        const extractOntoSegments = ({ seqs, posBreakpoints }, assignToSegment) => {
            let curIndex = 0;
            let cumulativeSegPos = 0;
            for (const segment of segments) {
                const attribution = new AttributionCollection(segment.cachedLength);
                const pushEntry = (offset, seq) => {
                    attribution.offsets.push(offset);
                    attribution.keys.push(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) => {
                    var _a;
                    // Cast is valid as we just assigned this field above
                    ((_a = segment.attribution).channels ?? (_a.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 = 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, core_utils_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);
                        seqs.push(!key ? null : key.type === "op" ? key.seq : key);
                    }
                    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((spec) => spec.channels?.[name] ?? [{ offset: 0, key: null }]);
                channels[name] = { posBreakpoints, seqs };
            }
            blobContents.channels = channels;
        }
        return blobContents;
    }
}
exports.AttributionCollection = AttributionCollection;
//# sourceMappingURL=attributionCollection.cjs.map