import _ from 'lodash';
import '../../../assert/js/assert.js';
import '../../../tandem/js/PhetioIDUtils.js';
import '../../../globals.js';
// Copyright 2022-2024, University of Colorado Boulder
/**
 * A listener for general keyboard input. Specify the keys with a `keys` option in a readable format that looks like
 * this: [ 'shift+t', 'alt+shift+r' ]
 *
 * - Each entry in the array represents a combination of keys that must be pressed to fire a callback.
 * - '+' separates each key in a single combination.
 * - The keys leading up to the last key in the combination are considered "modifier" keys. The last key in the
 *   combination needs to be pressed while the modifier keys are down to fire the callback.
 * - The order modifier keys are pressed does not matter for firing the callback.
 *
 * In the above example "shift+t" OR "alt+shift+r" will fire the callback when pressed.
 *
 * An example usage would like this:
 *
 * this.addInputListener( new KeyboardListener( {
 *   keys: [ 'a+b', 'a+c', 'shift+arrowLeft', 'alt+g+t', 'ctrl+3', 'alt+ctrl+t' ],
 *   fire: ( event, keysPressed, listener ) => {
 *     if ( keysPressed === 'a+b' ) {
 *       console.log( 'you just pressed a+b!' );
 *     }
 *     else if ( keysPressed === 'a+c' ) {
 *       console.log( 'you just pressed a+c!' );
 *     }
 *     else if ( keysPressed === 'alt+g+t' ) {
 *       console.log( 'you just pressed alt+g+t' );
 *     }
 *     else if ( keysPressed === 'ctrl+3' ) {
 *       console.log( 'you just pressed ctrl+3' );
 *     }
 *     else if ( keysPressed === 'shift+arrowLeft' ) {
 *       console.log( 'you just pressed shift+arrowLeft' );
 *     }
 *   }
 * } ) );
 *
 * By default, the fire callback will fire when the last key is pressed down. See additional options for firing on key
 * release or other press and hold behavior.
 *
 * **Important Modifier Key Behavior**
 * Modifier keys prevent other key combinations from firing their behavior if they are pressed down.
 * For example, if you have a listener with 'shift+t' and 'y', pressing 'shift' will prevent 'y' from firing.
 * This behavior matches the behavior of the browser and is intended to prevent unexpected behavior. However,
 * this behavior is also true for custom (non-standard) modifier keys. For example, if you have a listener with
 * 'j+t' and 'y', pressing 'j' will prevent 'y' from firing. This is a PhET specific design decision, but it
 * is consistent with typical modifier key behavior.
 *
 * **Ignored Modifier Keys**
 * You can specify modifier keys that should be ignored while the hotkey is active. This allows you to override
 * default modifier key behavior. For example, if you have a listener for the 'y' key and you want it to
 * trigger even when the shift key is pressed, you would add 'shift' to the ignored modifier keys list.
 *
 * Ignored modifier keys are indicated in the key string using the '?' character. You can also choose to ignore
 * all other modifier keys by placing the '?' before the modifier key. Here are some examples:
 *
 * 'shift?+y' - fires when 'y' is pressed, even if 'shift' is down.
 * '?shift+y' - fires when 'y' and shift are pressed, but also allows 'ctrl', 'alt', or 'meta' to be down.
 *
 * **Global Keyboard Listeners**
 * A KeyboardListener can be added to a Node with addInputListener, and it will fire with normal scenery input dispatch
 * behavior when the Node has focus. However, a KeyboardListener can also be added "globally", meaning it will fire
 * regardless of where focus is in the document. Use KeyboardListener.createGlobal. This adds Hotkeys to the
 * globalHotkeyRegistry. Be sure to dispose of the KeyboardListener when it is no longer needed.
 *
 * **Potential Pitfall!**
 * The fire callback is only called if exactly the keys in a group are pressed. If you need to listen to a modifier key,
 * you must include it in the keys array. For example if you add a listener for 'tab', you must ALSO include
 * 'shift+tab' in the array to observe 'shift+tab' presses. If you provide 'tab' alone, the callback will not fire
 * if 'shift' is also pressed.
 *
 * Beware of "key ghosting". Some key combinations are not possible on certain keyboards. For example, some keyboards
 * cannot press all arrow keys at the same time. Be sure to test your key combinations on a variety of keyboards.
 * See https://github.com/phetsims/scenery/issues/1655.
 *
 * @author Jesse Greenberg (PhET Interactive Simulations)
 */
