import '../../assert/js/assert.js';
import '../../globals.js';
// Copyright 2022-2025, University of Colorado Boulder
/**
 * A base class to help with managing disposal. Creates a disposeEmitter that will be fired when disposing. This occurs
 * AFTER all prototype dispose() methods have been called up the hierarchy, so be aware of potential disposal order
 * issues if using disposeEmitter and dispose() logic together.
 *
 * This class also includes a public flag set to true when disposed.
 *
 * You can also opt into asserting out when disposing, preventing disposal on your class, see Disposable.isDisposable
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Michael Kauzmann (PhET Interactive Simulations)
 */
import affirm, { isAffirmEnabled } from '../../perennial-alias/js/browser-and-node/affirm.js';
import axon from './axon.js';
import TinyEmitter from './TinyEmitter.js';
class Disposable {
    // Most time, Disposable should only be used by subtypes, only instantiate it if you run into multiple inheritance issues.
    constructor(providedOptions) {
        // Called after all code that is directly in `dispose()` methods. Be careful with mixing this pattern and the
        // `this.disposeMyClass()` pattern.
        this._disposeEmitter = new TinyEmitter();
        // Keep track if this instance supports disposing. If set to false, then an assertion will fire if trying to dispose
        // this instance.
        this._isDisposable = true;
        // Marked true when this Disposable has had dispose() called on it (after disposeEmitter is fired)
        this._isDisposed = false;
        providedOptions && this.initializeDisposable(providedOptions);
        if (isAffirmEnabled()) {
            // Wrap the prototype dispose method with a check. NOTE: We will not catch devious cases where the dispose() is
            // overridden after the Node constructor (which may happen).
            const protoDispose = this.dispose;
            this.dispose = () => {
                affirm(!this._isDisposed, 'This Disposable has already been disposed, and cannot be disposed again');
                protoDispose.call(this);
                affirm(this._isDisposed, 'Disposable.dispose() call is missing from an overridden dispose method');
            };
        }
    }
    getDisposeEmitter() {
        return this._disposeEmitter;
    }
    get disposeEmitter() {
        return this.getDisposeEmitter();
    }
    get isDisposed() {
        return this._isDisposed;
    }
    get isDisposable() {
        return this._isDisposable;
    }
    set isDisposable(isDisposable) {
        this._isDisposable = isDisposable;
    }
    initializeDisposable(options) {
        if (options && options.hasOwnProperty('isDisposable')) {
            this._isDisposable = options.isDisposable;
        }
    }
    /**
     * Add disposables that will be disposed when this instance is disposed.
     */
    addDisposable(...disposables) {
        this.disposeEmitter.addListener(() => {
            for (let i = 0; i < disposables.length; i++) {
                disposables[i].dispose();
            }
        });
    }
    dispose() {
        isAffirmEnabled() && !this._isDisposable && Disposable.assertNotDisposable();
        affirm(!this._isDisposed, 'Disposable can only be disposed once');
        // Unregister any disposers that were responsible for a portion of ths instance's cleanup, since it is getting
        // disposed. Do this before disposing this class.
        if (this.disposerMap) {
            for (const [disposerActionType] of this.disposerMap) {
                this.removeAllDisposerActions(disposerActionType);
            }
        }
        // TODO: Emit in reverse order so that tear-downs happen in the opposite order of setup. separate issue, see https://github.com/phetsims/axon/issues/456
        this._disposeEmitter.emit();
        this._disposeEmitter.dispose();
        this._isDisposed = true;
    }
    /**
     * Adds an action for the target to the disposeEmitter for the given disposer, so that when the disposer is disposed,
     * the unregisterAction related to this instance is also called. For instance, the unregisterAction would be the
     * corresponding removal/unlink/removeInputListener.
     * It is assumed that the target and unregisterAction are related to the memory management of this instance.
     */
    addDisposerAction(disposerActionType, target, disposer, unregisterAction) {
        this.disposerMap = this.disposerMap || new Map();
        if (!this.disposerMap.has(disposerActionType)) {
            this.disposerMap.set(disposerActionType, new Map());
        }
        const entries = this.disposerMap.get(disposerActionType);
        affirm(!entries.has(target), `Target already registered for disposer action type: ${disposerActionType}`, target);
        // Function that will unregister the disposer action for the target.
        const unregisterActionCallback = () => {
            entries.delete(target);
            unregisterAction();
        };
        entries.set(target, { disposer: disposer, unregisterCallback: unregisterActionCallback });
        disposer.disposeEmitter.addListener(unregisterActionCallback);
    }
    /**
     * Remove the target on the disposer's disposeEmitter. This will not call the unregistration.
     */
    removeDisposerAction(disposerActionType, target) {
        if (this.disposerMap) {
            const disposers = this.disposerMap.get(disposerActionType);
            if (disposers) {
                const entry = disposers.get(target);
                // If there is no entry, that means either:
                // a) this particular target was never registered with a disposer, which is fine.
                // b) or it has already been removed via the disposeEmitter or elsewhere.
                // Graceful is best here to avoid disposal order dependencies.
                if (entry) {
                    entry.disposer.disposeEmitter.removeListener(entry.unregisterCallback);
                    disposers.delete(target);
                }
                // NOTE: Do not delete any of the Maps, as they may be used again in the future.
            }
        }
    }
    // Remove all disposer actions for a given type
    removeAllDisposerActions(disposerActionType) {
        if (this.disposerMap) {
            const disposers = this.disposerMap.get(disposerActionType);
            disposers && disposers.forEach((_, target) => this.removeDisposerAction(disposerActionType, target));
        }
    }
    static assertNotDisposable() {
        // eslint-disable-next-line phet/bad-sim-text
        affirm(false, 'dispose is not supported, exists for the lifetime of the sim');
    }
}
axon.register('Disposable', Disposable);
export default Disposable;
//# sourceMappingURL=Disposable.js.map