/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { TypedEventEmitter } from "@fluid-internal/client-utils";
import { assert, Deferred } from "@fluidframework/core-utils/internal";
import { MessageType, } from "@fluidframework/driver-definitions/internal";
import { createChildLogger, } from "@fluidframework/telemetry-utils/internal";
var SummaryState;
(function (SummaryState) {
    SummaryState[SummaryState["Local"] = 0] = "Local";
    SummaryState[SummaryState["Broadcast"] = 1] = "Broadcast";
    SummaryState[SummaryState["Acked"] = 2] = "Acked";
    SummaryState[SummaryState["Nacked"] = -1] = "Nacked";
})(SummaryState || (SummaryState = {}));
class Summary {
    static createLocal(clientId, clientSequenceNumber) {
        return new Summary(clientId, clientSequenceNumber);
    }
    static createFromOp(op) {
        // TODO: Verify whether this should be able to handle server-generated ops (with null clientId)
        const summary = new Summary(op.clientId, op.clientSequenceNumber);
        summary.broadcast(op);
        return summary;
    }
    get summaryOp() {
        return this._summaryOp;
    }
    get summaryAckNack() {
        return this._summaryAckNack;
    }
    constructor(clientId, clientSequenceNumber) {
        this.clientId = clientId;
        this.clientSequenceNumber = clientSequenceNumber;
        this.state = SummaryState.Local;
        this.defSummaryOp = new Deferred();
        this.defSummaryAck = new Deferred();
    }
    hasBeenAcked() {
        return this.state === SummaryState.Acked;
    }
    broadcast(op) {
        assert(this.state === SummaryState.Local, 0x175 /* "Can only broadcast if summarizer starts in local state" */);
        this._summaryOp = op;
        this.defSummaryOp.resolve();
        this.state = SummaryState.Broadcast;
        return true;
    }
    ackNack(op) {
        assert(this.state === SummaryState.Broadcast, 0x176 /* "Can only ack/nack if summarizer is in broadcasting state" */);
        this._summaryAckNack = op;
        this.defSummaryAck.resolve();
        this.state = op.type === MessageType.SummaryAck ? SummaryState.Acked : SummaryState.Nacked;
        return true;
    }
    async waitBroadcast() {
        await this.defSummaryOp.promise;
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        return this._summaryOp;
    }
    async waitAckNack() {
        await this.defSummaryAck.promise;
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        return this._summaryAckNack;
    }
}
/**
 * This class watches summaries created by a specific client.
 * It should be created and managed from a SummaryCollection.
 */
class ClientSummaryWatcher {
    get disposed() {
        return this._disposed;
    }
    constructor(clientId, summaryCollection) {
        this.clientId = clientId;
        this.summaryCollection = summaryCollection;
        // key: clientSeqNum
        this.localSummaries = new Map();
        this._disposed = false;
    }
    /**
     * Watches for a specific sent summary op.
     * @param clientSequenceNumber - client sequence number of sent summary op
     */
    watchSummary(clientSequenceNumber) {
        let summary = this.localSummaries.get(clientSequenceNumber);
        if (!summary) {
            summary = Summary.createLocal(this.clientId, clientSequenceNumber);
            this.localSummaries.set(summary.clientSequenceNumber, summary);
        }
        return summary;
    }
    /**
     * Waits until all of the pending summaries in the underlying SummaryCollection
     * are acked/nacked.
     */
    async waitFlushed() {
        return this.summaryCollection.waitFlushed();
    }
    /**
     * Gets a watched summary or returns undefined if not watched.
     * @param clientSequenceNumber - client sequence number of sent summary op
     */
    tryGetSummary(clientSequenceNumber) {
        return this.localSummaries.get(clientSequenceNumber);
    }
    /**
     * Starts watching a summary made by this client.
     * @param summary - summary to start watching
     */
    setSummary(summary) {
        this.localSummaries.set(summary.clientSequenceNumber, summary);
    }
    dispose() {
        this.summaryCollection.removeWatcher(this.clientId);
        this._disposed = true;
    }
}
/**
 * Data structure that looks at the op stream to track summaries as they
 * are broadcast, acked and nacked.
 * It provides functionality for watching specific summaries.
 * @legacy @beta
 */
