import _ from 'lodash';
import '../../chipper/js/initialize-globals.js';
import '../../assert/js/assert.js';
import '../../globals.js';
// Copyright 2015-2024, University of Colorado Boulder
/**
 * Lightweight event & listener abstraction for a single event type.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Michael Kauzmann (PhET Interactive Simulations)
 */
import dotRandom from '../../dot/js/dotRandom.js';
import Random from '../../dot/js/Random.js';
import Pool from '../../phet-core/js/Pool.js';
import axon from './axon.js';
// constants
const listenerOrder = _.hasIn(window, 'phet.chipper.queryParameters') && phet.chipper.queryParameters.listenerOrder;
const listenerLimit = _.hasIn(window, 'phet.chipper.queryParameters') && phet.chipper.queryParameters.listenerLimit;
const EMIT_CONTEXT_MAX_LENGTH = 1000;
let random = null;
if (listenerOrder && listenerOrder.startsWith('random')) {
    // NOTE: this regular expression must be maintained in initialize-globals as well.
    const match = listenerOrder.match(/random(?:%28|\()(\d+)(?:%29|\))/);
    const seed = match ? Number(match[1]) : dotRandom.nextInt(1000000);
    random = new Random({ seed: seed });
    console.log('listenerOrder random seed: ' + seed);
}
// Store the number of listeners from the single TinyEmitter instance that has the most listeners in the whole runtime.
let maxListenerCount = 0;
export default class TinyEmitter {
    // Null on parameters is a no-op
    constructor(onBeforeNotify, hasListenerOrderDependencies, reentrantNotificationStrategy) {
        // During emit() keep track of iteration progress and guard listeners if mutated during emit()
        this.emitContexts = [];
        if (onBeforeNotify) {
            this.onBeforeNotify = onBeforeNotify;
        }
        if (hasListenerOrderDependencies) {
            this.hasListenerOrderDependencies = hasListenerOrderDependencies;
        }
        if (reentrantNotificationStrategy) {
            this.reentrantNotificationStrategy = reentrantNotificationStrategy;
        }
        // Listener order is preserved in Set
        this.listeners = new Set();
        // for production memory concerns; no need to keep this around.
        if (assert) {
            this.isDisposed = false;
        }
    }
    /**
     * Disposes an Emitter. All listeners are removed.
     */
    dispose() {
        this.removeAllListeners();
        if (assert) {
            this.isDisposed = true;
        }
    }
    /**
     * Notify listeners
     */
    emit(...args) {
        assert && assert(!this.isDisposed, 'TinyEmitter.emit() should not be called if disposed.');
        // optional callback, before notifying listeners
        this.onBeforeNotify && this.onBeforeNotify.apply(null, args);
        // Support for a query parameter that shuffles listeners, but bury behind assert so it will be stripped out on build
        // so it won't impact production performance.
        if (assert && listenerOrder && (listenerOrder !== 'default') && !this.hasListenerOrderDependencies) {
            const asArray = Array.from(this.listeners);
            const reorderedListeners = listenerOrder.startsWith('random') ? random.shuffle(asArray) : asArray.reverse();
            this.listeners = new Set(reorderedListeners);
        }
        // Notify wired-up listeners, if any
        if (this.listeners.size > 0) {
            // We may not be able to emit right away. If we are already emitting and this is a recursive call, then that
            // first emit needs to finish notifying its listeners before we start our notifications (in queue mode), so store
            // the args for later. No slice needed, we're not modifying the args array.
            const emitContext = EmitContext.create(0, args);
            this.emitContexts.push(emitContext);
            if (this.reentrantNotificationStrategy === 'queue') {
                // This handles all reentrancy here (with a while loop), instead of doing so with recursion. If not the first context, then no-op because a previous call will handle this call's listener notifications.
                if (this.emitContexts.length === 1) {
                    while (this.emitContexts.length) {
                        // Don't remove it from the list here. We need to be able to guardListeners.
                        const emitContext = this.emitContexts[0];
                        // It is possible that this emitContext is later on in the while loop, and has already had a listenerArray set
                        const listeners = emitContext.hasListenerArray ? emitContext.listenerArray : this.listeners;
                        this.notifyLoop(emitContext, listeners);
                        this.emitContexts.shift()?.freeToPool();
                    }
                }
                else {
                    assert && assert(this.emitContexts.length <= EMIT_CONTEXT_MAX_LENGTH, `emitting reentrant depth of ${EMIT_CONTEXT_MAX_LENGTH} seems like a infinite loop to me!`);
                }
            }
            else if (!this.reentrantNotificationStrategy || this.reentrantNotificationStrategy === 'stack') {
                this.notifyLoop(emitContext, this.listeners);
                this.emitContexts.pop()?.freeToPool();
            }
            else {
                assert && assert(false, `Unknown reentrantNotificationStrategy: ${this.reentrantNotificationStrategy}`);
            }
        }
    }
    /**
     * Execute the notification of listeners (from the provided context and list). This function supports guarding against
     * if listener order changes during the notification process, see guardListeners.
     */
    notifyLoop(emitContext, listeners) {
        const args = emitContext.args;
        for (const listener of listeners) {
            listener(...args);
            emitContext.index++;
            // If a listener was added or removed, we cannot continue processing the mutated Set, we must switch to
            // iterate over the guarded array
            if (emitContext.hasListenerArray) {
                break;
            }
        }
        // If the listeners were guarded during emit, we bailed out on the for..of and continue iterating over the original
        // listeners in order from where we left off.
        if (emitContext.hasListenerArray) {
            for (let i = emitContext.index; i < emitContext.listenerArray.length; i++) {
                emitContext.listenerArray[i](...args);
            }
        }
    }
    /**
     * Adds a listener which will be called during emit.
     */
    addListener(listener) {
        assert && assert(!this.isDisposed, 'Cannot add a listener to a disposed TinyEmitter');
        assert && assert(!this.hasListener(listener), 'Cannot add the same listener twice');
        // If a listener is added during an emit(), we must make a copy of the current list of listeners--the newly added
        // listener will be available for the next emit() but not the one in progress.  This is to match behavior with
        // removeListener.
        this.guardListeners();
        this.listeners.add(listener);
        this.changeCount && this.changeCount(1);
        if (assert && listenerLimit && isFinite(listenerLimit) && maxListenerCount < this.listeners.size) {
            maxListenerCount = this.listeners.size;
            console.log(`Max TinyEmitter listeners: ${maxListenerCount}`);
            assert(maxListenerCount <= listenerLimit, `listener count of ${maxListenerCount} above ?listenerLimit=${listenerLimit}`);
        }
    }
    /**
     * Removes a listener
     */
    removeListener(listener) {
        // Throw an error when removing a non-listener (except when the Emitter has already been disposed, see
        // https://github.com/phetsims/sun/issues/394#issuecomment-419998231
        if (assert && !this.isDisposed) {
            assert(this.listeners.has(listener), 'tried to removeListener on something that wasn\'t a listener');
        }
        this.guardListeners();
        this.listeners.delete(listener);
        this.changeCount && this.changeCount(-1);
    }
    /**
     * Removes all the listeners
     */
    removeAllListeners() {
        const size = this.listeners.size;
        this.guardListeners();
        this.listeners.clear();
        this.changeCount && this.changeCount(-size);
    }
    /**
     * If listeners are added/removed while emit() is in progress, we must make a defensive copy of the array of listeners
     * before changing the array, and use it for the rest of the notifications until the emit call has completed.
     */
    guardListeners() {
        for (let i = this.emitContexts.length - 1; i >= 0; i--) {
            const emitContext = this.emitContexts[i];
            // Once we meet a level that was already guarded, we can stop, since all previous levels were already guarded
            if (emitContext.hasListenerArray) {
                break;
            }
            // Mark copies as 'guarded' so that it will use the original listeners when emit started and not the modified
            // list.
            emitContext.listenerArray.push(...this.listeners);
            emitContext.hasListenerArray = true;
        }
    }
    /**
     * Checks whether a listener is registered with this Emitter
     */
    hasListener(listener) {
        assert && assert(arguments.length === 1, 'Emitter.hasListener should be called with 1 argument');
        return this.listeners.has(listener);
    }
    /**
     * Returns true if there are any listeners.
     */
    hasListeners() {
        assert && assert(arguments.length === 0, 'Emitter.hasListeners should be called without arguments');
        return this.listeners.size > 0;
    }
    /**
     * Returns the number of listeners.
     */
    getListenerCount() {
        return this.listeners.size;
    }
    /**
     * Invokes a callback once for each listener - meant for Property's use
     */
    forEachListener(callback) {
        this.listeners.forEach(callback);
    }
}
/**
 * Utility class for managing the context of an emit call. This is used to manage the state of the emit call, and
 * especially to handle reentrant emit calls (through the stack/queue notification strategies)
 */
