import _ from 'lodash';
import '../../../assert/js/assert.js';
import '../../../globals.js';
// Copyright 2024, University of Colorado Boulder
/**
 * Represents a single hotkey (keyboard shortcut) that can be either:
 *
 * 1. Added to globalHotkeyRegistry (to be available regardless of keyboard focus)
 * 2. Added to a node's inputListeners (to be available only when that node is part of the focused trail)
 *
 * For example:
 *
 *    globalHotkeyRegistry.add( new Hotkey( {
 *      keyStringProperty: new Property( 'y' ),
 *      fire: () => console.log( 'fire: y' )
 *    } ) );
 *
 *    myNode.addInputListener( {
 *      hotkeys: [
 *        new Hotkey( {
 *          keyStringProperty: new Property( 'x' ),
 *          fire: () => console.log( 'fire: x' )
 *        } )
 *      ]
 *    } );
 *
 * Also supports modifier keys that must be pressed in addition to the Key. See options for a description of how
 * they behave.
 *
 * Hotkeys are managed by hotkeyManager, which determines which hotkeys are active based on the globalHotkeyRegistry
 * and what Node has focus. See that class for information about how hotkeys work.
 *
 * @author Jesse Greenberg (PhET Interactive Simulations)
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */
import BooleanProperty from '../../../axon/js/BooleanProperty.js';
import CallbackTimer from '../../../axon/js/CallbackTimer.js';
import DerivedProperty from '../../../axon/js/DerivedProperty.js';
import EnabledComponent from '../../../axon/js/EnabledComponent.js';
import optionize from '../../../phet-core/js/optionize.js';
import { EnglishStringToCodeMap, hotkeyManager, KeyDescriptor, scenery } from '../imports.js';
export default class Hotkey extends EnabledComponent {
    constructor(providedOptions) {
        assert && assert(providedOptions.fireOnHoldTiming === 'custom' || (providedOptions.fireOnHoldCustomDelay === undefined && providedOptions.fireOnHoldCustomInterval === undefined), 'Cannot specify fireOnHoldCustomDelay / fireOnHoldCustomInterval if fireOnHoldTiming is not custom');
        const options = optionize()({
            fire: _.noop,
            press: _.noop,
            release: _.noop,
            fireOnDown: true,
            fireOnHold: false,
            fireOnHoldTiming: 'browser',
            fireOnHoldCustomDelay: 400,
            fireOnHoldCustomInterval: 100,
            allowOverlap: false,
            override: false
        }, providedOptions);
        super(options);
        // A Property that tracks whether the hotkey is currently pressed.
        // Will be true if it meets the following conditions:
        //
        // 1. Main `key` pressed
        // 2. All modifier keys in the hotkey's `modifierKeys` are pressed
        // 3. All modifier keys not in the hotkey's `modifierKeys` (but in the other enabled hotkeys) are not pressed
        this.isPressedProperty = new BooleanProperty(false);
        // (read-only for client code)
        // Whether the last release (value during isPressedProperty => false) was due to an interruption (e.g. focus changed).
        // If false, the hotkey was released due to the key being released.
        this.interrupted = false;
        // Store public things
        this.keyStringProperty = options.keyStringProperty;
        this.fire = options.fire;
        this.press = options.press;
        this.release = options.release;
        this.fireOnDown = options.fireOnDown;
        this.fireOnHold = options.fireOnHold;
        this.fireOnHoldTiming = options.fireOnHoldTiming;
        this.allowOverlap = options.allowOverlap;
        this.override = options.override;
        this.keyDescriptorProperty = new DerivedProperty([this.keyStringProperty], (keyString) => {
            return KeyDescriptor.keyStrokeToKeyDescriptor(keyString);
        });
        this.keysProperty = new DerivedProperty([this.keyDescriptorProperty], (keyDescriptor) => {
            const keys = _.uniq([keyDescriptor.key, ...keyDescriptor.modifierKeys]);
            // Make sure that every key has an entry in the EnglishStringToCodeMap
            for (const key of keys) {
                assert && assert(EnglishStringToCodeMap[key], `No codes for this key exists, do you need to add it to EnglishStringToCodeMap?: ${key}`);
            }
            return keys;
        });
        // Create a timer to handle the optional fire-on-hold feature.
        if (this.fireOnHold && this.fireOnHoldTiming === 'custom') {
            this.fireOnHoldTimer = new CallbackTimer({
                callback: this.fire.bind(this, null), // Pass null for fire-on-hold events
                delay: options.fireOnHoldCustomDelay,
                interval: options.fireOnHoldCustomInterval
            });
            this.disposeEmitter.addListener(() => this.fireOnHoldTimer.dispose());
            this.isPressedProperty.link(isPressed => {
                // We need to reset the timer, so we stop it (even if we are starting it in just a bit again)
                this.fireOnHoldTimer.stop(false);
                if (isPressed) {
                    this.fireOnHoldTimer.start();
                }
            });
        }
    }
    /**
     * On "press" of a Hotkey. All keys are pressed while the Hotkey is active. May also fire depending on
     * events. See hotkeyManager.
     *
     * (scenery-internal)
     */
    onPress(event, shouldFire) {
        // clear the flag on every press (set before notifying the isPressedProperty)
        this.interrupted = false;
        this.isPressedProperty.value = true;
        // press after setting up state
        this.press(event);
        if (shouldFire) {
            this.fire(event);
        }
    }
    /**
     * On "release" of a Hotkey. All keys are released while the Hotkey is inactive. May also fire depending on
     * events. See hotkeyManager.
     */
    onRelease(event, interrupted, shouldFire) {
        this.interrupted = interrupted;
        this.isPressedProperty.value = false;
        this.release(event);
        if (shouldFire) {
            this.fire(event);
        }
    }
    /**
     * Manually interrupt this hotkey, releasing it and setting a flag so that it will not fire until the next time
     * keys are pressed.
     */
    interrupt() {
        if (this.isPressedProperty.value) {
            hotkeyManager.interruptHotkey(this);
        }
    }
    dispose() {
        this.isPressedProperty.dispose();
        this.keysProperty.dispose();
        this.keyDescriptorProperty.dispose();
        super.dispose();
    }
}
scenery.register('Hotkey', Hotkey);
//# sourceMappingURL=Hotkey.js.map