# Orca
#
# Copyright 2005-2008 Sun Microsystems Inc.
# Copyright 2011-2016 Igalia, S.L.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA  02110-1301 USA.

# pylint: disable=too-many-public-methods
# pylint: disable=too-many-instance-attributes

"""Provides support for handling input events."""

from __future__ import annotations

import math
import sys
import time
import unicodedata
from typing import TYPE_CHECKING

import gi

import orca

gi.require_version("Atspi", "2.0")
gi.require_version("Gdk", "3.0")
from gi.repository import (
    Atspi,
    Gdk,  # pylint: disable=no-name-in-module
    GLib,
)

from . import (
    command_manager,
    debug,
    focus_manager,
    keybindings,
    keynames,
    messages,
    orca_modifier_manager,
    presentation_manager,
    script_manager,
)
from .ax_utilities import AXUtilities

if TYPE_CHECKING:
    from collections.abc import Callable

    from .command import BrailleCommand
    from .scripts import default

KEYBOARD_EVENT = "keyboard"
BRAILLE_EVENT = "braille"
MOUSE_BUTTON_EVENT = "mouse:button"
REMOTE_CONTROLLER_EVENT = "remote controller"
REMOTE_CONTROLLER_TESTING_EVENT = "remote controller testing"


class InputEvent:
    """Provides support for handling input events."""

    def __init__(self, event_type: str) -> None:
        """Creates a new KEYBOARD_EVENT, BRAILLE_EVENT, or MOUSE_BUTTON_EVENT."""

        self.type: str = event_type
        self.time: float = time.time()
        self._click_count: int = 0

    def get_click_count(self) -> int:
        """Return the count of the number of clicks a user has made."""

        return self._click_count

    def set_click_count(self, count: int) -> None:
        """Updates the count of the number of clicks a user has made."""

        self._click_count = count

    def as_single_line_string(self) -> str:
        """Returns a single-line string representation of this event."""

        return f"{self.type}"


