// Copyright 2013-2025, University of Colorado Boulder
import '../../../globals.js';
import _ from 'lodash';
import stepTimer from '../../../axon/js/stepTimer.js';
import TinyEmitter from '../../../axon/js/TinyEmitter.js';
import Vector2 from '../../../dot/js/Vector2.js';
import cleanArray from '../../../phet-core/js/cleanArray.js';
import optionize from '../../../phet-core/js/optionize.js';
import platform from '../../../phet-core/js/platform.js';
import EventType from '../../../tandem/js/EventType.js';
import PhetioAction from '../../../tandem/js/PhetioAction.js';
import PhetioObject from '../../../tandem/js/PhetioObject.js';
import ArrayIO from '../../../tandem/js/types/ArrayIO.js';
import IOType from '../../../tandem/js/types/IOType.js';
import NullableIO from '../../../tandem/js/types/NullableIO.js';
import NumberIO from '../../../tandem/js/types/NumberIO.js';
import { pdomUniqueIdToTrail } from '../accessibility/pdom/pdomUniqueIdToTrail.js';
import PDOMUtils from '../accessibility/pdom/PDOMUtils.js';
import DisplayGlobals from '../display/DisplayGlobals.js';
import BatchedDOMEvent, { BatchedDOMEventType } from '../input/BatchedDOMEvent.js';
import BrowserEvents from '../input/BrowserEvents.js';
import EventContext, { EventContextIO } from '../input/EventContext.js';
import Mouse from '../input/Mouse.js';
import PDOMPointer from '../input/PDOMPointer.js';
import Pen from '../input/Pen.js';
import Pointer from '../input/Pointer.js';
import SceneryEvent from '../input/SceneryEvent.js';
import Touch from '../input/Touch.js';
import Trail from '../util/Trail.js';
import { TARGET_SUBSTITUTE_KEY } from './eventSerialization.js';
const ArrayIOPointerIO = ArrayIO(Pointer.PointerIO);
// A list of events that should still fire, even when the Node is not pickable
const PDOM_UNPICKABLE_EVENTS = ['focus', 'blur', 'focusin', 'focusout'];
// A bit more than the maximum amount of time that iOS 14 VoiceOver was observed to delay between
// sending a mouseup event and a click event.
const PDOM_CLICK_DELAY = 80;
class Input extends PhetioObject {
    /**
     * @param display
     * @param attachToWindow - Whether to add listeners to the self (instead of the Display's domElement).
     * @param batchDOMEvents - If true, most event types will be batched until otherwise triggered.
     * @param assumeFullWindow - We can optimize certain things like computing points if we know the display
     *                                     fills the entire self.
     * @param passiveEvents - See Display's documentation (controls the presence of the passive flag for
     *                                       events, which has some advanced considerations).
     *
     * @param [providedOptions]
     */
    constructor(display, attachToWindow, batchDOMEvents, assumeFullWindow, passiveEvents, preventMultitouch, interruptMultitouch, providedOptions) {
        const options = optionize()({
            phetioType: Input.InputIO,
            phetioDocumentation: 'Central point for user input events, such as mouse, touch'
        }, providedOptions);
        super(options);
        this.currentSceneryEvent = null;
        this.display = display;
        this.rootNode = display.rootNode;
        this.attachToWindow = attachToWindow;
        this.batchDOMEvents = batchDOMEvents;
        this.assumeFullWindow = assumeFullWindow;
        this.passiveEvents = passiveEvents;
        this.preventMultitouch = preventMultitouch;
        this.interruptMultitouch = interruptMultitouch;
        this.batchedEvents = [];
        this.pdomPointer = null;
        this.mouse = null;
        this.pointers = [];
        this.pointerAddedEmitter = new TinyEmitter();
        this.currentlyFiringEvents = false;
        this.upTimeStamp = 0;
        ////////////////////////////////////////////////////
        // Declare the Actions that send scenery input events to the PhET-iO data stream.  Note they use the default value
        // of phetioReadOnly false, in case a client wants to synthesize events.
        this.validatePointersAction = new PhetioAction(() => {
            let i = this.pointers.length;
            while (i--) {
                const pointer = this.pointers[i];
                if (pointer.point && pointer !== this.pdomPointer) {
                    this.branchChangeEvents(pointer, pointer.lastEventContext || EventContext.createSynthetic(), false);
                }
            }
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('validatePointersAction'),
            phetioHighFrequency: true
        });
        this.mouseUpAction = new PhetioAction((point, context) => {
            const mouse = this.ensureMouse(point, context);
            mouse.id = null;
            this.upEvent(mouse, context, point);
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('mouseUpAction'),
            parameters: [
                { name: 'point', phetioType: Vector2.Vector2IO },
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when a mouse button is released.'
        });
        this.mouseDownAction = new PhetioAction((id, point, context) => {
            if (this.interruptMultitouch) {
                this.interruptMultitouchPointers(point, context);
            }
            const mouse = this.ensureMouse(point, context);
            mouse.id = id;
            this.downEvent(mouse, context, point);
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('mouseDownAction'),
            parameters: [
                { name: 'id', phetioType: NullableIO(NumberIO) },
                { name: 'point', phetioType: Vector2.Vector2IO },
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when a mouse button is pressed.'
        });
        this.mouseMoveAction = new PhetioAction((point, context) => {
            const mouse = this.ensureMouse(point, context);
            mouse.move(point);
            this.moveEvent(mouse, context);
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('mouseMoveAction'),
            parameters: [
                { name: 'point', phetioType: Vector2.Vector2IO },
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when the mouse is moved.',
            phetioHighFrequency: true
        });
        this.mouseOverAction = new PhetioAction((point, context) => {
            const mouse = this.ensureMouse(point, context);
            mouse.over(point);
            // TODO: how to handle mouse-over (and log it)... are we changing the pointer.point without a branch change? https://github.com/phetsims/scenery/issues/1581
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('mouseOverAction'),
            parameters: [
                { name: 'point', phetioType: Vector2.Vector2IO },
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when the mouse is moved while on the sim.'
        });
        this.mouseOutAction = new PhetioAction((point, context) => {
            const mouse = this.ensureMouse(point, context);
            mouse.out(point);
            // TODO: how to handle mouse-out (and log it)... are we changing the pointer.point without a branch change? https://github.com/phetsims/scenery/issues/1581
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('mouseOutAction'),
            parameters: [
                { name: 'point', phetioType: Vector2.Vector2IO },
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when the mouse moves out of the display.'
        });
        this.wheelScrollAction = new PhetioAction((context) => {
            const event = context.domEvent;
            const mouse = this.ensureMouse(this.pointFromEvent(event), context);
            mouse.wheel(event);
            // don't send mouse-wheel events if we don't yet have a mouse location!
            // TODO: Can we set the mouse location based on the wheel event? https://github.com/phetsims/scenery/issues/1581
            if (mouse.point) {
                const trail = this.rootNode.trailUnderPointer(mouse) || new Trail(this.rootNode);
                this.dispatchEvent(trail, 'wheel', mouse, context, true);
            }
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('wheelScrollAction'),
            parameters: [
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when the mouse wheel scrolls.',
            phetioHighFrequency: true
        });
        this.touchStartAction = new PhetioAction((id, point, context) => {
            const touch = new Touch(id, point, context.domEvent);
            this.addPointer(touch, point, context);
            this.downEvent(touch, context, point);
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('touchStartAction'),
            parameters: [
                { name: 'id', phetioType: NumberIO },
                { name: 'point', phetioType: Vector2.Vector2IO },
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when a touch begins.'
        });
        this.touchEndAction = new PhetioAction((id, point, context) => {
            const touch = this.findPointerById(id);
            if (touch) {
                ; // eslint-disable-line phet/no-simple-type-checking-assertions, phet/bad-sim-text
                this.upEvent(touch, context, point);
                this.removePointer(touch);
            }
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('touchEndAction'),
            parameters: [
                { name: 'id', phetioType: NumberIO },
                { name: 'point', phetioType: Vector2.Vector2IO },
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when a touch ends.'
        });
        this.touchMoveAction = new PhetioAction((id, point, context) => {
            const touch = this.findPointerById(id);
            if (touch) {
                ; // eslint-disable-line phet/no-simple-type-checking-assertions, phet/bad-sim-text
                touch.move(point);
                this.moveEvent(touch, context);
            }
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('touchMoveAction'),
            parameters: [
                { name: 'id', phetioType: NumberIO },
                { name: 'point', phetioType: Vector2.Vector2IO },
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when a touch moves.',
            phetioHighFrequency: true
        });
        this.touchCancelAction = new PhetioAction((id, point, context) => {
            const touch = this.findPointerById(id);
            if (touch) {
                ; // eslint-disable-line phet/no-simple-type-checking-assertions, phet/bad-sim-text
                this.cancelEvent(touch, context, point);
                this.removePointer(touch);
            }
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('touchCancelAction'),
            parameters: [
                { name: 'id', phetioType: NumberIO },
                { name: 'point', phetioType: Vector2.Vector2IO },
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when a touch is canceled.'
        });
        this.penStartAction = new PhetioAction((id, point, context) => {
            const pen = new Pen(id, point, context.domEvent);
            this.addPointer(pen, point, context);
            this.downEvent(pen, context, point);
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('penStartAction'),
            parameters: [
                { name: 'id', phetioType: NumberIO },
                { name: 'point', phetioType: Vector2.Vector2IO },
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when a pen touches the screen.'
        });
        this.penEndAction = new PhetioAction((id, point, context) => {
            const pen = this.findPointerById(id);
            if (pen) {
                this.upEvent(pen, context, point);
                this.removePointer(pen);
            }
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('penEndAction'),
            parameters: [
                { name: 'id', phetioType: NumberIO },
                { name: 'point', phetioType: Vector2.Vector2IO },
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when a pen is lifted.'
        });
        this.penMoveAction = new PhetioAction((id, point, context) => {
            const pen = this.findPointerById(id);
            if (pen) {
                pen.move(point);
                this.moveEvent(pen, context);
            }
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('penMoveAction'),
            parameters: [
                { name: 'id', phetioType: NumberIO },
                { name: 'point', phetioType: Vector2.Vector2IO },
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when a pen is moved.',
            phetioHighFrequency: true
        });
        this.penCancelAction = new PhetioAction((id, point, context) => {
            const pen = this.findPointerById(id);
            if (pen) {
                this.cancelEvent(pen, context, point);
                this.removePointer(pen);
            }
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('penCancelAction'),
            parameters: [
                { name: 'id', phetioType: NumberIO },
                { name: 'point', phetioType: Vector2.Vector2IO },
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when a pen is canceled.'
        });
        this.gotPointerCaptureAction = new PhetioAction((id, context) => {
            const pointer = this.findPointerById(id);
            if (pointer) {
                pointer.onGotPointerCapture();
            }
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('gotPointerCaptureAction'),
            parameters: [
                { name: 'id', phetioType: NumberIO },
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when a pointer is captured (normally at the start of an interaction)',
            phetioHighFrequency: true
        });
        this.lostPointerCaptureAction = new PhetioAction((id, context) => {
            const pointer = this.findPointerById(id);
            if (pointer) {
                // While investigating https://github.com/phetsims/mean-share-and-balance/issues/336 it was discovered that
                // pointerUp events were not being transmitted before lostPointerCapture events by the browser. This was
                // surprising since the spec says otherwise: https://w3c.github.io/pointerevents/#implicit-release-of-pointer-capture
                // This behavior was found to be occurring while using a mac trackpad with finger movements that could
                // potentially be perceived as gesture. The following setTimeout gives the pointerUp event a chance to fire first
                // before the lostPointerCapture event so that listeners can rely on the expected order.
                stepTimer.setTimeout(() => { pointer.onLostPointerCapture(); }, 2);
            }
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('lostPointerCaptureAction'),
            parameters: [
                { name: 'id', phetioType: NumberIO },
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when a pointer loses its capture (normally at the end of an interaction)',
            phetioHighFrequency: true
        });
        this.focusinAction = new PhetioAction((context) => {
            const trail = this.getPDOMEventTrail(context.domEvent, 'focusin');
            if (!trail) {
                return;
            }
            ;
            ;
            this.dispatchPDOMEvent(trail, 'focus', context, false);
            this.dispatchPDOMEvent(trail, 'focusin', context, true);
            ;
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('focusinAction'),
            parameters: [
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when the PDOM root gets the focusin DOM event.'
        });
        this.focusoutAction = new PhetioAction((context) => {
            const trail = this.getPDOMEventTrail(context.domEvent, 'focusout');
            if (!trail) {
                return;
            }
            ;
            ;
            this.dispatchPDOMEvent(trail, 'blur', context, false);
            this.dispatchPDOMEvent(trail, 'focusout', context, true);
            ;
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('focusoutAction'),
            parameters: [
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when the PDOM root gets the focusout DOM event.'
        });
        // https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event notes that the click action should result
        // in a MouseEvent
        this.clickAction = new PhetioAction((context) => {
            const trail = this.getPDOMEventTrail(context.domEvent, 'click');
            if (!trail) {
                return;
            }
            ;
            ;
            this.dispatchPDOMEvent(trail, 'click', context, true);
            ;
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('clickAction'),
            parameters: [
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when the PDOM root gets the click DOM event.'
        });
        this.inputAction = new PhetioAction((context) => {
            const trail = this.getPDOMEventTrail(context.domEvent, 'input');
            if (!trail) {
                return;
            }
            ;
            ;
            this.dispatchPDOMEvent(trail, 'input', context, true);
            ;
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('inputAction'),
            parameters: [
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when the PDOM root gets the input DOM event.'
        });
        this.changeAction = new PhetioAction((context) => {
            const trail = this.getPDOMEventTrail(context.domEvent, 'change');
            if (!trail) {
                return;
            }
            ;
            ;
            this.dispatchPDOMEvent(trail, 'change', context, true);
            ;
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('changeAction'),
            parameters: [
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when the PDOM root gets the change DOM event.'
        });
        this.keydownAction = new PhetioAction((context) => {
            ;
            ;
            const trail = this.getPDOMEventTrail(context.domEvent, 'keydown');
            trail && this.dispatchPDOMEvent(trail, 'keydown', context, true);
            ;
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('keydownAction'),
            parameters: [
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when the PDOM root gets the keydown DOM event.'
        });
        this.keyupAction = new PhetioAction((context) => {
            ;
            ;
            const trail = this.getPDOMEventTrail(context.domEvent, 'keydown');
            trail && this.dispatchPDOMEvent(trail, 'keyup', context, true);
            ;
        }, {
            phetioPlayback: true,
            tandem: options.tandem?.createTandem('keyupAction'),
            parameters: [
                { name: 'context', phetioType: EventContextIO }
            ],
            phetioEventType: EventType.USER,
            phetioDocumentation: 'Emits when the PDOM root gets the keyup DOM event.'
        });
    }
    /**
     * Interrupts any input actions that are currently taking place (should stop drags, etc.)
     *
     * If excludePointer is provided, it will NOT be interrupted along with the others
     */
    interruptPointers(excludePointer = null) {
        _.each(this.pointers, pointer => {
            if (pointer !== excludePointer) {
                pointer.interruptAll();
            }
        });
    }
    /**
     * Called to batch a raw DOM event (which may be immediately fired, depending on the settings). (scenery-internal)
     *
     * @param context
     * @param batchType - See BatchedDOMEvent's "enumeration"
     * @param callback - Parameter types defined by the batchType. See BatchedDOMEvent for details
     * @param triggerImmediate - Certain events can force immediate action, since browsers like Chrome
     *                                     only allow certain operations in the callback for a user gesture (e.g. like
     *                                     a mouseup to open a window).
     */
    batchEvent(context, batchType, callback, triggerImmediate) {
        ;
        ;
        // If our display is not interactive, do not respond to any events (but still prevent default)
        if (this.display.interactive) {
            this.batchedEvents.push(BatchedDOMEvent.pool.create(context, batchType, callback));
            if (triggerImmediate || !this.batchDOMEvents) {
                this.fireBatchedEvents();
            }
            // NOTE: If we ever want to Display.updateDisplay() on events, do so here
        }
        // Firefox has issues with preventDefault on pointer down events +
        // multitouch. https://github.com/scenerystack/scenerystack/issues/42 tracks
        // the main issue, we skip preventDefault here to prevent
        // https://github.com/phetsims/scenery/issues/837 from happening.
        // https://bugzilla.mozilla.org/show_bug.cgi?id=1729465 notes preventDefault()
        // as the culprit.
        const isFirefoxPointerDownTouch = platform.firefox &&
            context.domEvent instanceof PointerEvent &&
            callback === this.pointerDown &&
            context.domEvent.pointerType === 'touch';
        // Always preventDefault on touch events, since we don't want mouse events triggered afterwards. See
        // http://www.html5rocks.com/en/mobile/touchandmouse/ for more information.
        // Additionally, IE had some issues with skipping prevent default, see
        // https://github.com/phetsims/scenery/issues/464 for mouse handling.
        // WE WILL NOT preventDefault() on keyboard or alternative input events here
        if (!(this.passiveEvents === true) &&
            (callback !== this.mouseDown || platform.edge) &&
            !isFirefoxPointerDownTouch &&
            batchType !== BatchedDOMEventType.ALT_TYPE &&
            !context.allowsDOMInput()) {
            // We cannot prevent a passive event, so don't try
            context.domEvent.preventDefault();
        }
        ;
    }
    /**
     * Fires all of our events that were batched into the batchedEvents array. (scenery-internal)
     */
    fireBatchedEvents() {
        ;
        // Don't re-entrantly enter our loop, see https://github.com/phetsims/balloons-and-static-electricity/issues/406
        if (!this.currentlyFiringEvents && this.batchedEvents.length) {
            ;
            ;
            this.currentlyFiringEvents = true;
            // needs to be done in order
            const batchedEvents = this.batchedEvents;
            // IMPORTANT: We need to check the length of the array at every iteration, as it can change due to re-entrant
            // event handling, see https://github.com/phetsims/balloons-and-static-electricity/issues/406.
            // Events may be appended to this (synchronously) as part of firing initial events, so we want to FULLY run all
            // events before clearing our array.
            for (let i = 0; i < batchedEvents.length; i++) {
                const batchedEvent = batchedEvents[i];
                batchedEvent.run(this);
                batchedEvent.dispose();
            }
            cleanArray(batchedEvents);
            this.currentlyFiringEvents = false;
            ;
        }
    }
    /**
     * Clears any batched events that we don't want to process. (scenery-internal)
     *
     * NOTE: It is HIGHLY recommended to interrupt pointers and remove non-Mouse pointers before doing this, as
     * otherwise it can cause incorrect state in certain types of listeners (e.g. ones that count how many pointers
     * are over them).
     */
    clearBatchedEvents() {
        this.batchedEvents.length = 0;
    }
    /**
     * Checks all pointers to see whether they are still "over" the same nodes (trail). If not, it will fire the usual
     * enter/exit events. (scenery-internal)
     */
    validatePointers() {
        this.validatePointersAction.execute();
    }
    /**
     * Removes all non-Mouse pointers from internal tracking. (scenery-internal)
     */
    removeTemporaryPointers() {
        for (let i = this.pointers.length - 1; i >= 0; i--) {
            const pointer = this.pointers[i];
            if (!(pointer instanceof Mouse)) {
                this.pointers.splice(i, 1);
                // Send exit events. As we can't get a DOM event, we'll send a fake object instead.
                const exitTrail = pointer.trail || new Trail(this.rootNode);
                this.exitEvents(pointer, EventContext.createSynthetic(), exitTrail, 0, true);
            }
        }
    }
    /**
     * Hooks up DOM listeners to whatever type of object we are going to listen to. (scenery-internal)
     */
    connectListeners() {
        BrowserEvents.addDisplay(this.display, this.attachToWindow, this.passiveEvents);
    }
    /**
     * Removes DOM listeners from whatever type of object we were listening to. (scenery-internal)
     */
    disconnectListeners() {
        BrowserEvents.removeDisplay(this.display, this.attachToWindow, this.passiveEvents);
    }
    /**
     * Extract a {Vector2} global coordinate point from an arbitrary DOM event. (scenery-internal)
     */
    pointFromEvent(domEvent) {
        const position = Vector2.pool.create(domEvent.clientX, domEvent.clientY);
        if (!this.assumeFullWindow) {
            const domBounds = this.display.domElement.getBoundingClientRect();
            // TODO: consider totally ignoring any with zero width/height, as we aren't attached to the display? https://github.com/phetsims/scenery/issues/1581
            // For now, don't offset.
            if (domBounds.width > 0 && domBounds.height > 0) {
                position.subtractXY(domBounds.left, domBounds.top);
                // Detect a scaling of the display here (the client bounding rect having different dimensions from our
                // display), and attempt to compensate.
                // NOTE: We can't handle rotation here.
                if (domBounds.width !== this.display.width || domBounds.height !== this.display.height) {
                    // TODO: Have code verify the correctness here, and that it's not triggering all the time https://github.com/phetsims/scenery/issues/1581
                    position.x *= this.display.width / domBounds.width;
                    position.y *= this.display.height / domBounds.height;
                }
            }
        }
        return position;
    }
    interruptMultitouchPointers(point, context) {
        const touchLikePointers = this.pointers.filter(otherPointer => otherPointer.isTouchLike());
        for (const otherPointer of touchLikePointers) {
            this.cancelEvent(otherPointer, context, point);
            this.removePointer(otherPointer);
        }
    }
    /**
     * Adds a pointer to our list.
     */
    addPointer(pointer, point, context) {
        // Disable multitouch, see https://github.com/phetsims/scenery/issues/1684
        // For now, this is done by preventing a new "pointer" from being added, so that
        // the events that would be forwarded to it are ignored.
        // NOTE: NOT doing ` || this.mouse?.hasPointerCaptured()`, since it seems
        // like we are not fully clearing the capture state when we should be.
        if (this.preventMultitouch &&
            pointer.isTouchLike() &&
            this.pointers.some(otherPointer => otherPointer.isTouchLike())) {
            return;
        }
        // Interrupt touchlike pointers on a new pointer
        if (this.interruptMultitouch) {
            this.interruptMultitouchPointers(point, context);
            // And if we have a mouse, interrupt listeners on it as well.
            if (this.mouse) {
                this.mouse.interruptAll();
            }
        }
        this.pointers.push(pointer);
        this.pointerAddedEmitter.emit(pointer);
    }
    /**
     * Removes a pointer from our list. If we get future events for it (based on the ID) it will be ignored.
     */
    removePointer(pointer) {
        // sanity check version, will remove all instances
        for (let i = this.pointers.length - 1; i >= 0; i--) {
            if (this.pointers[i] === pointer) {
                this.pointers.splice(i, 1);
            }
        }
        pointer.dispose();
    }
    /**
     * Given a pointer's ID (given by the pointer/touch specifications to be unique to a specific pointer/touch),
     * returns the given pointer (if we have one).
     *
     * NOTE: There are some cases where we may have prematurely "removed" a pointer.
     */
    findPointerById(id) {
        let i = this.pointers.length;
        while (i--) {
            const pointer = this.pointers[i];
            if (pointer.id === id) {
                return pointer;
            }
        }
        return null;
    }
    getPDOMEventTrail(domEvent, eventName) {
        if (!this.display.interactive) {
            return null;
        }
        const trail = this.getTrailFromPDOMEvent(domEvent);
        // Only dispatch the event if the click did not happen rapidly after an up event. It is
        // likely that the screen reader dispatched both pointer AND click events in this case, and
        // we only want to respond to one or the other. See https://github.com/phetsims/scenery/issues/1094.
        // This is outside of the clickAction execution so that blocked clicks are not part of the PhET-iO data
        // stream.
        const notBlockingSubsequentClicksOccurringTooQuickly = trail && !(eventName === 'click' &&
            _.some(trail.nodes, node => node.positionInPDOM) &&
            domEvent.timeStamp - this.upTimeStamp <= PDOM_CLICK_DELAY);
        return notBlockingSubsequentClicksOccurringTooQuickly ? trail : null;
    }
    ensureMouse(point, context) {
        const mouse = this.mouse;
        if (mouse) {
            return mouse;
        }
        else {
            // Initializes the Mouse object on the first mouse event (this may never happen on touch devices).
            const mouse = new Mouse(point);
            this.mouse = mouse;
            this.addPointer(mouse, point, context);
            return mouse;
        }
    }
    ensurePDOMPointer(context) {
        const pdomPointer = this.pdomPointer;
        if (pdomPointer) {
            return pdomPointer;
        }
        else {
            // Initializes the accessible pointer object on the first pdom event.
            const pdomPointer = new PDOMPointer(this.display);
            this.pdomPointer = pdomPointer;
            this.addPointer(pdomPointer, Vector2.ZERO, context);
            return pdomPointer;
        }
    }
    /**
     * Steps to dispatch a pdom-related event. Before dispatch, the PDOMPointer is initialized if it
     * hasn't been created yet and a userGestureEmitter emits to indicate that a user has begun an interaction.
     */
    dispatchPDOMEvent(trail, eventType, context, bubbles) {
        this.ensurePDOMPointer(context).updateTrail(trail);
        // exclude focus and blur events because they can happen with scripting without user input
        if (PDOMUtils.USER_GESTURE_EVENTS.includes(eventType)) {
            DisplayGlobals.userGestureEmitter.emit();
        }
        const domEvent = context.domEvent;
        // This workaround hopefully won't be here forever, see ParallelDOM.setExcludeLabelSiblingFromInput() and https://github.com/phetsims/a11y-research/issues/156
        if (!(domEvent.target && domEvent.target.hasAttribute(PDOMUtils.DATA_EXCLUDE_FROM_INPUT))) {
            // If the trail is not pickable, don't dispatch PDOM events to those targets - but we still
            // dispatch with an empty trail to call listeners on the Display and Pointer.
            const canFireListeners = trail.isPickable() || PDOM_UNPICKABLE_EVENTS.includes(eventType);
            if (!canFireListeners) {
                trail = new Trail([]);
            }
            ;
            this.dispatchEvent(trail, eventType, this.pdomPointer, context, bubbles);
        }
    }
    /**
     * From a DOM Event, get its relatedTarget and map that to the scenery Node. Will return null if relatedTarget
     * is not provided, or if relatedTarget is not under PDOM, or there is no associated Node with trail id on the
     * relatedTarget element. (scenery-internal)
     *
     * @param domEvent - DOM Event, not a SceneryEvent!
     */
    getRelatedTargetTrail(domEvent) {
        const relatedTargetElement = domEvent.relatedTarget;
        if (relatedTargetElement && this.display.isElementUnderPDOM(relatedTargetElement, false)) {
            const relatedTarget = domEvent.relatedTarget;
            ;
            const trailIndices = relatedTarget.getAttribute(PDOMUtils.DATA_PDOM_UNIQUE_ID);
            ;
            return pdomUniqueIdToTrail(this.display, trailIndices);
        }
        return null;
    }
    /**
     * Get the trail ID of the node represented by a DOM element who is the target of a DOM Event in the accessible PDOM.
     * This is a bit of a misnomer, because the domEvent doesn't have to be under the PDOM. Returns null if not in the PDOM.
     */
    getTrailFromPDOMEvent(domEvent) {
        ;
        if (!this.display._accessible) {
            return null;
        }
        // could be serialized event for phet-io playbacks, see Input.serializeDOMEvent()
        if (domEvent[TARGET_SUBSTITUTE_KEY]) {
            const trailIndices = domEvent[TARGET_SUBSTITUTE_KEY].getAttribute(PDOMUtils.DATA_PDOM_UNIQUE_ID);
            return pdomUniqueIdToTrail(this.display, trailIndices);
        }
        else {
            const target = domEvent.target;
            if (target && target instanceof self.Element && this.display.isElementUnderPDOM(target, false)) {
                const trailIndices = target.getAttribute(PDOMUtils.DATA_PDOM_UNIQUE_ID);
                ;
                return pdomUniqueIdToTrail(this.display, trailIndices);
            }
        }
        return null;
    }
    /**
     * Triggers a logical mousedown event. (scenery-internal)
     *
     * NOTE: This may also be called from the pointer event handler (pointerDown) or from things like fuzzing or
     * playback. The event may be "faked" for certain purposes.
     */
    mouseDown(id, point, context) {
        ;
        ;
        this.mouseDownAction.execute(id, point, context);
        ;
    }
    /**
     * Triggers a logical mouseup event. (scenery-internal)
     *
     * NOTE: This may also be called from the pointer event handler (pointerUp) or from things like fuzzing or
     * playback. The event may be "faked" for certain purposes.
     */
    mouseUp(point, context) {
        ;
        ;
        this.mouseUpAction.execute(point, context);
        ;
    }
    /**
     * Triggers a logical mousemove event. (scenery-internal)
     *
     * NOTE: This may also be called from the pointer event handler (pointerMove) or from things like fuzzing or
     * playback. The event may be "faked" for certain purposes.
     */
    mouseMove(point, context) {
        ;
        ;
        this.mouseMoveAction.execute(point, context);
        ;
    }
    /**
     * Triggers a logical mouseover event (this does NOT correspond to the Scenery event, since this is for the display) (scenery-internal)
     */
    mouseOver(point, context) {
        ;
        ;
        this.mouseOverAction.execute(point, context);
        ;
    }
    /**
     * Triggers a logical mouseout event (this does NOT correspond to the Scenery event, since this is for the display) (scenery-internal)
     */
    mouseOut(point, context) {
        ;
        ;
        this.mouseOutAction.execute(point, context);
        ;
    }
    /**
     * Triggers a logical mouse-wheel/scroll event. (scenery-internal)
     */
    wheel(context) {
        ;
        ;
        this.wheelScrollAction.execute(context);
        ;
    }
    /**
     * Triggers a logical touchstart event. This is called for each touch point in a 'raw' event. (scenery-internal)
     *
     * NOTE: This may also be called from the pointer event handler (pointerDown) or from things like fuzzing or
     * playback. The event may be "faked" for certain purposes.
     */
    touchStart(id, point, context) {
        ;
        ;
        this.touchStartAction.execute(id, point, context);
        ;
    }
    /**
     * Triggers a logical touchend event. This is called for each touch point in a 'raw' event. (scenery-internal)
     *
     * NOTE: This may also be called from the pointer event handler (pointerUp) or from things like fuzzing or
     * playback. The event may be "faked" for certain purposes.
     */
    touchEnd(id, point, context) {
        ;
        ;
        this.touchEndAction.execute(id, point, context);
        ;
    }
    /**
     * Triggers a logical touchmove event. This is called for each touch point in a 'raw' event. (scenery-internal)
     *
     * NOTE: This may also be called from the pointer event handler (pointerMove) or from things like fuzzing or
     * playback. The event may be "faked" for certain purposes.
     */
    touchMove(id, point, context) {
        ;
        ;
        this.touchMoveAction.execute(id, point, context);
        ;
    }
    /**
     * Triggers a logical touchcancel event. This is called for each touch point in a 'raw' event. (scenery-internal)
     *
     * NOTE: This may also be called from the pointer event handler (pointerCancel) or from things like fuzzing or
     * playback. The event may be "faked" for certain purposes.
     */
    touchCancel(id, point, context) {
        ;
        ;
        this.touchCancelAction.execute(id, point, context);
        ;
    }
    /**
     * Triggers a logical penstart event (e.g. a stylus). This is called for each pen point in a 'raw' event. (scenery-internal)
     *
     * NOTE: This may also be called from the pointer event handler (pointerDown) or from things like fuzzing or
     * playback. The event may be "faked" for certain purposes.
     */
    penStart(id, point, context) {
        ;
        ;
        this.penStartAction.execute(id, point, context);
        ;
    }
    /**
     * Triggers a logical penend event (e.g. a stylus). This is called for each pen point in a 'raw' event. (scenery-internal)
     *
     * NOTE: This may also be called from the pointer event handler (pointerUp) or from things like fuzzing or
     * playback. The event may be "faked" for certain purposes.
     */
    penEnd(id, point, context) {
        ;
        ;
        this.penEndAction.execute(id, point, context);
        ;
    }
    /**
     * Triggers a logical penmove event (e.g. a stylus). This is called for each pen point in a 'raw' event. (scenery-internal)
     *
     * NOTE: This may also be called from the pointer event handler (pointerMove) or from things like fuzzing or
     * playback. The event may be "faked" for certain purposes.
     */
    penMove(id, point, context) {
        ;
        ;
        this.penMoveAction.execute(id, point, context);
        ;
    }
    /**
     * Triggers a logical pencancel event (e.g. a stylus). This is called for each pen point in a 'raw' event. (scenery-internal)
     *
     * NOTE: This may also be called from the pointer event handler (pointerCancel) or from things like fuzzing or
     * playback. The event may be "faked" for certain purposes.
     */
    penCancel(id, point, context) {
        ;
        ;
        this.penCancelAction.execute(id, point, context);
        ;
    }
    /**
     * Handles a pointerdown event, forwarding it to the proper logical event. (scenery-internal)
     */
    pointerDown(id, type, point, context) {
        // In IE for pointer down events, we want to make sure than the next interactions off the page are sent to
        // this element (it will bubble). See https://github.com/phetsims/scenery/issues/464 and
        // http://news.qooxdoo.org/mouse-capturing.
        const target = this.attachToWindow ? document.body : this.display.domElement;
        // NOTE: pointerId can be zero. It usually is zero for the mouse for Firefox.
        if (target.setPointerCapture && !context.allowsDOMInput()) {
            // NOTE: This will error out if run on a playback destination, where a pointer with the given ID does not exist.
            target.setPointerCapture(context.domEvent.pointerId);
        }
        type = this.handleUnknownPointerType(type, id);
        switch (type) {
            case 'mouse':
                // The actual event afterwards
                this.mouseDown(id, point, context);
                break;
            case 'touch':
                this.touchStart(id, point, context);
                break;
            case 'pen':
                this.penStart(id, point, context);
                break;
            default:
                false;
        }
    }
    /**
     * Handles a pointerup event, forwarding it to the proper logical event. (scenery-internal)
     */
    pointerUp(id, type, point, context) {
        // update this outside of the Action executions so that PhET-iO event playback does not override it
        this.upTimeStamp = context.domEvent.timeStamp;
        type = this.handleUnknownPointerType(type, id);
        switch (type) {
            case 'mouse':
                this.mouseUp(point, context);
                break;
            case 'touch':
                this.touchEnd(id, point, context);
                break;
            case 'pen':
                this.penEnd(id, point, context);
                break;
            default:
                false;
        }
    }
    /**
     * Handles a pointercancel event, forwarding it to the proper logical event. (scenery-internal)
     */
    pointerCancel(id, type, point, context) {
        type = this.handleUnknownPointerType(type, id);
        switch (type) {
            case 'mouse':
                if (console && console.log) {
                    console.log('WARNING: Pointer mouse cancel was received');
                }
                break;
            case 'touch':
                this.touchCancel(id, point, context);
                break;
            case 'pen':
                this.penCancel(id, point, context);
                break;
            default:
                if (console.log) {
                    console.log(`Unknown pointer type: ${type}`);
                }
        }
    }
    /**
     * Handles a gotpointercapture event, forwarding it to the proper logical event. (scenery-internal)
     */
    gotPointerCapture(id, type, point, context) {
        ;
        ;
        this.gotPointerCaptureAction.execute(id, context);
        ;
    }
    /**
     * Handles a lostpointercapture event, forwarding it to the proper logical event. (scenery-internal)
     */
    lostPointerCapture(id, type, point, context) {
        ;
        ;
        this.lostPointerCaptureAction.execute(id, context);
        ;
    }
    /**
     * Handles a pointermove event, forwarding it to the proper logical event. (scenery-internal)
     */
    pointerMove(id, type, point, context) {
        type = this.handleUnknownPointerType(type, id);
        switch (type) {
            case 'mouse':
                this.mouseMove(point, context);
                break;
            case 'touch':
                this.touchMove(id, point, context);
                break;
            case 'pen':
                this.penMove(id, point, context);
                break;
            default:
                if (console.log) {
                    console.log(`Unknown pointer type: ${type}`);
                }
        }
    }
    /**
     * Handles a pointerover event, forwarding it to the proper logical event. (scenery-internal)
     */
    pointerOver(id, type, point, context) {
        // TODO: accumulate mouse/touch info in the object if needed? https://github.com/phetsims/scenery/issues/1581
        // TODO: do we want to branch change on these types of events? https://github.com/phetsims/scenery/issues/1581
    }
    /**
     * Handles a pointerout event, forwarding it to the proper logical event. (scenery-internal)
     */
    pointerOut(id, type, point, context) {
        // TODO: accumulate mouse/touch info in the object if needed? https://github.com/phetsims/scenery/issues/1581
        // TODO: do we want to branch change on these types of events? https://github.com/phetsims/scenery/issues/1581
    }
    /**
     * Handles a pointerenter event, forwarding it to the proper logical event. (scenery-internal)
     */
    pointerEnter(id, type, point, context) {
        // TODO: accumulate mouse/touch info in the object if needed? https://github.com/phetsims/scenery/issues/1581
        // TODO: do we want to branch change on these types of events? https://github.com/phetsims/scenery/issues/1581
    }
    /**
     * Handles a pointerleave event, forwarding it to the proper logical event. (scenery-internal)
     */
    pointerLeave(id, type, point, context) {
        // TODO: accumulate mouse/touch info in the object if needed? https://github.com/phetsims/scenery/issues/1581
        // TODO: do we want to branch change on these types of events? https://github.com/phetsims/scenery/issues/1581
    }
    /**
     * Handles a focusin event, forwarding it to the proper logical event. (scenery-internal)
     */
    focusIn(context) {
        ;
        ;
        this.focusinAction.execute(context);
        ;
    }
    /**
     * Handles a focusout event, forwarding it to the proper logical event. (scenery-internal)
     */
    focusOut(context) {
        ;
        ;
        this.focusoutAction.execute(context);
        ;
    }
    /**
     * Handles an input event, forwarding it to the proper logical event. (scenery-internal)
     */
    input(context) {
        ;
        ;
        this.inputAction.execute(context);
        ;
    }
    /**
     * Handles a change event, forwarding it to the proper logical event. (scenery-internal)
     */
    change(context) {
        ;
        ;
        this.changeAction.execute(context);
        ;
    }
    /**
     * Handles a click event, forwarding it to the proper logical event. (scenery-internal)
     */
    click(context) {
        ;
        ;
        this.clickAction.execute(context);
        ;
    }
    /**
     * Handles a keydown event, forwarding it to the proper logical event. (scenery-internal)
     */
    keyDown(context) {
        ;
        ;
        this.keydownAction.execute(context);
        ;
    }
    /**
     * Handles a keyup event, forwarding it to the proper logical event. (scenery-internal)
     */
    keyUp(context) {
        ;
        ;
        this.keyupAction.execute(context);
        ;
    }
    /**
     * When we get an unknown pointer event type (allowed in the spec, see
     * https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/pointerType), we'll try to guess the pointer type
     * so that we can properly start/end the interaction. NOTE: this can happen for an 'up' where we received a
     * proper type for a 'down', so thus we need the detection.
     */
    handleUnknownPointerType(type, id) {
        if (type !== '') {
            return type;
        }
        return (this.mouse && this.mouse.id === id) ? 'mouse' : 'touch';
    }
    /**
     * Given a pointer reference, hit test it and determine the Trail that the pointer is over.
     */
    getPointerTrail(pointer) {
        return this.rootNode.trailUnderPointer(pointer) || new Trail(this.rootNode);
    }
    /**
     * Called for each logical "up" event, for any pointer type.
     */
    upEvent(pointer, context, point) {
        // if the event target is within the PDOM the AT is sending a fake pointer event to the document - do not
        // dispatch this since the PDOM should only handle Input.PDOM_EVENT_TYPES, and all other pointer input should
        // go through the Display div. Otherwise, activation will be duplicated when we handle pointer and PDOM events
        if (this.display.isElementUnderPDOM(context.domEvent.target, true)) {
            return;
        }
        const pointChanged = pointer.up(point, context.domEvent);
        ;
        ;
        // We'll use this trail for the entire dispatch of this event.
        const eventTrail = this.branchChangeEvents(pointer, context, pointChanged);
        this.dispatchEvent(eventTrail, 'up', pointer, context, true);
        // touch pointers are transient, so fire exit/out to the trail afterwards
        if (pointer.isTouchLike()) {
            this.exitEvents(pointer, context, eventTrail, 0, true);
        }
        ;
    }
    /**
     * Called for each logical "down" event, for any pointer type.
     */
    downEvent(pointer, context, point) {
        // if the event target is within the PDOM the AT is sending a fake pointer event to the document - do not
        // dispatch this since the PDOM should only handle Input.PDOM_EVENT_TYPES, and all other pointer input should
        // go through the Display div. Otherwise, activation will be duplicated when we handle pointer and PDOM events
        if (this.display.isElementUnderPDOM(context.domEvent.target, true)) {
            return;
        }
        const pointChanged = pointer.updatePoint(point);
        ;
        ;
        // We'll use this trail for the entire dispatch of this event.
        const eventTrail = this.branchChangeEvents(pointer, context, pointChanged);
        pointer.down(context.domEvent);
        this.dispatchEvent(eventTrail, 'down', pointer, context, true);
        ;
    }
    /**
     * Called for each logical "move" event, for any pointer type.
     */
    moveEvent(pointer, context) {
        ;
        ;
        // Always treat move events as "point changed"
        this.branchChangeEvents(pointer, context, true);
        ;
    }
    /**
     * Called for each logical "cancel" event, for any pointer type.
     */
    cancelEvent(pointer, context, point) {
        const pointChanged = pointer.cancel(point);
        ;
        ;
        // We'll use this trail for the entire dispatch of this event.
        const eventTrail = this.branchChangeEvents(pointer, context, pointChanged);
        this.dispatchEvent(eventTrail, 'cancel', pointer, context, true);
        // touch pointers are transient, so fire exit/out to the trail afterwards
        if (pointer.isTouchLike()) {
            this.exitEvents(pointer, context, eventTrail, 0, true);
        }
        ;
    }
    /**
     * Dispatches any necessary events that would result from the pointer's trail changing.
     *
     * This will send the necessary exit/enter events (on subtrails that have diverged between before/after), the
     * out/over events, and if flagged a move event.
     *
     * @param pointer
     * @param context
     * @param sendMove - Whether to send move events
     * @returns - The current trail of the pointer
     */
    branchChangeEvents(pointer, context, sendMove) {
        ;
        ;
        const trail = this.getPointerTrail(pointer);
        const inputEnabledTrail = trail.slice(0, Math.min(trail.nodes.length, trail.getLastInputEnabledIndex() + 1));
        const oldInputEnabledTrail = pointer.inputEnabledTrail || new Trail(this.rootNode);
        const branchInputEnabledIndex = Trail.branchIndex(inputEnabledTrail, oldInputEnabledTrail);
        const lastInputEnabledNodeChanged = oldInputEnabledTrail.lastNode() !== inputEnabledTrail.lastNode();
        false;
        // event order matches http://www.w3.org/TR/DOM-Level-3-Events/#events-mouseevent-event-order
        if (sendMove) {
            this.dispatchEvent(trail, 'move', pointer, context, true);
        }
        // We want to approximately mimic http://www.w3.org/TR/DOM-Level-3-Events/#events-mouseevent-event-order
        this.exitEvents(pointer, context, oldInputEnabledTrail, branchInputEnabledIndex, lastInputEnabledNodeChanged);
        this.enterEvents(pointer, context, inputEnabledTrail, branchInputEnabledIndex, lastInputEnabledNodeChanged);
        pointer.trail = trail;
        pointer.inputEnabledTrail = inputEnabledTrail;
        ;
        return trail;
    }
    /**
     * Triggers 'enter' events along a trail change, and an 'over' event on the leaf.
     *
     * For example, if we change from a trail [ a, b, c, d, e ] => [ a, b, x, y ], it will fire:
     *
     * - enter x
     * - enter y
     * - over y (bubbles)
     *
     * @param pointer
     * @param event
     * @param trail - The "new" trail
     * @param branchIndex - The first index where the old and new trails have a different node. We will notify
     *                               for this node and all "descendant" nodes in the relevant trail.
     * @param lastNodeChanged - If the last node didn't change, we won't sent an over event.
     */
    enterEvents(pointer, context, trail, branchIndex, lastNodeChanged) {
        if (lastNodeChanged) {
            this.dispatchEvent(trail, 'over', pointer, context, true, true);
        }
        for (let i = branchIndex; i < trail.length; i++) {
            this.dispatchEvent(trail.slice(0, i + 1), 'enter', pointer, context, false);
        }
    }
    /**
     * Triggers 'exit' events along a trail change, and an 'out' event on the leaf.
     *
     * For example, if we change from a trail [ a, b, c, d, e ] => [ a, b, x, y ], it will fire:
     *
     * - out e (bubbles)
     * - exit c
     * - exit d
     * - exit e
     *
     * @param pointer
     * @param event
     * @param trail - The "old" trail
     * @param branchIndex - The first index where the old and new trails have a different node. We will notify
     *                               for this node and all "descendant" nodes in the relevant trail.
     * @param lastNodeChanged - If the last node didn't change, we won't sent an out event.
     */
    exitEvents(pointer, context, trail, branchIndex, lastNodeChanged) {
        for (let i = trail.length - 1; i >= branchIndex; i--) {
            this.dispatchEvent(trail.slice(0, i + 1), 'exit', pointer, context, false, true);
        }
        if (lastNodeChanged) {
            this.dispatchEvent(trail, 'out', pointer, context, true);
        }
    }
    /**
     * Dispatch to all nodes in the Trail, optionally bubbling down from the leaf to the root.
     *
     * @param trail
     * @param type
     * @param pointer
     * @param context
     * @param bubbles - If bubbles is false, the event is only dispatched to the leaf node of the trail.
     * @param fireOnInputDisabled - Whether to fire this event even if nodes have inputEnabled:false
     */
    dispatchEvent(trail, type, pointer, context, bubbles, fireOnInputDisabled = false) {
        ;
        ;
        ;
        ;
        // NOTE: event is not immutable, as its currentTarget changes
        const inputEvent = new SceneryEvent(trail, type, pointer, context);
        this.currentSceneryEvent = inputEvent;
        // first run through the pointer's listeners to see if one of them will handle the event
        this.dispatchToListeners(pointer, pointer.getListeners(), type, inputEvent);
        // if not yet handled, run through the trail in order to see if one of them will handle the event
        // at the base of the trail should be the scene node, so the scene will be notified last
        this.dispatchToTargets(trail, type, pointer, inputEvent, bubbles, fireOnInputDisabled);
        // Notify input listeners on the Display
        this.dispatchToListeners(pointer, this.display.getInputListeners(), type, inputEvent);
        // Notify input listeners to any Display
        if (DisplayGlobals.inputListeners.length) {
            this.dispatchToListeners(pointer, DisplayGlobals.inputListeners.slice(), type, inputEvent);
        }
        this.currentSceneryEvent = null;
        ;
    }
    /**
     * Notifies an array of listeners with a specific event.
     *
     * @param pointer
     * @param listeners - Should be a defensive array copy already.
     * @param type
     * @param inputEvent
     */
    dispatchToListeners(pointer, listeners, type, inputEvent) {
        if (inputEvent.handled) {
            return;
        }
        const specificType = pointer.type + type; // e.g. mouseup, touchup
        for (let i = 0; i < listeners.length; i++) {
            const listener = listeners[i];
            if (!inputEvent.aborted && listener[specificType]) {
                ;
                ;
                listener[specificType](inputEvent);
                ;
            }
            if (!inputEvent.aborted && listener[type]) {
                ;
                ;
                listener[type](inputEvent);
                ;
            }
        }
    }
    /**
     * Dispatch to all nodes in the Trail, optionally bubbling down from the leaf to the root.
     *
     * @param trail
     * @param type
     * @param pointer
     * @param inputEvent
     * @param bubbles - If bubbles is false, the event is only dispatched to the leaf node of the trail.
     * @param [fireOnInputDisabled]
     */
    dispatchToTargets(trail, type, pointer, inputEvent, bubbles, fireOnInputDisabled = false) {
        if (inputEvent.aborted || inputEvent.handled) {
            return;
        }
        const inputEnabledIndex = trail.getLastInputEnabledIndex();
        for (let i = trail.nodes.length - 1; i >= 0; bubbles ? i-- : i = -1) {
            const target = trail.nodes[i];
            const trailInputDisabled = inputEnabledIndex < i;
            if (target.isDisposed || (!fireOnInputDisabled && trailInputDisabled)) {
                continue;
            }
            inputEvent.currentTarget = target;
            this.dispatchToListeners(pointer, target.getInputListeners(), type, inputEvent);
            // if the input event was aborted or handled, don't follow the trail down another level
            if (inputEvent.aborted || inputEvent.handled) {
                return;
            }
        }
    }
    /**
     * Convenience function for logging out a point/event combination.
     *
     * @param point - Not logged if null
     * @param domEvent
     */
    static debugText(point, domEvent) {
        let result = `${domEvent.timeStamp} ${domEvent.type}`;
        if (point !== null) {
            result = `${point.x},${point.y} ${result}`;
        }
        return result;
    }
}
Input.InputIO = new IOType('InputIO', {
    valueType: Input,
    applyState: _.noop,
    toStateObject: (input) => {
        return {
            pointers: ArrayIOPointerIO.toStateObject(input.pointers)
        };
    },
    stateSchema: {
        pointers: ArrayIOPointerIO
    }
});
export default Input;
//# sourceMappingURL=Input.js.map