import DerivedProperty from '../../../axon/js/DerivedProperty.js';
import optionize from '../../../phet-core/js/optionize.js';
import { DisplayedTrailsProperty, EventContext, globalHotkeyRegistry, Hotkey, scenery, SceneryEvent, Trail } from '../imports.js';
import EnabledComponent from '../../../axon/js/EnabledComponent.js';
import Property from '../../../axon/js/Property.js';
import assertMutuallyExclusiveOptions from '../../../phet-core/js/assertMutuallyExclusiveOptions.js';
class KeyboardListener extends EnabledComponent {
    constructor(providedOptions) {
        // You can either provide keys directly OR provide a list of KeyDescriptor Properties. You cannot provide both.
        assertMutuallyExclusiveOptions(providedOptions, ['keys'], ['keyStringProperties']);
        const options = optionize()({
            keys: null,
            keyStringProperties: null,
            fire: _.noop,
            press: _.noop,
            release: _.noop,
            focus: _.noop,
            blur: _.noop,
            fireOnDown: true,
            fireOnHold: false,
            fireOnHoldTiming: 'browser',
            fireOnHoldCustomDelay: 400,
            fireOnHoldCustomInterval: 100,
            allowOverlap: false,
            override: true,
            // EnabledComponent
            // By default, do not instrument the enabledProperty; opt in with this option. See EnabledComponent
            phetioEnabledPropertyInstrumented: false
        }, providedOptions);
        super(options);
        this._fire = options.fire;
        this._press = options.press;
        this._release = options.release;
        this._focus = options.focus;
        this._blur = options.blur;
        this.fireOnDown = options.fireOnDown;
        this.fireOnHold = options.fireOnHold;
        this.fireOnHoldTiming = options.fireOnHoldTiming;
        this.fireOnHoldCustomDelay = options.fireOnHoldCustomDelay;
        this.fireOnHoldCustomInterval = options.fireOnHoldCustomInterval;
        this.allowOverlap = options.allowOverlap;
        this.override = options.override;
        // convert the provided keys to data that we can respond to with scenery's Input system
        this.hotkeys = this.createHotkeys(options.keys, options.keyStringProperties);
        this.isPressedProperty = DerivedProperty.or(this.hotkeys.map(hotkey => hotkey.isPressedProperty));
        this.pressedKeyStringPropertiesProperty = new Property([]);
        this.interrupted = false;
        this.enabledPropertyListener = this.onEnabledPropertyChange.bind(this);
        this.enabledProperty.lazyLink(this.enabledPropertyListener);
    }
    /**
     * Whether this listener is currently activated with a press.
     */
    get isPressed() {
        return this.isPressedProperty.value;
    }
    /**
     * Fired when the enabledProperty changes
     */
    onEnabledPropertyChange(enabled) {
        !enabled && this.interrupt();
    }
    /**
     * Dispose of this listener by disposing of any Callback timers. Then clear all KeyGroups.
     */
    dispose() {
        this.hotkeys.forEach(hotkey => hotkey.dispose());
        this.isPressedProperty.dispose();
        this.pressedKeyStringPropertiesProperty.dispose();
        this.enabledProperty.unlink(this.enabledPropertyListener);
        super.dispose();
    }
    /**
     * Everything that uses a KeyboardListener should prevent more global scenery keyboard behavior, such as pan/zoom
     * from arrow keys.
     */
    keydown(event) {
        event.pointer.reserveForKeyboardDrag();
    }
    /**
     * Public because this is called with the scenery listener API. Do not call this directly.
     */
    focusout(event) {
        this.interrupt();
        // Optional work to do on blur.
        this._blur(this);
    }
    /**
     * Public because this is called through the scenery listener API. Do not call this directly.
     */
    focusin(event) {
        // Optional work to do on focus.
        this._focus(this);
    }
    /**
     * Part of the scenery listener API. On cancel, clear active KeyGroups and stop their behavior.
     */
    cancel() {
        this.handleInterrupt();
    }
    /**
     * Part of the scenery listener API. Clear active KeyGroups and stop their callbacks.
     */
    interrupt() {
        this.handleInterrupt();
    }
    /**
     * Work to be done on both cancel and interrupt.
     */
    handleInterrupt() {
        // interrupt all hotkeys (will do nothing if hotkeys are interrupted or not active)
        this.hotkeys.forEach(hotkey => hotkey.interrupt());
    }
    createSyntheticEvent(pointer) {
        const context = EventContext.createSynthetic();
        return new SceneryEvent(new Trail(), 'synthetic', pointer, context);
    }
    /**
     * Converts the provided keys for this listener into a collection of Hotkeys to easily track what keys are down.
     */
    createHotkeys(keys, keyStringProperties) {
        assert && assert(keys || keyStringProperties, 'Must provide keys or keyDescriptorProperties');
        let usableKeyStringProperties;
        if (keyStringProperties) {
            usableKeyStringProperties = keyStringProperties;
        }
        else {
            // Convert provided keys into KeyDescriptors for the Hotkey.
            usableKeyStringProperties = keys.map(naturalKeys => {
                return new Property(naturalKeys);
            });
        }
        return usableKeyStringProperties.map(keyStringProperty => {
            const hotkey = new Hotkey({
                keyStringProperty: keyStringProperty,
                fire: (event) => {
                    this._fire(event, keyStringProperty.value, this);
                },
                press: (event) => {
                    this.interrupted = false;
                    const naturalKeys = keyStringProperty.value;
                    this._press(event, naturalKeys, this);
                    assert && assert(!this.pressedKeyStringPropertiesProperty.value.includes(keyStringProperty), 'Key already pressed');
                    this.pressedKeyStringPropertiesProperty.value = [...this.pressedKeyStringPropertiesProperty.value, keyStringProperty];
                },
                release: (event) => {
                    this.interrupted = hotkey.interrupted;
                    const naturalKeys = keyStringProperty.value;
                    this._release(event, naturalKeys, this);
                    assert && assert(this.pressedKeyStringPropertiesProperty.value.includes(keyStringProperty), 'Key not pressed');
                    this.pressedKeyStringPropertiesProperty.value = this.pressedKeyStringPropertiesProperty.value.filter(descriptor => descriptor !== keyStringProperty);
                },
                fireOnDown: this.fireOnDown,
                fireOnHold: this.fireOnHold,
                fireOnHoldTiming: this.fireOnHoldTiming,
                fireOnHoldCustomDelay: this.fireOnHoldTiming === 'custom' ? this.fireOnHoldCustomDelay : undefined,
                fireOnHoldCustomInterval: this.fireOnHoldTiming === 'custom' ? this.fireOnHoldCustomInterval : undefined,
                allowOverlap: this.allowOverlap,
                enabledProperty: this.enabledProperty,
                override: this.override
            });
            return hotkey;
        });
    }
    /**
     * Adds a global KeyboardListener to a target Node. This listener will fire regardless of where focus is in
     * the document. The listener is returned so that it can be disposed.
     */
    static createGlobal(target, providedOptions) {
        return new GlobalKeyboardListener(target, providedOptions);
    }
}
/**
 * Inner class for a KeyboardListener that is global with a target Node. The listener will fire no matter where
 * focus is in the document, as long as the target Node can receive input events. Create this listener with
 * KeyboardListener.createGlobal.
 */