class KeyboardEvent(InputEvent):
    """Provides support for handling keyboard events."""

    _NAVIGATION_KEYS: frozenset[int] = frozenset()
    _ACTION_KEYS: frozenset[int] = frozenset()
    _DIACRITICAL_KEYS: frozenset[int] = frozenset()
    _FUNCTION_KEYS: frozenset[int] = frozenset()
    _LOCKING_KEYS: frozenset[int] = frozenset()
    _MODIFIER_KEYS: frozenset[int] = frozenset()
    _NUMERIC_KEYS: frozenset[int] = frozenset()
    _PUNCTUATION_KEYS: frozenset[int] = frozenset()
    _KEYPAD_KEYCODES: frozenset[int] = frozenset()
    _key_sets_initialized: bool = False

    @classmethod
    def _initialize_key_sets(cls) -> None:
        """Initializes key sets on first use."""

        if cls._key_sets_initialized:
            return

        cls._NAVIGATION_KEYS = frozenset(
            {
                Gdk.KEY_Down,
                Gdk.KEY_End,
                Gdk.KEY_Home,
                Gdk.KEY_Left,
                Gdk.KEY_Right,
                Gdk.KEY_Up,
            }
        )

        cls._ACTION_KEYS = frozenset(
            {
                Gdk.KEY_BackSpace,
                Gdk.KEY_Delete,
                Gdk.KEY_Escape,
                Gdk.KEY_KP_Enter,
                Gdk.KEY_Page_Down,
                Gdk.KEY_Page_Up,
                Gdk.KEY_Return,
                Gdk.KEY_Tab,
            }
        )

        cls._DIACRITICAL_KEYS = frozenset(
            {
                Gdk.KEY_dead_A,
                Gdk.KEY_dead_a,
                Gdk.KEY_dead_abovecomma,
                Gdk.KEY_dead_abovedot,
                Gdk.KEY_dead_abovereversedcomma,
                Gdk.KEY_dead_abovering,
                Gdk.KEY_dead_aboveverticalline,
                Gdk.KEY_dead_acute,
                Gdk.KEY_dead_belowbreve,
                Gdk.KEY_dead_belowcircumflex,
                Gdk.KEY_dead_belowcomma,
                Gdk.KEY_dead_belowdiaeresis,
                Gdk.KEY_dead_belowdot,
                Gdk.KEY_dead_belowmacron,
                Gdk.KEY_dead_belowring,
                Gdk.KEY_dead_belowtilde,
                Gdk.KEY_dead_belowverticalline,
                Gdk.KEY_dead_breve,
                Gdk.KEY_dead_capital_schwa,
                Gdk.KEY_dead_caron,
                Gdk.KEY_dead_cedilla,
                Gdk.KEY_dead_circumflex,
                Gdk.KEY_dead_currency,
                Gdk.KEY_dead_dasia,
                Gdk.KEY_dead_diaeresis,
                Gdk.KEY_dead_doubleacute,
                Gdk.KEY_dead_doublegrave,
                Gdk.KEY_dead_E,
                Gdk.KEY_dead_e,
                Gdk.KEY_dead_grave,
                Gdk.KEY_dead_greek,
                Gdk.KEY_dead_hook,
                Gdk.KEY_dead_horn,
                Gdk.KEY_dead_I,
                Gdk.KEY_dead_i,
                Gdk.KEY_dead_invertedbreve,
                Gdk.KEY_dead_iota,
                Gdk.KEY_dead_longsolidusoverlay,
                Gdk.KEY_dead_lowline,
                Gdk.KEY_dead_macron,
                Gdk.KEY_dead_O,
                Gdk.KEY_dead_o,
                Gdk.KEY_dead_ogonek,
                Gdk.KEY_dead_perispomeni,
                Gdk.KEY_dead_psili,
                Gdk.KEY_dead_semivoiced_sound,
                Gdk.KEY_dead_small_schwa,
                Gdk.KEY_dead_stroke,
                Gdk.KEY_dead_tilde,
                Gdk.KEY_dead_U,
                Gdk.KEY_dead_u,
                Gdk.KEY_dead_voiced_sound,
            }
        )

        cls._FUNCTION_KEYS = frozenset(
            {
                Gdk.KEY_F1,
                Gdk.KEY_F2,
                Gdk.KEY_F3,
                Gdk.KEY_F4,
                Gdk.KEY_F5,
                Gdk.KEY_F6,
                Gdk.KEY_F7,
                Gdk.KEY_F8,
                Gdk.KEY_F9,
                Gdk.KEY_F10,
                Gdk.KEY_F11,
                Gdk.KEY_F12,
            }
        )

        cls._LOCKING_KEYS = frozenset(
            {
                Gdk.KEY_Caps_Lock,
                Gdk.KEY_Num_Lock,
                Gdk.KEY_Scroll_Lock,
                Gdk.KEY_Shift_Lock,
            }
        )

        cls._MODIFIER_KEYS = frozenset(
            {
                Gdk.KEY_Alt_L,
                Gdk.KEY_Alt_R,
                Gdk.KEY_Control_L,
                Gdk.KEY_Control_R,
                Gdk.KEY_Meta_L,
                Gdk.KEY_Meta_R,
                Gdk.KEY_Super_L,
                Gdk.KEY_Super_R,
                Gdk.KEY_Shift_L,
                Gdk.KEY_Shift_R,
                Gdk.KEY_ISO_Level3_Shift,
            }
        )

        cls._NUMERIC_KEYS = frozenset(
            {
                Gdk.KEY_0,
                Gdk.KEY_1,
                Gdk.KEY_2,
                Gdk.KEY_3,
                Gdk.KEY_4,
                Gdk.KEY_5,
                Gdk.KEY_6,
                Gdk.KEY_7,
                Gdk.KEY_8,
                Gdk.KEY_9,
                Gdk.KEY_KP_0,
                Gdk.KEY_KP_1,
                Gdk.KEY_KP_2,
                Gdk.KEY_KP_3,
                Gdk.KEY_KP_4,
                Gdk.KEY_KP_5,
                Gdk.KEY_KP_6,
                Gdk.KEY_KP_7,
                Gdk.KEY_KP_8,
                Gdk.KEY_KP_9,
            }
        )

        cls._PUNCTUATION_KEYS = frozenset(
            {
                Gdk.KEY_acute,
                Gdk.KEY_ampersand,
                Gdk.KEY_apostrophe,
                Gdk.KEY_asciicircum,
                Gdk.KEY_asciitilde,
                Gdk.KEY_asterisk,
                Gdk.KEY_at,
                Gdk.KEY_backslash,
                Gdk.KEY_bar,
                Gdk.KEY_braceleft,
                Gdk.KEY_braceright,
                Gdk.KEY_bracketleft,
                Gdk.KEY_bracketright,
                Gdk.KEY_brokenbar,
                Gdk.KEY_cedilla,
                Gdk.KEY_cent,
                Gdk.KEY_colon,
                Gdk.KEY_comma,
                Gdk.KEY_copyright,
                Gdk.KEY_currency,
                Gdk.KEY_degree,
                Gdk.KEY_diaeresis,
                Gdk.KEY_dollar,
                Gdk.KEY_EuroSign,
                Gdk.KEY_equal,
                Gdk.KEY_exclam,
                Gdk.KEY_exclamdown,
                Gdk.KEY_grave,
                Gdk.KEY_greater,
                Gdk.KEY_guillemotleft,
                Gdk.KEY_guillemotright,
                Gdk.KEY_hyphen,
                Gdk.KEY_KP_Decimal,
                Gdk.KEY_KP_Add,
                Gdk.KEY_KP_Divide,
                Gdk.KEY_KP_Multiply,
                Gdk.KEY_KP_Subtract,
                Gdk.KEY_less,
                Gdk.KEY_macron,
                Gdk.KEY_minus,
                Gdk.KEY_notsign,
                Gdk.KEY_numbersign,
                Gdk.KEY_paragraph,
                Gdk.KEY_parenleft,
                Gdk.KEY_parenright,
                Gdk.KEY_percent,
                Gdk.KEY_period,
                Gdk.KEY_periodcentered,
                Gdk.KEY_plus,
                Gdk.KEY_plusminus,
                Gdk.KEY_question,
                Gdk.KEY_questiondown,
                Gdk.KEY_quotedbl,
                Gdk.KEY_quoteleft,
                Gdk.KEY_quoteright,
                Gdk.KEY_registered,
                Gdk.KEY_section,
                Gdk.KEY_semicolon,
                Gdk.KEY_slash,
                Gdk.KEY_sterling,
                Gdk.KEY_underscore,
                Gdk.KEY_yen,
            }
        )

        cls._KEYPAD_KEYCODES = frozenset(
            {
                63,
                77,
                79,
                80,
                81,
                82,
                83,
                84,
                85,
                86,
                87,
                88,
                89,
                90,
                91,
                104,
                106,
            }
        )

        cls._key_sets_initialized = True

    # pylint:disable=too-many-arguments
    # pylint:disable=too-many-positional-arguments
    def __init__(self, pressed: bool, keycode: int, keysym: int, modifiers: int, text: str) -> None:
        """Creates a new InputEvent of type KEYBOARD_EVENT.

        Arguments:
        - pressed: True if this is a key press, False for a release.
        - keycode: the hardware keycode.
        - keysym: the translated keysym.
        - modifiers: a bitflag giving the active modifiers.
        - text: the text that would be inserted if this key is pressed.
        """

        super().__init__(KEYBOARD_EVENT)
        self._initialize_key_sets()
        self.id: int = keysym
        self.type: Atspi.EventType = (
            Atspi.EventType.KEY_PRESSED_EVENT if pressed else Atspi.EventType.KEY_RELEASED_EVENT
        )
        self.hw_code: int = keycode
        self._text: str = text
        self.modifiers: int = modifiers & Gdk.ModifierType.MODIFIER_MASK  # pylint: disable=no-member
        if modifiers & (1 << Atspi.ModifierType.NUMLOCK):
            self.modifiers |= 1 << Atspi.ModifierType.NUMLOCK
        self.keyval_name: str = Gdk.keyval_name(keysym) or ""
        self.timestamp: float = time.time()
        self._script: default.Script | None = None
        self._window: Atspi.Accessible | None = None
        self._obj: Atspi.Accessible | None = None
        self._handler: Callable[[], bool] | None = None
        self._is_kp_with_numlock: bool = False

        # Some implementors don't include numlock in the modifiers. Unfortunately,
        # trying to heuristically hack around this just by looking at the event
        # is not reliable. Ditto regarding asking Gdk for the numlock state.
        if self._is_keypad_key() and self.modifiers & (1 << Atspi.ModifierType.NUMLOCK):
            self._is_kp_with_numlock = True

        modifier_manager = orca_modifier_manager.get_manager()
        if self.is_orca_modifier():
            modifier_manager.set_pressed_state(pressed)
        if modifier_manager.get_pressed_state():
            self.modifiers |= keybindings.ORCA_MODIFIER_MASK

    # pylint:enable=too-many-arguments
    # pylint:enable=too-many-positional-arguments

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, KeyboardEvent):
            return False

        if self.type == other.type and self.hw_code == other.hw_code:
            return self.timestamp == other.timestamp

        return False

    def __hash__(self) -> int:
        return hash((self.type, self.hw_code, self.timestamp))

    def __str__(self) -> str:
        if self.should_obscure():
            keyid = hw_code = modifiers = text = keyval_name = "*"
        else:
            keyid = str(self.id)
            hw_code = str(self.hw_code)
            modifiers = str(self.modifiers)
            keyval_name = self.keyval_name
            text = self._text

        return (
            f"KEYBOARD_EVENT:  type={self.type.value_name.upper()}\n"
            f"                 id={keyid}\n"
            f"                 hw_code={hw_code}\n"
            f"                 modifiers={modifiers}\n"
            f"                 text='{text}'\n"
            f"                 keyval_name='{keyval_name}'\n"
            f"                 timestamp={self.timestamp}\n"
            f"                 clickCount={self._click_count}"
        )

    def as_single_line_string(self) -> str:
        """Returns a single-line string representation of this event."""

        if self.should_obscure():
            return "(obscured)"

        return (
            f"'{self.keyval_name}' ({self.hw_code}) mods: {self.modifiers} {self.type.value_nick}"
        )

    def is_alt_control_or_orca_modified(self) -> bool:
        """Return True if this key is Alt, Control, or Orca modified."""

        return bool(
            self.modifiers & keybindings.CTRL_MODIFIER_MASK
            or self.modifiers & keybindings.ALT_MODIFIER_MASK
            or self.modifiers & keybindings.ORCA_MODIFIER_MASK,
        )

    def should_obscure(self) -> bool:
        """Returns True if we should obscure the details of this event."""

        if not AXUtilities.is_password_text(self._obj):
            return False

        if not self.is_printable_key() and not self.is_space():
            return False

        return not self.is_alt_control_or_orca_modified()

    def is_navigation_key(self) -> bool:
        """Return True if this is a navigation key."""

        return self.id in self._NAVIGATION_KEYS

    def is_action_key(self) -> bool:
        """Return True if this is an action key."""

        return self.id in self._ACTION_KEYS

    def is_alphabetic_key(self) -> bool:
        """Return True if this is an alphabetic key."""

        name = self.get_key_name()
        if len(name) != 1:
            return False

        return name.isalpha()

    def is_diacritical_key(self) -> bool:
        """Return True if this is a non-spacing diacritical key."""

        if self.id in self._DIACRITICAL_KEYS:
            return True

        name = self.get_key_name()
        if len(name) == 1:
            category = unicodedata.category(name)
            # Mn = Mark, nonspacing; Mc = Mark, spacing combining; Me = Mark, enclosing
            if category in ("Mn", "Mc", "Me"):
                return True

        return False

    def is_function_key(self) -> bool:
        """Return True if this is a function key."""

        return self.id in self._FUNCTION_KEYS

    def is_locking_key(self) -> bool:
        """Return True if this is a locking key."""

        if self.is_orca_modifier():
            return self._click_count == 2

        return self.id in self._LOCKING_KEYS

    def is_modifier_key(self) -> bool:
        """Return True if this is a modifier key."""

        return self.id in self._MODIFIER_KEYS or self.is_orca_modifier()

    def is_numeric_key(self) -> bool:
        """Return True if this is a numeric key."""

        return self.id in self._NUMERIC_KEYS

    def is_orca_modifier(self) -> bool:
        """Return True if this is the Orca modifier key."""

        if self.id == Gdk.KEY_KP_0 and self.modifiers & keybindings.SHIFT_MODIFIER_MASK:
            return orca_modifier_manager.get_manager().is_orca_modifier("KP_Insert")

        return orca_modifier_manager.get_manager().is_orca_modifier(self.keyval_name)

    def is_orca_modified(self) -> bool:
        """Return True if this key is Orca modified."""

        if self.is_orca_modifier():
            return False

        return bool(self.modifiers & keybindings.ORCA_MODIFIER_MASK)

    def _is_keypad_key(self) -> bool:
        """Return True if this is a keypad key based on keyval name or hardware keycode."""

        if self.keyval_name.startswith("KP"):
            return True
        return self.hw_code in self._KEYPAD_KEYCODES

    def is_keypad_key_with_numlock_on(self) -> bool:
        """Return True if this is a key pad key with numlock on."""

        return self._is_kp_with_numlock

    def is_printable_key(self) -> bool:
        """Return True if this is a printable key."""

        name = self.get_key_name()
        if len(name) != 1:
            return False

        return name.isprintable()

    def is_pressed_key(self) -> bool:
        """Returns True if the key is pressed"""

        return self.type == Atspi.EventType.KEY_PRESSED_EVENT

    def is_punctuation_key(self) -> bool:
        """Return True if this is a punctuation key."""

        return self.id in self._PUNCTUATION_KEYS

    def is_space(self) -> bool:
        """Return True if this is the space key."""

        return self.id == Gdk.KEY_space

    def get_locking_state(self) -> bool | None:
        """Returns True if the event locked a locking key, False if the event unlocked it, and None
        if not a locking key."""

        if not self.is_locking_key():
            return None

        if self.id == Gdk.KEY_Caps_Lock:
            mod = Atspi.ModifierType.SHIFTLOCK
        elif self.id == Gdk.KEY_Shift_Lock:
            mod = Atspi.ModifierType.SHIFT
        elif self.id == Gdk.KEY_Num_Lock:
            mod = Atspi.ModifierType.NUMLOCK
        else:
            return None

        return not self.modifiers & (1 << mod)

    def get_locking_state_string(self) -> str:
        """Returns the string reflecting the locking state."""

        locked = self.get_locking_state()
        if locked is None:
            return ""

        if not locked:
            return messages.LOCKING_KEY_STATE_OFF

        return messages.LOCKING_KEY_STATE_ON

    def get_key_name(self) -> str:
        """Returns the string to be used for presenting the key."""

        if self._text.strip() and self._text.isprintable():
            return self._text

        name = keynames.get_key_name(self.keyval_name)
        if name is not None:
            return name

        unicode_codepoint = Gdk.keyval_to_unicode(self.id)
        if unicode_codepoint:
            char = chr(unicode_codepoint)
            if char.isprintable():
                return char

        return self.keyval_name

    def get_object(self) -> Atspi.Accessible | None:
        """Returns the object believed to be associated with this key event."""

        return self._obj

    def set_object(self, obj: Atspi.Accessible | None) -> None:
        """Sets the object believed to be associated with this key event."""

        self._ensure_allowed_call("set_object")

        self._obj = obj

    def get_window(self) -> Atspi.Accessible | None:
        """Returns the window believed to be associated with this key event."""

        return self._window

    def set_window(self, window: Atspi.Accessible | None) -> None:
        """Sets the window believed to be associated with this key event."""

        self._ensure_allowed_call("set_window")

        self._window = window

    def get_script(self) -> default.Script | None:
        """Returns the script believed to be associated with this key event."""

        return self._script

    def set_script(self, script: default.Script | None) -> None:
        """Sets the script believed to be associated with this key event."""

        self._ensure_allowed_call("set_script")

        self._script = script

    @staticmethod
    def _ensure_allowed_call(method_name: str) -> None:
        """Raise PermissionError if the caller is not an Orca input-event module."""

        try:
            filename = sys._getframe(2).f_code.co_filename  # pylint: disable=protected-access
        except ValueError:
            filename = ""

        if orca.is_orca(filename, "input_event"):
            return

        raise PermissionError(f"Unauthorized setter of input event property: {method_name}")

    def _present(self) -> None:
        if not self._script:
            return

        if self.is_pressed_key():
            presentation_manager.get_manager().interrupt_presentation()

        # pylint: disable-next=import-outside-toplevel
        from . import learn_mode_presenter, sleep_mode_manager

        if learn_mode_presenter.get_presenter().is_active():
            return

        if sleep_mode_manager.get_manager().is_active_for_app(self._script.app):
            return

        presentation_manager.get_manager().present_keyboard_event(self)

    def process(self) -> None:
        """Processes this input event."""

        start_time = time.time()
        should_obscure = self.should_obscure()
        if not should_obscure:
            data = f"'{self.keyval_name}' ({self.hw_code})"
        else:
            data = "(obscured)"

        debug.print_message(debug.LEVEL_INFO, f"\n{self}")

        msg = f"\nvvvvv PROCESS {self.type.value_name.upper()}: {data} vvvvv"
        debug.print_message(debug.LEVEL_INFO, msg, False)

        tokens = ["SCRIPT:", self._script]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        tokens = ["WINDOW:", self._window]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        tokens = ["LOCATION:", self._obj]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        if self._script:
            script = self._script
            command = command_manager.get_manager().get_command_for_event(self)
            if not should_obscure and command is not None:
                tokens = ["COMMAND:", command.get_name()]
                debug.print_tokens(debug.LEVEL_INFO, tokens, True)

            cmd_manager = command_manager.get_manager()
            modal_handler = cmd_manager.get_modal_handler()
            modal_handler_claimed_event = False
            if modal_handler is not None:
                can_handle_modal_event = cmd_manager.can_modal_handler_handle_event(
                    modal_handler,
                    command,
                )
                if not can_handle_modal_event:
                    msg = "KEYBOARD EVENT: Skipping user-extension modal handler without command"
                    debug.print_message(debug.LEVEL_INFO, msg, True)
                elif modal_handler.will_handle_event(script, self, command):
                    msg = "KEYBOARD EVENT: Modal handler claimed the event"
                    debug.print_message(debug.LEVEL_INFO, msg, True)
                    self._handler = lambda: modal_handler.handle_event(script, self, command)
                    modal_handler_claimed_event = True

            if not modal_handler_claimed_event and command is not None and command.is_enabled():
                self._handler = lambda: command.execute(script, self)

        if self.is_orca_modifier():
            if self._click_count == 2:
                orca_modifier_manager.get_manager().toggle_modifier(self)

        if self.is_pressed_key() and self.keyval_name == "Num_Lock":
            numlock_on = self.get_locking_state()
            if numlock_on is not None:
                command_manager.get_manager().handle_numlock_toggled(numlock_on)

        self._present()

        if self.is_pressed_key() and self._handler:
            GLib.timeout_add(1, self._handle)

        msg = f"TOTAL PROCESSING TIME: {time.time() - start_time:.4f}"
        debug.print_message(debug.LEVEL_INFO, msg, True)

        msg = f"^^^^^ PROCESS {self.type.value_name.upper()}: {data} ^^^^^\n"
        debug.print_message(debug.LEVEL_INFO, msg, False)

    def _handle(self) -> bool:
        """Handles this event after a timeout. Returns False to stop the timeout."""

        start_time = time.time()
        data = f"'{self.keyval_name}' ({self.hw_code})"
        msg = f"\nvvvvv HANDLE {self.type.value_name.upper()}: {data} vvvvv"
        debug.print_message(debug.LEVEL_INFO, msg, False)

        if self._handler:
            try:
                self._handler()
            except GLib.GError as error:
                msg = f"KEYBOARD EVENT: Exception calling handler: {error}"
                debug.print_message(debug.LEVEL_WARNING, msg, True)

        msg = f"TOTAL PROCESSING TIME: {time.time() - start_time:.4f}"
        debug.print_message(debug.LEVEL_INFO, msg, True)

        msg = f"^^^^^ HANDLE {self.type.value_name.upper()}: {data} ^^^^^\n"
        debug.print_message(debug.LEVEL_INFO, msg, False)

        return False


