import _ from 'lodash';
import '../../../assert/js/assert.js';
import '../../../globals.js';
// Copyright 2017-2024, University of Colorado Boulder
/**
 * Listens to presses (down events), attaching a listener to the pointer when one occurs, so that a release (up/cancel
 * or interruption) can be recorded.
 *
 * This is the base type for both DragListener and FireListener, which contains the shared logic that would be needed
 * by both.
 *
 * PressListener is fine to use directly, particularly when drag-coordinate information is needed (e.g. DragListener),
 * or if the interaction is more complicated than a simple button fire (e.g. FireListener).
 *
 * For example usage, see scenery/examples/input.html. Additionally, a typical "simple" PressListener direct usage
 * would be something like:
 *
 *   someNode.addInputListener( new PressListener( {
 *     press: () => { ... },
 *     release: () => { ... }
 *   } ) );
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */
import BooleanProperty from '../../../axon/js/BooleanProperty.js';
import createObservableArray from '../../../axon/js/createObservableArray.js';
import DerivedProperty from '../../../axon/js/DerivedProperty.js';
import EnabledComponent from '../../../axon/js/EnabledComponent.js';
import stepTimer from '../../../axon/js/stepTimer.js';
import optionize from '../../../phet-core/js/optionize.js';
import EventType from '../../../tandem/js/EventType.js';
import PhetioAction from '../../../tandem/js/PhetioAction.js';
import PhetioObject from '../../../tandem/js/PhetioObject.js';
import Tandem from '../../../tandem/js/Tandem.js';
import NullableIO from '../../../tandem/js/types/NullableIO.js';
import { Mouse, Node, scenery, SceneryEvent } from '../imports.js';
// global
let globalID = 0;
// Factor out to reduce memory footprint, see https://github.com/phetsims/tandem/issues/71
const truePredicate = _.constant(true);
const isPressedListener = (listener) => listener.isPressed;
class PressListener extends EnabledComponent {
    constructor(providedOptions) {
        const options = optionize()({
            press: _.noop,
            release: _.noop,
            targetNode: null,
            drag: _.noop,
            attach: true,
            mouseButton: 0,
            pressCursor: 'pointer',
            useInputListenerCursor: false,
            canStartPress: truePredicate,
            a11yLooksPressedInterval: 100,
            collapseDragEvents: false,
            phetioPressActionInstrumented: true,
            phetioReleaseActionInstrumented: true,
            // EnabledComponent
            // By default, PressListener does not have an instrumented enabledProperty, but you can opt in with this option.
            phetioEnabledPropertyInstrumented: false,
            // phet-io (EnabledComponent)
            // For PhET-iO instrumentation. If only using the PressListener for hover behavior, there is no need to
            // instrument because events are only added to the data stream for press/release and not for hover events. Please pass
            // Tandem.OPT_OUT as the tandem option to not instrument an instance.
            tandem: Tandem.REQUIRED,
            phetioReadOnly: true,
            phetioFeatured: PhetioObject.DEFAULT_OPTIONS.phetioFeatured
        }, providedOptions);
        assert && assert(typeof options.mouseButton === 'number' && options.mouseButton >= 0 && options.mouseButton % 1 === 0, 'mouseButton should be a non-negative integer');
        assert && assert(options.pressCursor === null || typeof options.pressCursor === 'string', 'pressCursor should either be a string or null');
        assert && assert(typeof options.press === 'function', 'The press callback should be a function');
        assert && assert(typeof options.release === 'function', 'The release callback should be a function');
        assert && assert(typeof options.drag === 'function', 'The drag callback should be a function');
        assert && assert(options.targetNode === null || options.targetNode instanceof Node, 'If provided, targetNode should be a Node');
        assert && assert(typeof options.attach === 'boolean', 'attach should be a boolean');
        assert && assert(typeof options.a11yLooksPressedInterval === 'number', 'a11yLooksPressedInterval should be a number');
        super(options);
        this._id = globalID++;
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener(`PressListener#${this._id} construction`);
        this._mouseButton = options.mouseButton;
        this._a11yLooksPressedInterval = options.a11yLooksPressedInterval;
        this._pressCursor = options.pressCursor;
        this._pressListener = options.press;
        this._releaseListener = options.release;
        this._dragListener = options.drag;
        this._canStartPress = options.canStartPress;
        this._targetNode = options.targetNode;
        this._attach = options.attach;
        this._collapseDragEvents = options.collapseDragEvents;
        this.overPointers = createObservableArray();
        this.isPressedProperty = new BooleanProperty(false, { reentrant: true });
        this.isOverProperty = new BooleanProperty(false);
        this.looksOverProperty = new BooleanProperty(false);
        this.isHoveringProperty = new BooleanProperty(false);
        this.isHighlightedProperty = new BooleanProperty(false);
        this.isFocusedProperty = new BooleanProperty(false);
        this.cursorProperty = new DerivedProperty([this.enabledProperty], enabled => {
            if (options.useInputListenerCursor && enabled && this._attach) {
                return this._pressCursor;
            }
            else {
                return null;
            }
        });
        this.pointer = null;
        this.pressedTrail = null;
        this.interrupted = false;
        this._pendingCollapsedDragEvent = null;
        this._listeningToPointer = false;
        this._isHoveringListener = this.invalidateHovering.bind(this);
        this._isHighlightedListener = this.invalidateHighlighted.bind(this);
        this.pdomClickingProperty = new BooleanProperty(false);
        this.looksPressedProperty = DerivedProperty.or([this.pdomClickingProperty, this.isPressedProperty]);
        this._pdomClickingTimeoutListener = null;
        this._pointerListener = {
            up: this.pointerUp.bind(this),
            cancel: this.pointerCancel.bind(this),
            move: this.pointerMove.bind(this),
            interrupt: this.pointerInterrupt.bind(this),
            listener: this
        };
        this._pressAction = new PhetioAction(this.onPress.bind(this), {
            tandem: options.phetioPressActionInstrumented ? options.tandem.createTandem('pressAction') : Tandem.OPT_OUT,
            phetioDocumentation: 'Executes whenever a press occurs. The first argument when executing can be ' +
                'used to convey info about the SceneryEvent.',
            phetioReadOnly: options.phetioReadOnly,
            phetioFeatured: options.phetioFeatured,
            phetioEventType: EventType.USER,
            parameters: [{
                    name: 'event',
                    phetioType: SceneryEvent.SceneryEventIO
                }, {
                    phetioPrivate: true,
                    valueType: [Node, null]
                }, {
                    phetioPrivate: true,
                    valueType: ['function', null]
                }
            ]
        });
        this._releaseAction = new PhetioAction(this.onRelease.bind(this), {
            parameters: [{
                    name: 'event',
                    phetioType: NullableIO(SceneryEvent.SceneryEventIO)
                }, {
                    phetioPrivate: true,
                    valueType: ['function', null]
                }],
            // phet-io
            tandem: options.phetioReleaseActionInstrumented ? options.tandem.createTandem('releaseAction') : Tandem.OPT_OUT,
            phetioDocumentation: 'Executes whenever a release occurs.',
            phetioReadOnly: options.phetioReadOnly,
            phetioFeatured: options.phetioFeatured,
            phetioEventType: EventType.USER
        });
        this.display = null;
        this.boundInvalidateOverListener = this.invalidateOver.bind(this);
        // update isOverProperty (not a DerivedProperty because we need to hook to passed-in properties)
        this.overPointers.lengthProperty.link(this.invalidateOver.bind(this));
        this.isFocusedProperty.link(this.invalidateOver.bind(this));
        // update isHoveringProperty (not a DerivedProperty because we need to hook to passed-in properties)
        this.overPointers.lengthProperty.link(this._isHoveringListener);
        this.isPressedProperty.link(this._isHoveringListener);
        // Update isHovering when any pointer's isDownProperty changes.
        // NOTE: overPointers is cleared on dispose, which should remove all of these (interior) listeners)
        this.overPointers.addItemAddedListener(pointer => pointer.isDownProperty.link(this._isHoveringListener));
        this.overPointers.addItemRemovedListener(pointer => pointer.isDownProperty.unlink(this._isHoveringListener));
        // update isHighlightedProperty (not a DerivedProperty because we need to hook to passed-in properties)
        this.isHoveringProperty.link(this._isHighlightedListener);
        this.isPressedProperty.link(this._isHighlightedListener);
        this.enabledProperty.lazyLink(this.onEnabledPropertyChange.bind(this));
    }
    /**
     * Whether this listener is currently activated with a press.
     */
    get isPressed() {
        return this.isPressedProperty.value;
    }
    get cursor() {
        return this.cursorProperty.value;
    }
    get attach() {
        return this._attach;
    }
    get targetNode() {
        return this._targetNode;
    }
    /**
     * The main node that this listener is responsible for dragging.
     */
    getCurrentTarget() {
        assert && assert(this.isPressed, 'We have no currentTarget if we are not pressed');
        return this.pressedTrail.lastNode();
    }
    get currentTarget() {
        return this.getCurrentTarget();
    }
    /**
     * Returns whether a press can be started with a particular event.
     */
    canPress(event) {
        return !!this.enabledProperty.value &&
            !this.isPressed &&
            this._canStartPress(event, this) &&
            // Only let presses be started with the correct mouse button.
            // @ts-expect-error Typed SceneryEvent
            (!(event.pointer instanceof Mouse) || event.domEvent.button === this._mouseButton) &&
            // We can't attach to a pointer that is already attached.
            (!this._attach || !event.pointer.isAttached());
    }
    /**
     * Returns whether this PressListener can be clicked from keyboard input. This copies part of canPress, but
     * we didn't want to use canClick in canPress because canClick could be overridden in subtypes.
     */
    canClick() {
        // If this listener is already involved in pressing something (or our options predicate returns false) we can't
        // press something.
        return this.enabledProperty.value && !this.isPressed && this._canStartPress(null, this);
    }
    /**
     * Moves the listener to the 'pressed' state if possible (attaches listeners and initializes press-related
     * properties).
     *
     * This can be overridden (with super-calls) when custom press behavior is needed for a type.
     *
     * This can be called by outside clients in order to try to begin a process (generally on an already-pressed
     * pointer), and is useful if a 'drag' needs to change between listeners. Use canPress( event ) to determine if
     * a press can be started (if needed beforehand).
     *
     * @param event
     * @param [targetNode] - If provided, will take the place of the targetNode for this call. Useful for
     *                              forwarded presses.
     * @param [callback] - to be run at the end of the function, but only on success
     * @returns success - Returns whether the press was actually started
     */
    press(event, targetNode, callback) {
        assert && assert(event, 'An event is required');
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener(`PressListener#${this._id} press`);
        if (!this.canPress(event)) {
            sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener(`PressListener#${this._id} could not press`);
            return false;
        }
        // Flush out a pending drag, so it happens before we press
        this.flushCollapsedDrag();
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener(`PressListener#${this._id} successful press`);
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        this._pressAction.execute(event, targetNode || null, callback || null); // cannot pass undefined into execute call
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
        return true;
    }
    /**
     * Releases a pressed listener.
     *
     * This can be overridden (with super-calls) when custom release behavior is needed for a type.
     *
     * This can be called from the outside to release the press without the pointer having actually fired any 'up'
     * events. If the cancel/interrupt behavior is more preferable, call interrupt() on this listener instead.
     *
     * @param [event] - scenery event if there was one. We can't guarantee an event, in part to support interrupting.
     * @param [callback] - called at the end of the release
     */
    release(event, callback) {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener(`PressListener#${this._id} release`);
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        // Flush out a pending drag, so it happens before we release
        this.flushCollapsedDrag();
        this._releaseAction.execute(event || null, callback || null); // cannot pass undefined to execute call
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
    }
    /**
     * Called when move events are fired on the attached pointer listener.
     *
     * This can be overridden (with super-calls) when custom drag behavior is needed for a type.
     *
     * (scenery-internal, effectively protected)
     */
    drag(event) {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener(`PressListener#${this._id} drag`);
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        // If we got interrupted while events were queued up, we MAY get a drag when not pressed. We can ignore this.
        if (!this.isPressed) {
            return;
        }
        this._dragListener(event, this);
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
    }
    /**
     * Interrupts the listener, releasing it (canceling behavior).
     *
     * This effectively releases/ends the press, and sets the `interrupted` flag to true while firing these events
     * so that code can determine whether a release/end happened naturally, or was canceled in some way.
     *
     * This can be called manually, but can also be called through node.interruptSubtreeInput().
     */
    interrupt() {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener(`PressListener#${this._id} interrupt`);
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        // handle pdom interrupt
        if (this.pdomClickingProperty.value) {
            this.interrupted = true;
            // it is possible we are interrupting a click with a pointer press, in which case
            // we are listening to the Pointer listener - do a full release in this case
            if (this._listeningToPointer) {
                this.release();
            }
            else {
                // release on interrupt (without going through onRelease, which handles mouse/touch specific things)
                this.isPressedProperty.value = false;
                this._releaseListener(null, this);
            }
            // clear the clicking timer, specific to pdom input
            // @ts-expect-error TODO: This looks buggy, will need to ignore for now https://github.com/phetsims/scenery/issues/1581
            if (stepTimer.hasListener(this._pdomClickingTimeoutListener)) {
                // @ts-expect-error TODO: This looks buggy, will need to ignore for now https://github.com/phetsims/scenery/issues/1581
                stepTimer.clearTimeout(this._pdomClickingTimeoutListener);
                // interrupt may be called after the PressListener has been disposed (for instance, internally by scenery
                // if the Node receives a blur event after the PressListener is disposed)
                if (!this.pdomClickingProperty.isDisposed) {
                    this.pdomClickingProperty.value = false;
                }
            }
        }
        else if (this.isPressed) {
            // handle pointer interrupt
            sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener(`PressListener#${this._id} interrupting`);
            this.interrupted = true;
            this.release();
        }
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
    }
    /**
     * This should be called when the listened "Node" is effectively removed from the scene graph AND
     * expected to be placed back in such that it could potentially get multiple "enter" events, see
     * https://github.com/phetsims/scenery/issues/1021
     *
     * This will clear the list of pointers considered "over" the Node, so that when it is placed back in, the state
     * will be correct, and another "enter" event will not be missing an "exit".
     */
    clearOverPointers() {
        this.overPointers.clear(); // We have listeners that will trigger the proper refreshes
    }
    /**
     * If collapseDragEvents is set to true, this step() should be called every frame so that the collapsed drag
     * can be fired.
     */
    step() {
        this.flushCollapsedDrag();
    }
    /**
     * Set the callback that will create a Bounds2 in the global coordinate frame for the AnimatedPanZoomListener to
     * keep in view during a drag operation. During drag input the AnimatedPanZoomListener will pan the screen to
     * try and keep the returned Bounds2 visible. By default, the AnimatedPanZoomListener will try to keep the target of
     * the drag in view but that may not always work if the target is not associated with the translated Node, the target
     * is not defined, or the target has bounds that do not accurately surround the graphic you want to keep in view.
     */
    setCreatePanTargetBounds(createDragPanTargetBounds) {
        // Forwarded to the pointerListener so that the AnimatedPanZoomListener can get this callback from the attached
        // listener
        this._pointerListener.createPanTargetBounds = createDragPanTargetBounds;
    }
    set createPanTargetBounds(createDragPanTargetBounds) { this.setCreatePanTargetBounds(createDragPanTargetBounds); }
    /**
     * A convenient way to create and set the callback that will return a Bounds2 in the global coordinate frame for the
     * AnimatedPanZoomListener to keep in view during a drag operation. The AnimatedPanZoomListener will try to keep the
     * bounds of the last Node of the provided trail visible by panning the screen during a drag operation. See
     * setCreatePanTargetBounds() for more documentation.
     */
    setCreatePanTargetBoundsFromTrail(trail) {
        assert && assert(trail.length > 0, 'trail has no Nodes to provide localBounds');
        this.setCreatePanTargetBounds(() => trail.localToGlobalBounds(trail.lastNode().localBounds));
    }
    set createPanTargetBoundsFromTrail(trail) { this.setCreatePanTargetBoundsFromTrail(trail); }
    /**
     * If there is a pending collapsed drag waiting, we'll fire that drag (usually before other events or during a step)
     */
    flushCollapsedDrag() {
        if (this._pendingCollapsedDragEvent) {
            this.drag(this._pendingCollapsedDragEvent);
        }
        this._pendingCollapsedDragEvent = null;
    }
    /**
     * Recomputes the value for isOverProperty. Separate to reduce anonymous function closures.
     */
    invalidateOver() {
        let pointerAttachedToOther = false;
        if (this._listeningToPointer) {
            // this pointer listener is attached to the pointer
            pointerAttachedToOther = false;
        }
        else {
            // a listener other than this one is attached to the pointer so it should not be considered over
            for (let i = 0; i < this.overPointers.length; i++) {
                if (this.overPointers.get(i).isAttached()) {
                    pointerAttachedToOther = true;
                    break;
                }
            }
        }
        // isOverProperty is only for the `over` event, looksOverProperty includes focused pressListeners (only when the
        // display is showing focus highlights)
        this.isOverProperty.value = (this.overPointers.length > 0 && !pointerAttachedToOther);
        this.looksOverProperty.value = this.isOverProperty.value ||
            (this.isFocusedProperty.value && !!this.display && this.display.focusManager.pdomFocusHighlightsVisibleProperty.value);
    }
    /**
     * Recomputes the value for isHoveringProperty. Separate to reduce anonymous function closures.
     */
    invalidateHovering() {
        for (let i = 0; i < this.overPointers.length; i++) {
            const pointer = this.overPointers[i];
            if (!pointer.isDown || pointer === this.pointer) {
                this.isHoveringProperty.value = true;
                return;
            }
        }
        this.isHoveringProperty.value = false;
    }
    /**
     * Recomputes the value for isHighlightedProperty. Separate to reduce anonymous function closures.
     */
    invalidateHighlighted() {
        this.isHighlightedProperty.value = this.isHoveringProperty.value || this.isPressedProperty.value;
    }
    /**
     * Fired when the enabledProperty changes
     */
    onEnabledPropertyChange(enabled) {
        !enabled && this.interrupt();
    }
    /**
     * Internal code executed as the first step of a press.
     *
     * @param event
     * @param [targetNode] - If provided, will take the place of the targetNode for this call. Useful for
     *                              forwarded presses.
     * @param [callback] - to be run at the end of the function, but only on success
     */
    onPress(event, targetNode, callback) {
        assert && assert(!this.isDisposed, 'Should not press on a disposed listener');
        const givenTargetNode = targetNode || this._targetNode;
        // Set this properties before the property change, so they are visible to listeners.
        this.pointer = event.pointer;
        this.pressedTrail = givenTargetNode ? givenTargetNode.getUniqueTrail() : event.trail.subtrailTo(event.currentTarget, false);
        this.interrupted = false; // clears the flag (don't set to false before here)
        this.pointer.addInputListener(this._pointerListener, this._attach);
        this._listeningToPointer = true;
        this.pointer.cursor = this.pressedTrail.lastNode().getEffectiveCursor() || this._pressCursor;
        this.isPressedProperty.value = true;
        // Notify after everything else is set up
        this._pressListener(event, this);
        callback && callback();
    }
    /**
     * Internal code executed as the first step of a release.
     *
     * @param event - scenery event if there was one
     * @param [callback] - called at the end of the release
     */
    onRelease(event, callback) {
        assert && assert(this.isPressed, 'This listener is not pressed');
        const pressedListener = this;
        pressedListener.pointer.removeInputListener(this._pointerListener);
        this._listeningToPointer = false;
        // Set the pressed state false *before* invoking the callback, otherwise an infinite loop can result in some
        // circumstances.
        this.isPressedProperty.value = false;
        // Notify after the rest of release is called in order to prevent it from triggering interrupt().
        this._releaseListener(event, this);
        callback && callback();
        // These properties are cleared now, at the end of the onRelease, in case they were needed by the callback or in
        // listeners on the pressed Property.
        pressedListener.pointer.cursor = null;
        this.pointer = null;
        this.pressedTrail = null;
    }
    /**
     * Called with 'down' events (part of the listener API). (scenery-internal)
     *
     * NOTE: Do not call directly. See the press method instead.
     */
    down(event) {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener(`PressListener#${this._id} down`);
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        this.press(event);
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
    }
    /**
     * Called with 'up' events (part of the listener API). (scenery-internal)
     *
     * NOTE: Do not call directly.
     */
    up(event) {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener(`PressListener#${this._id} up`);
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        // Recalculate over/hovering Properties.
        this.invalidateOver();
        this.invalidateHovering();
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
    }
    /**
     * Called with 'enter' events (part of the listener API). (scenery-internal)
     *
     * NOTE: Do not call directly.
     */
    enter(event) {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener(`PressListener#${this._id} enter`);
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        this.overPointers.push(event.pointer);
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
    }
    /**
     * Called with `move` events (part of the listener API). It is necessary to check for `over` state changes on move
     * in case a pointer listener gets interrupted and resumes movement over a target. (scenery-internal)
     */
    move(event) {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener(`PressListener#${this._id} move`);
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        this.invalidateOver();
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
    }
    /**
     * Called with 'exit' events (part of the listener API). (scenery-internal)
     *
     * NOTE: Do not call directly.
     */
    exit(event) {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener(`PressListener#${this._id} exit`);
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        // NOTE: We don't require the pointer to be included here, since we may have added the listener after the 'enter'
        // was fired. See https://github.com/phetsims/area-model-common/issues/159 for more details.
        if (this.overPointers.includes(event.pointer)) {
            this.overPointers.remove(event.pointer);
        }
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
    }
    /**
     * Called with 'up' events from the pointer (part of the listener API) (scenery-internal)
     *
     * NOTE: Do not call directly.
     */
    pointerUp(event) {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener(`PressListener#${this._id} pointer up`);
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        // Since our callback can get queued up and THEN interrupted before this happens, we'll check to make sure we are
        // still pressed by the time we get here. If not pressed, then there is nothing to do.
        // See https://github.com/phetsims/capacitor-lab-basics/issues/251
        if (this.isPressed) {
            assert && assert(event.pointer === this.pointer);
            this.release(event);
        }
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
    }
    /**
     * Called with 'cancel' events from the pointer (part of the listener API) (scenery-internal)
     *
     * NOTE: Do not call directly.
     */
    pointerCancel(event) {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener(`PressListener#${this._id} pointer cancel`);
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        // Since our callback can get queued up and THEN interrupted before this happens, we'll check to make sure we are
        // still pressed by the time we get here. If not pressed, then there is nothing to do.
        // See https://github.com/phetsims/capacitor-lab-basics/issues/251
        if (this.isPressed) {
            assert && assert(event.pointer === this.pointer);
            this.interrupt(); // will mark as interrupted and release()
        }
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
    }
    /**
     * Called with 'move' events from the pointer (part of the listener API) (scenery-internal)
     *
     * NOTE: Do not call directly.
     */
    pointerMove(event) {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener(`PressListener#${this._id} pointer move`);
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        // Since our callback can get queued up and THEN interrupted before this happens, we'll check to make sure we are
        // still pressed by the time we get here. If not pressed, then there is nothing to do.
        // See https://github.com/phetsims/capacitor-lab-basics/issues/251
        if (this.isPressed) {
            assert && assert(event.pointer === this.pointer);
            if (this._collapseDragEvents) {
                this._pendingCollapsedDragEvent = event;
            }
            else {
                this.drag(event);
            }
        }
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
    }
    /**
     * Called when the pointer needs to interrupt its current listener (usually so another can be added). (scenery-internal)
     *
     * NOTE: Do not call directly.
     */
    pointerInterrupt() {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener(`PressListener#${this._id} pointer interrupt`);
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        this.interrupt();
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
    }
    /**
     * Click listener, called when this is treated as an accessible input listener.
     * In general not needed to be public, but just used in edge cases to get proper click logic for pdom.
     *
     * Handle the click event from DOM for PDOM. Clicks by calling press and release immediately.
     * When assistive technology is used, the browser may not receive 'keydown' or 'keyup' events on input elements, but
     * only a single 'click' event. We need to toggle the pressed state from the single 'click' event.
     *
     * This will fire listeners immediately, but adds a delay for the pdomClickingProperty so that you can make a
     * button look pressed from a single DOM click event. For example usage, see sun/ButtonModel.looksPressedProperty.
     *
     * @param event
     * @param [callback] optionally called immediately after press, but only on successful click
     * @returns success - Returns whether the press was actually started
     */
    click(event, callback) {
        if (this.canClick()) {
            this.interrupted = false; // clears the flag (don't set to false before here)
            this.pdomClickingProperty.value = true;
            // ensure that button is 'focused' so listener can be called while button is down
            this.isFocusedProperty.value = true;
            this.isPressedProperty.value = true;
            // fire the optional callback
            // @ts-expect-error
            this._pressListener(event, this);
            callback && callback();
            // no longer down, don't reset 'over' so button can be styled as long as it has focus
            this.isPressedProperty.value = false;
            // fire the callback from options
            this._releaseListener(event, this);
            // if we are already clicking, remove the previous timeout - this assumes that clearTimeout is a noop if the
            // listener is no longer attached
            // @ts-expect-error TODO: This looks buggy, will need to ignore for now https://github.com/phetsims/scenery/issues/1581
            stepTimer.clearTimeout(this._pdomClickingTimeoutListener);
            // Now add the timeout back to start over, saving so that it can be removed later. Even when this listener was
            // interrupted from above logic, we still delay setting this to false to support visual "pressing" redraw.
            // @ts-expect-error TODO: This looks buggy, will need to ignore for now https://github.com/phetsims/scenery/issues/1581
            this._pdomClickingTimeoutListener = stepTimer.setTimeout(() => {
                // the listener may have been disposed before the end of a11yLooksPressedInterval, like if it fires and
                // disposes itself immediately
                if (!this.pdomClickingProperty.isDisposed) {
                    this.pdomClickingProperty.value = false;
                }
            }, this._a11yLooksPressedInterval);
        }
        return true;
    }
    /**
     * Focus listener, called when this is treated as an accessible input listener and its target is focused. (scenery-internal)
     * @pdom
     */
    focus(event) {
        // Get the Display related to this accessible event.
        const accessibleDisplays = event.trail.rootNode().getRootedDisplays().filter(display => display.isAccessible());
        assert && assert(accessibleDisplays.length === 1, 'cannot focus node with zero or multiple accessible displays attached');
        //
        this.display = accessibleDisplays[0];
        if (!this.display.focusManager.pdomFocusHighlightsVisibleProperty.hasListener(this.boundInvalidateOverListener)) {
            this.display.focusManager.pdomFocusHighlightsVisibleProperty.link(this.boundInvalidateOverListener);
        }
        // On focus, button should look 'over'.
        this.isFocusedProperty.value = true;
    }
    /**
     * Blur listener, called when this is treated as an accessible input listener.
     * @pdom
     */
    blur() {
        if (this.display) {
            if (this.display.focusManager.pdomFocusHighlightsVisibleProperty.hasListener(this.boundInvalidateOverListener)) {
                this.display.focusManager.pdomFocusHighlightsVisibleProperty.unlink(this.boundInvalidateOverListener);
            }
            this.display = null;
        }
        // On blur, the button should no longer look 'over'.
        this.isFocusedProperty.value = false;
    }
    /**
     * Disposes the listener, releasing references. It should not be used after this.
     */
    dispose() {
        sceneryLog && sceneryLog.InputListener && sceneryLog.InputListener(`PressListener#${this._id} dispose`);
        sceneryLog && sceneryLog.InputListener && sceneryLog.push();
        // We need to release references to any pointers that are over us.
        this.overPointers.clear();
        if (this._listeningToPointer && isPressedListener(this)) {
            this.pointer.removeInputListener(this._pointerListener);
        }
        // These Properties could have already been disposed, for example in the sun button hierarchy, see https://github.com/phetsims/sun/issues/372
        if (!this.isPressedProperty.isDisposed) {
            this.isPressedProperty.unlink(this._isHighlightedListener);
            this.isPressedProperty.unlink(this._isHoveringListener);
        }
        !this.isHoveringProperty.isDisposed && this.isHoveringProperty.unlink(this._isHighlightedListener);
        this._pressAction.dispose();
        this._releaseAction.dispose();
        this.looksPressedProperty.dispose();
        this.pdomClickingProperty.dispose();
        this.cursorProperty.dispose();
        this.isFocusedProperty.dispose();
        this.isHighlightedProperty.dispose();
        this.isHoveringProperty.dispose();
        this.looksOverProperty.dispose();
        this.isOverProperty.dispose();
        this.isPressedProperty.dispose();
        this.overPointers.dispose();
        // Remove references to the stored display, if we have any.
        if (this.display) {
            this.display.focusManager.pdomFocusHighlightsVisibleProperty.unlink(this.boundInvalidateOverListener);
            this.display = null;
        }
        super.dispose();
        sceneryLog && sceneryLog.InputListener && sceneryLog.pop();
    }
}
PressListener.phetioAPI = {
    pressAction: { phetioType: PhetioAction.PhetioActionIO([SceneryEvent.SceneryEventIO]) },
    releaseAction: { phetioType: PhetioAction.PhetioActionIO([NullableIO(SceneryEvent.SceneryEventIO)]) }
};
export default PressListener;
scenery.register('PressListener', PressListener);
//# sourceMappingURL=PressListener.js.map