class GlobalKeyboardListener extends KeyboardListener {
    constructor(target, providedOptions) {
        const displayedTrailsProperty = new DisplayedTrailsProperty(target, {
            // For alt input events, use the PDOM trail to determine if the trail is displayed. This may be different
            // from the "visual" trail if the Node is placed in a PDOM order that is different from the visual order.
            followPDOMOrder: true,
            // Additionally, the target must have each of these true up its Trails to receive alt input events.
            requirePDOMVisible: true,
            requireEnabled: true,
            requireInputEnabled: true
        });
        const globallyEnabledProperty = new DerivedProperty([displayedTrailsProperty], (trails) => {
            return trails.length > 0;
        });
        const options = optionize()({
            // The enabledProperty is forwarded to the Hotkeys so that they are disabled when the target cannot receive input.
            enabledProperty: globallyEnabledProperty
        }, providedOptions);
        super(options);
        this.displayedTrailsProperty = displayedTrailsProperty;
        this.globallyEnabledProperty = globallyEnabledProperty;
        // Add all global keys to the registry
        this.hotkeys.forEach(hotkey => {
            globalHotkeyRegistry.add(hotkey);
        });
    }
    /**
     * Dispose Properties and remove all Hotkeys from the global registry.
     */
    dispose() {
        // Remove all global keys from the registry.
        this.hotkeys.forEach(hotkey => {
            globalHotkeyRegistry.remove(hotkey);
        });
        this.globallyEnabledProperty.dispose();
        this.displayedTrailsProperty.dispose();
        super.dispose();
    }
}
scenery.register('KeyboardListener', KeyboardListener);
export default KeyboardListener;
//# sourceMappingURL=KeyboardListener.js.map