// Copyright 2014-2025, University of Colorado Boulder
/**
 * Base class for button models, which describe the behavior of buttons when users interact with them.  Property values
 * are set by an associated listener, see PressListener for details.
 *
 * @author John Blanco (PhET Interactive Simulations)
 */
import '../../../globals.js';
import _ from 'lodash';
import BooleanProperty from '../../../axon/js/BooleanProperty.js';
import Emitter from '../../../axon/js/Emitter.js';
import EnabledComponent from '../../../axon/js/EnabledComponent.js';
import Multilink from '../../../axon/js/Multilink.js';
import optionize, { combineOptions } from '../../../phet-core/js/optionize.js';
import PressListener from '../../../scenery/js/listeners/PressListener.js';
import PhetioObject from '../../../tandem/js/PhetioObject.js';
import Tandem from '../../../tandem/js/Tandem.js';
export default class ButtonModel extends EnabledComponent {
    constructor(providedOptions) {
        const options = optionize()({
            startCallback: _.noop,
            endCallback: _.noop,
            // phet-io
            tandem: Tandem.REQUIRED,
            phetioState: PhetioObject.DEFAULT_OPTIONS.phetioState,
            phetioReadOnly: PhetioObject.DEFAULT_OPTIONS.phetioReadOnly,
            phetioFeatured: PhetioObject.DEFAULT_OPTIONS.phetioFeatured
        }, providedOptions);
        // Set up enabledPropertyOptions for the enabledProperty that the mixin might create
        options.enabledPropertyOptions = combineOptions({
            // phet-io
            phetioState: options.phetioState,
            phetioReadOnly: options.phetioReadOnly,
            phetioDocumentation: 'When disabled, the button is grayed out and cannot be pressed',
            phetioFeatured: true
        }, options.enabledPropertyOptions);
        super(options);
        // model Properties
        this.overProperty = new BooleanProperty(false);
        this.downProperty = new BooleanProperty(false, { reentrant: true });
        this.pdomClickingProperty = new BooleanProperty(false);
        this.focusedProperty = new BooleanProperty(false);
        this.looksPressedProperty = new BooleanProperty(false);
        this.isOverOrFocusedProperty = new BooleanProperty(false);
        this.produceSoundEmitter = new Emitter();
        this.interrupted = false;
        this.listeners = [];
        // Links all of the looksPressedProperties from the listeners that were created
        // by this ButtonModel, and updates the looksPressedProperty accordingly. First Multilink is added when the
        // first listener is created. See this.createPressListener.
        this.looksPressedMultilink = null;
        // Links all of the isOverOrFocusedProperties from the listeners that were created
        // by this ButtonModel, and updates the isOverOrFocusedProperty accordingly. First Multilink is added when the
        // first listener is created. See this.createPressListener.
        this.overOrFocusedMultilink = null;
        // Call startCallback on pointer down, endCallback on pointer up. Use lazyLink so they aren't called immediately.
        // No unlink needed since this button model owns the Property.
        this.downProperty.lazyLink(down => {
            if (down) {
                options.startCallback();
            }
            else {
                options.endCallback(this.isOverOrFocusedProperty.get());
            }
        });
        // Interrupt input listeners when enabled is set to false. This is the equivalent of Node.interruptSubtreeInput,
        // but ButtonModel is not a Node, so we have to interrupt each listener. See https://github.com/phetsims/sun/issues/642.
        this.enabledProperty.link(enabled => {
            if (!enabled) {
                for (let i = 0; i < this.listeners.length; i++) {
                    const listener = this.listeners[i];
                    listener.interrupt && listener.interrupt();
                }
            }
        });
        this.disposeButtonModel = () => {
            // This will unlink all listeners, causing potential issues if listeners try to unlink Properties afterwards
            this.overProperty.dispose();
            this.downProperty.dispose();
            this.pdomClickingProperty.dispose();
            this.produceSoundEmitter.dispose();
            this.looksPressedMultilink && this.looksPressedMultilink.dispose();
            this.overOrFocusedMultilink && this.overOrFocusedMultilink.dispose();
            this.listeners = [];
        };
    }
    dispose() {
        this.disposeButtonModel();
        super.dispose();
    }
    /**
     * Creates a PressListener that will handle changes to ButtonModel when the associated button Node is pressed.
     * The client is responsible for adding this PressListener to the associated button Node.
     */
    createPressListener(options) {
        options = combineOptions({
            canStartPress: () => this.enabledProperty.value
        }, options);
        const pressListener = new PressListener(options);
        this.listeners.push(pressListener);
        // Link lazily in case client externally sets downProperty - don't update until the next press.  No unlink needed
        // because the pressListener is local.
        pressListener.isPressedProperty.lazyLink(isPressed => {
            // determine interrupted first so listeners on downProperty have access
            this.interrupted = pressListener.interrupted;
            this.downProperty.set(isPressed);
        });
        pressListener.isOverProperty.lazyLink(this.overProperty.set.bind(this.overProperty));
        pressListener.isFocusedProperty.lazyLink(this.focusedProperty.set.bind(this.focusedProperty));
        pressListener.pdomClickingProperty.lazyLink(this.pdomClickingProperty.set.bind(this.pdomClickingProperty));
        // dispose the previous multilink in case we already created a PressListener with this model
        this.looksPressedMultilink && this.looksPressedMultilink.dispose();
        this.overOrFocusedMultilink && this.overOrFocusedMultilink.dispose();
        // the downProperty is included because it can be set externally, looksPressedProperty should update in this case
        const looksPressedProperties = this.listeners.map(listener => listener.looksPressedProperty);
        looksPressedProperties.push(this.downProperty);
        // assign a new Multilink (for disposal), and make sure that the button looks pressed when any of the
        // PressListeners created by this ButtonModel look pressed.
        this.looksPressedMultilink = Multilink.multilinkAny(looksPressedProperties, (...args) => {
            this.looksPressedProperty.value = _.reduce(args, (sum, newValue) => sum || newValue, false);
        });
        const overOrFocusedProperties = this.listeners.map(listener => listener.isOverOrFocusedProperty);
        // assign a new Multilink (for disposal), and make sure that the button looks over when any of the
        // PressListeners created by this ButtonModel look over. Note that this cannot be an arrow function
        // because its implementation relies on arguments.
        this.overOrFocusedMultilink = Multilink.multilinkAny(overOrFocusedProperties, (...args) => {
            this.isOverOrFocusedProperty.value = _.reduce(args, (sum, newValue) => sum || newValue, false);
        });
        return pressListener;
    }
}
//# sourceMappingURL=ButtonModel.js.map