class BrailleEvent(InputEvent):
    """Provides support for handling braille events."""

    def __init__(self, event: dict) -> None:
        super().__init__(BRAILLE_EVENT)
        self.event: dict = event
        self._script: default.Script | None = script_manager.get_manager().get_active_script()

    def __str__(self) -> str:
        return f"{self.type.upper()} {self.event}"

    def get_command(self) -> BrailleCommand | None:
        """Returns the BrailleCommand associated with this event."""

        braille_key: int = self.event["command"]
        command = command_manager.get_manager().get_command_for_braille_event(braille_key)
        tokens = [f"BRAILLE EVENT: Command for braille key {braille_key} is", command]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)
        return command

    def process(self):
        """Processes this event."""

        tokens = ["\nvvvvv PROCESS", self, "vvvvv"]
        debug.print_tokens(debug.LEVEL_INFO, tokens, False)

        start_time = time.time()
        result = self._process()
        msg = f"TOTAL PROCESSING TIME: {time.time() - start_time:.4f}"
        debug.print_message(debug.LEVEL_INFO, msg, False)

        tokens = ["^^^^^ PROCESS", self, "^^^^^"]
        debug.print_tokens(debug.LEVEL_INFO, tokens, False)
        return result

    def _process(self):
        # pylint: disable-next=import-outside-toplevel
        from . import learn_mode_presenter

        presenter = learn_mode_presenter.get_presenter()

        command = self.get_command()
        if not command:
            if presenter.is_active():
                tokens = ["BRAILLE EVENT: No command, but in learn mode", self]
                debug.print_tokens(debug.LEVEL_INFO, tokens, True)
                return True

            tokens = ["BRAILLE EVENT: No command found for", self]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            return False

        tokens = ["BRAILLE EVENT: Command is:", command]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        if presenter.is_active():
            if presenter.handle_braille_event(self._script, self, command):
                return True

        command.execute(self._script, self)
        return True


