"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInsertOnlyAttributionPolicy = exports.AttributionCollection = exports.areEqualAttributionKeys = void 0;
const common_utils_1 = require("@fluidframework/common-utils");
const constants_1 = require("./constants");
const mergeTreeDeltaCallback_1 = require("./mergeTreeDeltaCallback");
const ops_1 = require("./ops");
function areEqualAttributionKeys(a, b) {
    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, common_utils_1.unreachableCase)(a, "Unhandled AttributionKey type");
    }
}
exports.areEqualAttributionKeys = areEqualAttributionKeys;
class AttributionCollection {
    constructor(baseEntry, _length) {
        this._length = _length;
        this.offsets = [0];
        this.keys = [baseEntry];
    }
    getAtOffset(offset) {
        (0, common_utils_1.assert)(offset >= 0 && offset < this._length, 0x443 /* Requested offset should be valid */);
        return this.keys[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 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 splitBaseEntry = this.keys[splitIndex];
        const splitCollection = new AttributionCollection(splitBaseEntry, this.length - pos);
        for (let i = splitIndex + 1; i < this.keys.length; i++) {
            splitCollection.offsets.push(this.offsets[i] - pos);
            splitCollection.keys.push(this.keys[i]);
        }
        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]);
            }
        }
        this._length += other.length;
    }
    getAll() {
        const results = new Array(this.keys.length);
        for (let i = 0; i < this.keys.length; i++) {
            results[i] = { offset: this.offsets[i], key: this.keys[i] };
        }
        return results;
    }
    clone() {
        const copy = new AttributionCollection(this.keys[0], this.length);
        copy.keys = this.keys.slice();
        copy.offsets = this.offsets.slice();
        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, common_utils_1.assert)(seqs.length === posBreakpoints.length && seqs.length > 0, 0x445 /* Invalid attribution summary blob provided */);
        let curIndex = 0;
        let currentInfo = seqs[curIndex];
        let cumulativeSegPos = 0;
        for (const segment of segments) {
            const attribution = new AttributionCollection(typeof currentInfo === "object" ? currentInfo : { type: "op", seq: currentInfo }, segment.cachedLength);
            while (posBreakpoints[curIndex] < cumulativeSegPos + segment.cachedLength) {
                currentInfo = seqs[curIndex];
                const nextOffset = posBreakpoints[curIndex] - cumulativeSegPos;
                if (attribution.offsets[attribution.offsets.length - 1] !== nextOffset) {
                    attribution.offsets.push(nextOffset);
                    attribution.keys.push(typeof currentInfo === "object"
                        ? currentInfo
                        : { type: "op", seq: currentInfo });
                }
                curIndex++;
            }
            if (posBreakpoints[curIndex] === cumulativeSegPos + segment.cachedLength) {
                currentInfo = seqs[curIndex];
            }
            segment.attribution = attribution;
            cumulativeSegPos += segment.cachedLength;
        }
    }
    /**
     * Condenses attribution information on consecutive segments into a `SerializedAttributionCollection`
     */
    static serializeAttributionCollections(segments) {
        var _a, _b;
        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 } of (_b = (_a = segment.attribution) === null || _a === void 0 ? void 0 : _a.getAll()) !== null && _b !== void 0 ? _b : []) {
                    if (!mostRecentAttributionKey ||
                        !areEqualAttributionKeys(key, mostRecentAttributionKey)) {
                        posBreakpoints.push(offset + cumulativePos);
                        seqs.push(key.type === "op" ? key.seq : key);
                    }
                    mostRecentAttributionKey = key;
                }
            }
            else {
                segmentsWithoutAttribution++;
            }
            cumulativePos += segment.cachedLength;
        }
        (0, common_utils_1.assert)(segmentsWithAttribution === 0 || segmentsWithoutAttribution === 0, 0x446 /* Expected either all segments or no segments to have attribution information. */);
        const blobContents = {
            seqs,
            posBreakpoints,
            length: cumulativePos,
        };
        return blobContents;
    }
}
exports.AttributionCollection = AttributionCollection;
/**
 * @alpha
 * @returns - An {@link AttributionPolicy} which tracks only insertion of content.
 * Content is only attributed at ack time, unless the container is in a detached state.
 * Detached content is attributed with a {@link @fluidframework/runtime-definitions#DetachedAttributionKey}.
 */
function createInsertOnlyAttributionPolicy() {
    let unsubscribe;
    return {
        attach: (client) => {
            (0, common_utils_1.assert)(unsubscribe === undefined, 0x557 /* cannot attach to multiple clients at once */);
            const deltaCallback = (opArgs, { deltaSegments, operation }) => {
                var _a;
                if (operation !== ops_1.MergeTreeDeltaType.INSERT) {
                    return;
                }
                for (const { segment } of deltaSegments) {
                    if (segment.seq !== undefined && segment.seq !== constants_1.UnassignedSequenceNumber) {
                        const key = segment.seq === constants_1.UniversalSequenceNumber
                            ? { type: "detached", id: 0 }
                            : { type: "op", seq: segment.seq };
                        (_a = segment.attribution) !== null && _a !== void 0 ? _a : (segment.attribution = new AttributionCollection(key, segment.cachedLength));
                    }
                }
            };
            const maintenanceCallback = ({ deltaSegments, operation }, opArgs) => {
                if (operation !== mergeTreeDeltaCallback_1.MergeTreeMaintenanceType.ACKNOWLEDGED ||
                    opArgs === undefined ||
                    opArgs.op.type !== ops_1.MergeTreeDeltaType.INSERT) {
                    return;
                }
                for (const { segment } of deltaSegments) {
                    (0, common_utils_1.assert)(segment.seq !== undefined, 0x558 /* segment.seq should be set after ack. */);
                    segment.attribution = new AttributionCollection({ type: "op", seq: segment.seq }, segment.cachedLength);
                }
            };
            client.on("delta", deltaCallback);
            client.on("maintenance", maintenanceCallback);
            unsubscribe = () => {
                client.off("delta", deltaCallback);
                client.off("maintenance", maintenanceCallback);
            };
        },
        detach: () => {
            unsubscribe === null || unsubscribe === void 0 ? void 0 : unsubscribe();
            unsubscribe = undefined;
        },
        get isAttached() {
            return unsubscribe !== undefined;
        },
        serializer: AttributionCollection,
    };
}
exports.createInsertOnlyAttributionPolicy = createInsertOnlyAttributionPolicy;
//# sourceMappingURL=attributionCollection.js.map