/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { UsageError } from "@fluidframework/telemetry-utils/internal";
/**
 * Create a {@link Listenable} that can be instructed to emit events via the {@link IEmitter} interface.
 *
 * A class can delegate handling {@link Listenable} to the returned value while using it to emit the events.
 * See also `EventEmitter` which be used as a base class to implement {@link Listenable} via extension.
 * @internal
 */
export function createEmitter(noListeners) {
    return new ComposableEventEmitter(noListeners);
}
/**
 * Provides an API for subscribing to and listening to events.
 *
 * @remarks Classes wishing to emit events may either extend this class or compose over it.
 *
 * @example Extending this class
 *
 * ```typescript
 * interface MyEvents {
 *   "loaded": () => void;
 * }
 *
 * class MyClass extends EventEmitter<MyEvents> {
 *   private load() {
 *     this.emit("loaded");
 *   }
 * }
 * ```
 *
 * @example Composing over this class
 *
 * ```typescript
 * class MyClass implements ISubscribable<MyEvents> {
 *   private readonly events = EventEmitter.create<MyEvents>();
 *
 *   private load() {
 *     this.events.emit("loaded");
 *   }
 *
 *   public on<K extends keyof MyEvents>(eventName: K, listener: MyEvents[K]): Off {
 *     return this.events.on(eventName, listener);
 *   }
 * }
 * ```
 */
export class EventEmitter {
    noListeners;
    listeners = new Map();
    // Because this is protected and not public, calling this externally (not from a subclass) makes sending events to the constructed instance impossible.
    // Instead, use the static `create` function to get an instance which allows emitting events.
    constructor(noListeners) {
        this.noListeners = noListeners;
    }
    emit(eventName, ...args) {
        const listeners = this.listeners.get(eventName);
        if (listeners !== undefined) {
            const argArray = args; // TODO: Current TS (4.5.5) cannot spread `args` into `listener()`, but future versions (e.g. 4.8.4) can.
            // This explicitly copies listeners so that new listeners added during this call to emit will not receive this event.
            for (const [off, listener] of [...listeners]) {
                // If listener has been unsubscribed while invoking other listeners, skip it.
                if (listeners.has(off)) {
                    listener(...argArray);
                }
            }
        }
    }
    emitAndCollect(eventName, ...args) {
        const listeners = this.listeners.get(eventName);
        if (listeners !== undefined) {
            const argArray = args;
            const resultArray = [];
            for (const listener of [...listeners.values()]) {
                resultArray.push(listener(...argArray));
            }
            return resultArray;
        }
        return [];
    }
    /**
     * Register an event listener.
     * @param eventName - the name of the event
     * @param listener - the handler to run when the event is fired by the emitter
     * @returns a function which will deregister the listener when run.
     * This function will error if called more than once.
     * @privateRemarks
     * TODO:
     * invoking the returned callback can error even if its only called once if the same listener was provided to two calls to "on".
     * This behavior is not documented and its unclear if its a bug or not: see note on listeners.
     */
    on(eventName, listener) {
        const off = () => {
            const currentListeners = this.listeners.get(eventName);
            if (currentListeners?.delete(off) === true) {
                if (currentListeners.size === 0) {
                    this.listeners.delete(eventName);
                    this.noListeners?.(eventName);
                }
            }
        };
        const listeners = this.listeners.get(eventName);
        if (listeners === undefined) {
            const map = new Map([[off, listener]]);
            this.listeners.set(eventName, map);
        }
        else {
            // If the same listener function is already registered, error.
            // This policy may change in the future, but in the meantime this is a conservative choice that can accommodate multiple future eventing API/implementation options.
            // For example, adding an `Listenable.off()` method in the future could be problematic if we allowed registering the same function twice (should `off(f)` deregister _both_ `f`s or just one?).
            for (const l of listeners.values()) {
                if (Object.is(l, listener)) {
                    throw new UsageError("The same listener may not be registered more than once for the same event");
                }
            }
            listeners.set(off, listener);
        }
        return off;
    }
    hasListeners(eventName) {
        if (eventName === undefined) {
            return this.listeners.size !== 0;
        }
        return this.listeners.has(eventName);
    }
}
/**
 * This class exposes the constructor and the `emit` method of `EventEmitter`, elevating them from protected to public
 */
export class ComposableEventEmitter extends EventEmitter {
    constructor(noListeners) {
        super(noListeners);
    }
    emit(eventName, ...args) {
        return super.emit(eventName, ...args);
    }
    emitAndCollect(eventName, ...args) {
        return super.emitAndCollect(eventName, ...args);
    }
}
//# sourceMappingURL=events.js.map