class MouseButtonEvent(InputEvent):
    """Provides support for handling mouse button events."""

    # TODO - JD: Remove this and the validation logic once we have a fix for
    # https://gitlab.gnome.org/GNOME/at-spi2-core/-/issues/194.
    try:
        display = Gdk.Display.get_default()  # pylint: disable=no-value-for-parameter
        seat = Gdk.Display.get_default_seat(display)
        _pointer = seat.get_pointer()
    except Exception:  # pylint: disable=broad-exception-caught
        _pointer = None

    def __init__(self, event):
        super().__init__(MOUSE_BUTTON_EVENT)
        self.x = event.detail1
        self.y = event.detail2
        self.pressed = event.type.endswith("p")
        self.button = event.type[len("mouse:button:") : -1]
        self._script = script_manager.get_manager().get_active_script()
        self.window = focus_manager.get_manager().get_active_window()
        self.app = None

        if self.pressed:
            self._validate_coordinates()

        if not self._script:
            return

        if not AXUtilities.can_be_active_window(self.window):
            self.window = AXUtilities.find_active_window()

        if not self.window:
            return

        self.app = AXUtilities.get_application(self.window)

    def _validate_coordinates(self):
        if not self._pointer:
            return

        x, y = self._pointer.get_position()[1:]
        if math.sqrt((self.x - x) ** 2 + (self.y - y) ** 2) < 25:
            return

        msg = (
            f"WARNING: Event coordinates ({self.x}, {self.y}) may be bogus. Updating to ({x}, {y})"
        )
        debug.print_message(debug.LEVEL_INFO, msg, True)
        self.x, self.y = x, y


class RemoteControllerEvent(InputEvent):
    """A simple input event whose main purpose is identification of the origin."""

    def __init__(self, event_type: str = REMOTE_CONTROLLER_EVENT):
        super().__init__(event_type)


class RemoteControllerTestingEvent(RemoteControllerEvent):
    """A remote controller event from a test-only command, not a real user action."""

    def __init__(self):
        super().__init__(REMOTE_CONTROLLER_TESTING_EVENT)