class EmitContext {
    constructor(index, args) {
        // Whether we should act like there is a listenerArray (has it been copied?)
        this.hasListenerArray = false;
        // Only use this if hasListenerArray is true. NOTE: for performance, we're not using getters/etc.
        this.listenerArray = [];
        this.initialize(index, args);
    }
    initialize(index, args) {
        this.index = index;
        this.args = args;
        this.hasListenerArray = false;
        return this;
    }
    freeToPool() {
        // TypeScript doesn't need to know that we're using this for different types. When it is "active", it will be
        // the correct type.
        EmitContext.pool.freeToPool(this);
        // NOTE: If we have fewer concerns about memory in the future, we could potentially improve performance by
        // removing the clearing out of memory here. We don't seem to create many EmitContexts, HOWEVER if we have ONE
        // "more re-entrant" case on sim startup that references a BIG BIG object, it could theoretically keep that
        // object alive forever.
        // We want to null things out to prevent memory leaks. Don't tell TypeScript!
        // (It will have the correct types after the initialization, so this works well with our pooling pattern).
        this.args = null;
        // Clear out the listeners array, so we don't leak memory while we are in the pool. If we have less concerns
        this.listenerArray.length = 0;
    }
    static create(index, args) {
        // TypeScript doesn't need to know that we're using this for different types. When it is "active", it will be
        // the correct type.
        return EmitContext.pool.create(index, args);
    }
}
EmitContext.pool = new Pool(EmitContext, {
    initialize: EmitContext.prototype.initialize
});
axon.register('TinyEmitter', TinyEmitter);
//# sourceMappingURL=TinyEmitter.js.map