export class SummaryCollection extends TypedEventEmitter {
    get latestAck() {
        return this.lastAck;
    }
    emit(event, ...args) {
        return super.emit(event, ...args);
    }
    get opsSinceLastAck() {
        return (this.deltaManager.lastSequenceNumber -
            (this.lastAck?.summaryAck.sequenceNumber ?? this.deltaManager.initialSequenceNumber));
    }
    addOpListener(listener) {
        this.deltaManager.on("op", listener);
    }
    removeOpListener(listener) {
        this.deltaManager.off("op", listener);
    }
    constructor(deltaManager, logger) {
        super();
        this.deltaManager = deltaManager;
        // key: clientId
        this.summaryWatchers = new Map();
        // key: summarySeqNum
        this.pendingSummaries = new Map();
        this.refreshWaitNextAck = new Deferred();
        this.deltaManager.on("op", (op) => this.handleOp(op));
        this.logger = createChildLogger({ logger });
    }
    /**
     * Creates and returns a summary watcher for a specific client.
     * This will allow for local sent summaries to be better tracked.
     * @param clientId - client id for watcher
     */
    createWatcher(clientId) {
        const watcher = new ClientSummaryWatcher(clientId, this);
        this.summaryWatchers.set(clientId, watcher);
        return watcher;
    }
    removeWatcher(clientId) {
        this.summaryWatchers.delete(clientId);
    }
    setPendingAckTimerTimeoutCallback(maxAckWaitTime, timeoutCallback) {
        this.maxAckWaitTime = maxAckWaitTime;
        this.pendingAckTimerTimeoutCallback = timeoutCallback;
    }
    unsetPendingAckTimerTimeoutCallback() {
        this.maxAckWaitTime = undefined;
        this.pendingAckTimerTimeoutCallback = undefined;
    }
    /**
     * Returns a promise that resolves once all pending summary ops
     * have been acked or nacked.
     */
    async waitFlushed() {
        while (this.pendingSummaries.size > 0) {
            // eslint-disable-next-line @typescript-eslint/promise-function-async
            const promises = Array.from(this.pendingSummaries, ([, summary]) => summary.waitAckNack());
            await Promise.all(promises);
        }
        return this.lastAck;
    }
    /**
     * Returns a promise that resolves once a summary is acked that has a reference
     * sequence number greater than or equal to the passed in sequence number.
     * @param referenceSequenceNumber - reference sequence number to wait for
     * @returns The latest acked summary
     */
    async waitSummaryAck(referenceSequenceNumber) {
        while (!this.lastAck ||
            this.lastAck.summaryOp.referenceSequenceNumber < referenceSequenceNumber) {
            await this.refreshWaitNextAck.promise;
        }
        return this.lastAck;
    }
    parseContent(op) {
        // This should become unconditional once (Loader LTS) reaches 2.4 or later
        // There will be a long time of needing both cases, until LTS catches up to the change.
        // That said, we may instead move to listen for "op" events from ContainerRuntime,
        // and parsing may not be required at all if ContainerRuntime.process() continues to parse it for all types of ops.
        if (typeof op.contents === "string") {
            op.contents = JSON.parse(op.contents);
        }
    }
    /**
     * Handler for ops; only handles ops relating to summaries.
     * @param op - op message to handle
     */
    handleOp(opArg) {
        const op = { ...opArg };
        switch (op.type) {
            case MessageType.Summarize: {
                this.parseContent(op);
                return this.handleSummaryOp(op);
            }
            case MessageType.SummaryAck:
            case MessageType.SummaryNack: {
                // Old files (prior to PR #10077) may not contain this info
                if (op.data === undefined) {
                    this.parseContent(op);
                }
                else {
                    op.contents = JSON.parse(op.data);
                }
                return op.type === MessageType.SummaryAck
                    ? this.handleSummaryAck(op)
                    : this.handleSummaryNack(op);
            }
            default: {
                // If the difference between timestamp of current op and last summary op is greater than
                // the maxAckWaitTime, then we need to inform summarizer to not wait and summarize
                // immediately as we have already waited for maxAckWaitTime.
                const lastOpTimestamp = op.timestamp;
                if (this.lastSummaryTimestamp !== undefined &&
                    this.maxAckWaitTime !== undefined &&
                    lastOpTimestamp - this.lastSummaryTimestamp >= this.maxAckWaitTime) {
                    this.pendingAckTimerTimeoutCallback?.();
                }
                this.emit("default", op);
                return;
            }
        }
    }
    handleSummaryOp(op) {
        let summary;
        // Check if summary already being watched, broadcast if so
        // TODO: Verify whether this should be able to handle server-generated ops (with null clientId)
        const watcher = this.summaryWatchers.get(op.clientId);
        if (watcher) {
            summary = watcher.tryGetSummary(op.clientSequenceNumber);
            if (summary) {
                summary.broadcast(op);
            }
        }
        // If not watched, create from op
        if (!summary) {
            summary = Summary.createFromOp(op);
            if (watcher) {
                watcher.setSummary(summary);
            }
        }
        this.pendingSummaries.set(op.sequenceNumber, summary);
        this.lastSummaryTimestamp = op.timestamp;
        this.emit(MessageType.Summarize, op);
    }
    handleSummaryAck(op) {
        const seq = op.contents.summaryProposal.summarySequenceNumber;
        const summary = this.pendingSummaries.get(seq);
        // eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- optional chain is not logically equivalent
        if (!summary || summary.summaryOp === undefined) {
            // Summary ack without an op should be rare. We could fetch the
            // reference sequence number from the snapshot, but instead we
            // will not emit this ack. It should be the case that the summary
            // op that this ack is for is earlier than this file was loaded
            // from. i.e. initialSequenceNumber > summarySequenceNumber.
            // We really don't care about it for now, since it is older than
            // the one we loaded from.
            if (seq > this.deltaManager.initialSequenceNumber) {
                // Potential causes for it to be later than our initialSequenceNumber
                // are that the summaryOp was nacked then acked, double-acked, or
                // the summarySequenceNumber is incorrect.
                this.logger.sendTelemetryEvent({
                    eventName: "SummaryAckWithoutOp",
                    sequenceNumber: op.sequenceNumber, // summary ack seq #
                    summarySequenceNumber: seq, // missing summary seq #
                    initialSequenceNumber: this.deltaManager.initialSequenceNumber,
                });
            }
            return;
        }
        summary.ackNack(op);
        this.pendingSummaries.delete(seq);
        // Track latest ack
        if (!this.lastAck ||
            seq > this.lastAck.summaryAck.contents.summaryProposal.summarySequenceNumber) {
            this.lastAck = {
                summaryOp: summary.summaryOp,
                summaryAck: op,
            };
            this.refreshWaitNextAck.resolve();
            this.refreshWaitNextAck = new Deferred();
            this.emit(MessageType.SummaryAck, op);
        }
    }
    handleSummaryNack(op) {
        const seq = op.contents.summaryProposal.summarySequenceNumber;
        const summary = this.pendingSummaries.get(seq);
        if (summary) {
            summary.ackNack(op);
            this.pendingSummaries.delete(seq);
            this.emit(MessageType.SummaryNack, op);
        }
    }
}
//# sourceMappingURL=summaryCollection.js.map