# Orca
#
# Copyright 2024 Igalia, S.L.
# Copyright 2024 GNOME Foundation Inc.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
#
# 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-lines
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-arguments
# pylint: disable=too-many-positional-arguments

"""Provides utilities for managing input events."""

from __future__ import annotations

from typing import TYPE_CHECKING

import gi

gi.require_version("Atspi", "2.0")
from gi.repository import Atspi

from . import ax_device_manager, debug, focus_manager, input_event, script_manager
from .ax_utilities import AXUtilities

if TYPE_CHECKING:
    from . import keybindings

_DOUBLE_CLICK_TIMEOUT: float = 0.5


class InputEventManager:
    """Provides utilities for managing input events."""

    def __init__(self) -> None:
        self._last_input_event: input_event.InputEvent | None = None
        self._last_non_modifier_key_event: input_event.KeyboardEvent | None = None
        self._device: Atspi.Device | None = None
        self._mapped_keycodes: list[int] = []
        self._mapped_keysyms: list[int] = []
        self._grabbed_bindings: dict[int, keybindings.KeyBinding] = {}
        self._paused: bool = False
        self._key_pressed_id: int = 0
        self._key_released_id: int = 0

    def start_key_watcher(self) -> None:
        """Starts the watcher for keyboard input events."""

        msg = "INPUT EVENT MANAGER: Starting key watcher."
        debug.print_message(debug.LEVEL_INFO, msg, True)
        self._device = ax_device_manager.get_manager().get_device()

        atspi_version = Atspi.get_version()  # pylint: disable=no-value-for-parameter
        if atspi_version[0] > 2 or atspi_version[1] >= 60:
            self._key_pressed_id = self._device.connect("key-pressed", self._on_key_pressed)
            self._key_released_id = self._device.connect("key-released", self._on_key_released)
        else:
            self._device.add_key_watcher(self.process_keyboard_event)

    def stop_key_watcher(self) -> None:
        """Stops the watcher for keyboard input events."""

        msg = "INPUT EVENT MANAGER: Stopping key watcher."
        debug.print_message(debug.LEVEL_INFO, msg, True)
        if self._device is None:
            return

        atspi_version = Atspi.get_version()  # pylint: disable=no-value-for-parameter
        if atspi_version[0] > 2 or atspi_version[1] >= 60:
            self._device.disconnect(self._key_pressed_id)
            self._device.disconnect(self._key_released_id)
        else:
            self._device = None

    def has_device(self) -> bool:
        """Returns True if there is an active input device."""

        return self._device is not None

    def pause_key_watcher(self, pause: bool = True, reason: str = "") -> None:
        """Pauses processing of keyboard input events."""

        msg = f"INPUT EVENT MANAGER: Pause queueing: {pause}. {reason}"
        debug.print_message(debug.LEVEL_INFO, msg, True)
        self._paused = pause

    def add_grabs_for_keybinding(
        self,
        binding: keybindings.KeyBinding,
        orca_modifiers: list[str],
    ) -> list[int]:
        """Adds grabs for binding, returns grab IDs."""

        if binding.has_grabs():
            tokens = ["INPUT EVENT MANAGER:", binding, "already has grabs."]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            return binding.get_grab_ids()

        if self._device is None:
            tokens = ["INPUT EVENT MANAGER: No device to add grab for", binding]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            return []

        grab_ids = []
        for kd in binding.key_definitions(orca_modifiers):
            grab_id = self._device.add_key_grab(kd, None)
            # When we have double/triple-click bindings, the single-click binding will be
            # registered first, and subsequent attempts to register what is externally the
            # same grab will fail. If we only have a double/triple-click, it succeeds.
            # A grab id of 0 indicates failure.
            if grab_id == 0:
                continue
            grab_ids.append(grab_id)
            self._grabbed_bindings[grab_id] = binding

        return grab_ids

    def remove_grabs_for_keybinding(self, binding: keybindings.KeyBinding) -> None:
        """Removes grabs for binding."""

        if self._device is None:
            tokens = ["INPUT EVENT MANAGER: No device to remove grab from", binding]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            return

        grab_ids = binding.get_grab_ids()
        if not grab_ids:
            return

        for grab_id in grab_ids:
            self._device.remove_key_grab(grab_id)
            removed = self._grabbed_bindings.pop(grab_id, None)
            if removed is None:
                msg = f"INPUT EVENT MANAGER: No key binding for grab id {grab_id}"
                debug.print_message(debug.LEVEL_INFO, msg, True)

    def map_keysym_to_modifier(self, keysym: int) -> int:
        """Maps keysym as a modifier, returns the newly-mapped modifier."""

        if self._device is None:
            msg = f"INPUT EVENT MANAGER: No device to map keysym {keysym} to modifier"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return 0

        self._mapped_keysyms.append(keysym)
        return self._device.map_keysym_modifier(keysym)

    def unmap_all_modifiers(self) -> None:
        """Unmaps all previously mapped modifiers."""

        if self._device is None:
            msg = "INPUT EVENT MANAGER: No device to unmap all modifiers from"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return

        for keycode in self._mapped_keycodes:
            self._device.unmap_modifier(keycode)

        for keysym in self._mapped_keysyms:
            self._device.unmap_keysym_modifier(keysym)

        self._mapped_keycodes.clear()
        self._mapped_keysyms.clear()

    def add_grab_for_modifier(self, modifier: str, keysym: int, keycode: int) -> int:
        """Adds grab for modifier, returns grab id."""

        if self._device is None:
            msg = f"INPUT EVENT MANAGER: No device to add grab for {modifier}"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return -1

        kd = Atspi.KeyDefinition()
        kd.keysym = keysym
        kd.keycode = keycode
        kd.modifiers = 0
        grab_id = self._device.add_key_grab(kd)

        msg = f"INPUT EVENT MANAGER: Grab id for {modifier}: {grab_id}"
        debug.print_message(debug.LEVEL_INFO, msg, True)
        return grab_id

    def remove_grab_for_modifier(self, modifier: str, grab_id: int) -> None:
        """Removes grab for modifier."""

        if self._device is None:
            msg = f"INPUT EVENT MANAGER: No device to remove grab from {modifier}"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return

        self._device.remove_key_grab(grab_id)
        msg = f"INPUT EVENT MANAGER: Grab id removed for {modifier}: {grab_id}"
        debug.print_message(debug.LEVEL_INFO, msg, True)

    def grab_keyboard(self, reason: str = "") -> None:
        """Grabs the keyboard, e.g. when entering learn mode."""

        msg = "INPUT EVENT MANAGER: Grabbing keyboard"
        if reason:
            msg += f" Reason: {reason}"
        debug.print_message(debug.LEVEL_INFO, msg, True)
        Atspi.Device.grab_keyboard(self._device)

    def ungrab_keyboard(self, reason: str = "") -> None:
        """Removes keyboard grab, e.g. when exiting learn mode."""

        msg = "INPUT EVENT MANAGER: Ungrabbing keyboard"
        if reason:
            msg += f" Reason: {reason}"
        debug.print_message(debug.LEVEL_INFO, msg, True)
        Atspi.Device.ungrab_keyboard(self._device)

    def process_braille_event(self, event: Atspi.Event) -> bool:
        """Processes this Braille event."""

        braille_event = input_event.BrailleEvent(event)
        result = braille_event.process()
        self._last_input_event = braille_event
        self._last_non_modifier_key_event = None
        return result

    def process_mouse_button_event(self, event: Atspi.Event) -> None:
        """Processes this Mouse event."""

        mouse_event = input_event.MouseButtonEvent(event)
        mouse_event.set_click_count(self._determine_mouse_event_click_count(mouse_event))
        self._last_input_event = mouse_event

    def process_remote_controller_event(self, event: input_event.RemoteControllerEvent) -> None:
        """Processes this RemoteController event."""

        # TODO - JD: It probably makes sense to process remote controller events here rather
        # than just updating state.
        self._last_input_event = event
        self._last_non_modifier_key_event = None

    def _on_key_pressed(
        self,
        _device: Atspi.Device,
        keycode: int,
        keysym: int,
        modifiers: int,
        text: str,
    ) -> None:
        self.process_keyboard_event(_device, True, keycode, keysym, modifiers, text)

    def _on_key_released(
        self,
        _device: Atspi.Device,
        keycode: int,
        keysym: int,
        modifiers: int,
        text: str,
    ) -> None:
        self.process_keyboard_event(_device, False, keycode, keysym, modifiers, text)

    def process_keyboard_event(
        self,
        _device: Atspi.Device,
        pressed: bool,
        keycode: int,
        keysym: int,
        modifiers: int,
        text: str,
    ) -> bool:
        """Processes this Atspi keyboard event."""

        if self._paused:
            msg = "INPUT EVENT MANAGER: Keyboard event processing is paused."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        event = input_event.KeyboardEvent(pressed, keycode, keysym, modifiers, text)
        if event in [self._last_input_event, self._last_non_modifier_key_event]:
            msg = "INPUT EVENT MANAGER: Received duplicate event."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        manager = focus_manager.get_manager()
        if pressed:
            window = manager.get_active_window()
            if not AXUtilities.can_be_active_window(window):
                new_window = AXUtilities.find_active_window()
                if new_window is not None:
                    window = new_window
                    tokens = ["INPUT EVENT MANAGER: Updating window and active window to", window]
                    debug.print_tokens(debug.LEVEL_INFO, tokens, True)
                    manager.set_active_window(window)
                else:
                    # One example: Brave's popup menus live in frames which lack the active state.
                    tokens = ["WARNING:", window, "cannot be active window. No alternative found."]
                    debug.print_tokens(debug.LEVEL_WARNING, tokens, True)
            event.set_window(window)
            event.set_object(manager.get_locus_of_focus())
            event.set_script(script_manager.get_manager().get_active_script())
        elif self.last_event_was_keyboard():
            assert isinstance(self._last_input_event, input_event.KeyboardEvent)
            event.set_window(self._last_input_event.get_window())
            event.set_object(self._last_input_event.get_object())
            event.set_script(self._last_input_event.get_script())
        else:
            event.set_window(manager.get_active_window())
            event.set_object(manager.get_locus_of_focus())
            event.set_script(script_manager.get_manager().get_active_script())

        event.set_click_count(self._determine_keyboard_event_click_count(event))
        event.process()

        if event.is_modifier_key():
            if self.is_release_for(event, self._last_input_event):
                msg = "INPUT EVENT MANAGER: Clearing last non modifier key event"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                self._last_non_modifier_key_event = None
        else:
            self._last_non_modifier_key_event = event
        self._last_input_event = event
        return True

    def _determine_keyboard_event_click_count(self, event: input_event.KeyboardEvent) -> int:
        """Determines the click count of event."""

        if not self.last_event_was_keyboard():
            return 1

        if event.is_modifier_key():
            last_event = self._last_input_event
        else:
            last_event = self._last_non_modifier_key_event or self._last_input_event

        assert isinstance(last_event, input_event.KeyboardEvent)
        if (
            (event.time - last_event.time > _DOUBLE_CLICK_TIMEOUT)
            or (event.keyval_name != last_event.keyval_name)
            or (event.get_object() != last_event.get_object())
        ):
            return 1

        last_count = last_event.get_click_count()
        if not event.is_pressed_key() or last_event.is_pressed_key():
            return last_count
        if (event.is_modifier_key() and last_count == 2) or last_count == 3:
            return 1

        # Only increment click count if there are multi-click bindings for this key,
        # OR if it's an Orca modifier (double-click toggles the locking state).
        # This prevents accidental "double-clicks" from breaking commands that only
        # have single-click bindings.
        if event.is_orca_modifier():
            return last_count + 1

        # pylint: disable-next=import-outside-toplevel
        from . import command_manager

        has_multi_click = command_manager.get_manager().has_multi_click_bindings(
            event.id,
            event.hw_code,
            event.modifiers,
        )
        return last_count + 1 if has_multi_click else 1

    def _determine_mouse_event_click_count(self, event: input_event.MouseButtonEvent) -> int:
        """Determines the click count of event."""

        if not self.last_event_was_mouse_button():
            return 1

        assert isinstance(self._last_input_event, input_event.MouseButtonEvent)
        if not event.pressed:
            return self._last_input_event.get_click_count()
        if self._last_input_event.button != event.button:
            return 1
        if event.time - self._last_input_event.time > _DOUBLE_CLICK_TIMEOUT:
            return 1

        return self._last_input_event.get_click_count() + 1

    def last_event_was_keyboard(self) -> bool:
        """Returns True if the last event is a keyboard event."""

        return isinstance(self._last_input_event, input_event.KeyboardEvent)

    def last_event_was_mouse_button(self) -> bool:
        """Returns True if the last event is a mouse button event."""

        return isinstance(self._last_input_event, input_event.MouseButtonEvent)

    def is_release_for(self, event1, event2):
        """Returns True if event1 is a release for event2."""

        if event1 is None or event2 is None:
            return False

        if not isinstance(event1, input_event.KeyboardEvent) or not isinstance(
            event2,
            input_event.KeyboardEvent,
        ):
            return False

        if event1.is_pressed_key() or not event2.is_pressed_key():
            return False

        result = (
            event1.id == event2.id
            and event1.hw_code == event2.hw_code
            and event1.keyval_name == event2.keyval_name
        )

        if result and not event1.is_modifier_key():
            result = event1.modifiers == event2.modifiers

        msg = (
            f"INPUT EVENT MANAGER: {event1.as_single_line_string()} "
            f"is release for {event2.as_single_line_string()}: {result}"
        )
        debug.print_message(debug.LEVEL_INFO, msg, True)
        return result

    def last_event_equals_or_is_release_for_event(self, event):
        """Returns True if the last event equals the provided event, or is the release for it."""

        if self._last_input_event is event:
            return True

        if not self.last_event_was_keyboard():
            return False

        if self._last_non_modifier_key_event is None:
            return False

        if event == self._last_non_modifier_key_event:
            return True

        return self.is_release_for(self._last_non_modifier_key_event, event)

    def _last_key_and_modifiers(self):
        """Returns the last keyval name and modifiers"""

        if self._last_non_modifier_key_event is None:
            return "", 0

        if not self.last_event_was_keyboard():
            return "", 0

        return self._last_non_modifier_key_event.keyval_name, self._last_input_event.modifiers

    def last_event_was_command(self):
        """Returns True if the last event is believed to be a command."""

        if bool(self._last_key_and_modifiers()[1] & 1 << Atspi.ModifierType.CONTROL):
            msg = "INPUT EVENT MANAGER: Last event was command."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        return False

    def last_event_was_shortcut_for(self, obj):
        """Returns True if the last event is believed to be a shortcut key for obj."""

        string = self._last_key_and_modifiers()[0]
        if not string:
            return False

        rv = AXUtilities.has_matching_shortcut(obj, string)
        if rv:
            tokens = ["INPUT EVENT MANAGER: Last event was shortcut for", obj]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
        return rv

    def last_event_was_printable_key(self):
        """Returns True if the last event is believed to be a printable key."""

        if not self.last_event_was_keyboard():
            return False

        if self._last_input_event.is_printable_key():
            msg = "INPUT EVENT MANAGER: Last event was printable key"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        return False

    def last_event_was_caret_navigation(self):
        """Returns True if the last event is believed to be caret navigation."""

        return (
            self.last_event_was_character_navigation()
            or self.last_event_was_word_navigation()
            or self.last_event_was_line_navigation()
            or self.last_event_was_line_boundary_navigation()
            or self.last_event_was_file_boundary_navigation()
            or self.last_event_was_page_navigation()
        )

    def last_event_was_caret_selection(self):
        """Returns True if the last event is believed to be caret selection."""

        string, mods = self._last_key_and_modifiers()
        if string not in ["Home", "End", "Up", "Down", "Left", "Right"]:
            rv = False
        else:
            rv = bool(mods & 1 << Atspi.ModifierType.SHIFT)

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was caret selection"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_backward_caret_navigation(self):
        """Returns True if the last event is believed to be backward caret navigation."""

        string, mods = self._last_key_and_modifiers()
        if string not in ["Up", "Left"]:
            rv = False
        else:
            rv = not mods & 1 << Atspi.ModifierType.SHIFT

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was backward caret navigation"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_forward_caret_navigation(self):
        """Returns True if the last event is believed to be forward caret navigation."""

        string, mods = self._last_key_and_modifiers()
        if string not in ["Down", "Right"]:
            rv = False
        else:
            rv = not mods & 1 << Atspi.ModifierType.SHIFT

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was forward caret navigation"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_forward_caret_selection(self):
        """Returns True if the last event is believed to be forward caret selection."""

        string, mods = self._last_key_and_modifiers()
        if string not in ["Down", "Right"]:
            rv = False
        else:
            rv = bool(mods & 1 << Atspi.ModifierType.SHIFT)

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was forward caret selection"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_character_navigation(self):
        """Returns True if the last event is believed to be character navigation."""

        string, mods = self._last_key_and_modifiers()
        if (
            string not in ["Left", "Right"]
            or mods & 1 << Atspi.ModifierType.CONTROL
            or mods & 1 << Atspi.ModifierType.ALT
        ):
            rv = False
        else:
            rv = True

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was character navigation"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_word_navigation(self):
        """Returns True if the last event is believed to be word navigation."""

        string, mods = self._last_key_and_modifiers()
        if string not in ["Left", "Right"]:
            rv = False
        else:
            rv = bool(mods & 1 << Atspi.ModifierType.CONTROL)

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was word navigation"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_previous_word_navigation(self):
        """Returns True if the last event is believed to be previous-word navigation."""

        string, mods = self._last_key_and_modifiers()
        if string != "Left":
            rv = False
        else:
            rv = bool(mods & 1 << Atspi.ModifierType.CONTROL)

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was previous-word navigation"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_next_word_navigation(self):
        """Returns True if the last event is believed to be next-word navigation."""

        string, mods = self._last_key_and_modifiers()
        if string != "Right":
            rv = False
        else:
            rv = bool(mods & 1 << Atspi.ModifierType.CONTROL)

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was next-word navigation"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_line_navigation(self):
        """Returns True if the last event is believed to be line navigation."""

        string, mods = self._last_key_and_modifiers()
        if string not in ["Up", "Down"] or mods & 1 << Atspi.ModifierType.CONTROL:
            rv = False
        else:
            focus = focus_manager.get_manager().get_locus_of_focus()
            if AXUtilities.is_single_line(focus):
                rv = False
            else:
                rv = not AXUtilities.is_widget_controlled_by_line_navigation(focus)

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was line navigation"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_paragraph_navigation(self):
        """Returns True if the last event is believed to be paragraph navigation."""

        string, mods = self._last_key_and_modifiers()
        if not (string in ["Up", "Down"] and mods & 1 << Atspi.ModifierType.CONTROL):
            rv = False
        else:
            rv = not mods & 1 << Atspi.ModifierType.SHIFT

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was paragraph navigation"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_line_boundary_navigation(self):
        """Returns True if the last event is believed to be navigation to start/end of line."""

        string, mods = self._last_key_and_modifiers()
        if string not in ["Home", "End"]:
            rv = False
        else:
            rv = not mods & 1 << Atspi.ModifierType.CONTROL

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was line boundary navigation"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_file_boundary_navigation(self):
        """Returns True if the last event is believed to be navigation to top/bottom of file."""

        string, mods = self._last_key_and_modifiers()
        if string not in ["Home", "End"]:
            rv = False
        else:
            rv = bool(mods & 1 << Atspi.ModifierType.CONTROL)

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was file boundary navigation"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_page_navigation(self):
        """Returns True if the last event is believed to be page navigation."""

        string, mods = self._last_key_and_modifiers()
        if string not in ["Page_Up", "Page_Down"] or mods & 1 << Atspi.ModifierType.CONTROL:
            rv = False
        else:
            focus = focus_manager.get_manager().get_locus_of_focus()
            if AXUtilities.is_single_line(focus):
                rv = False
            else:
                rv = not AXUtilities.is_widget_controlled_by_line_navigation(focus)

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was page navigation"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_page_switch(self):
        """Returns True if the last event is believed to be a page switch."""

        string, mods = self._last_key_and_modifiers()
        if string.isnumeric():
            rv = bool(mods & 1 << Atspi.ModifierType.ALT)
        elif string in ["Page_Up", "Page_Down"]:
            rv = bool(mods & 1 << Atspi.ModifierType.CONTROL)
        else:
            rv = False

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was page switch"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_tab_navigation(self):
        """Returns True if the last event is believed to be Tab navigation."""

        string, mods = self._last_key_and_modifiers()
        if (
            string not in ["Tab", "ISO_Left_Tab"]
            or mods & 1 << Atspi.ModifierType.CONTROL
            or mods & 1 << Atspi.ModifierType.ALT
        ):
            rv = False
        else:
            rv = True

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was Tab navigation"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_table_sort(self):
        """Returns True if the last event is believed to be a table sort."""

        focus = focus_manager.get_manager().get_locus_of_focus()
        if not AXUtilities.is_table_header(focus):
            rv = False
        elif self.last_event_was_mouse_button():
            rv = self.last_event_was_primary_click()
        elif self.last_event_was_keyboard():
            rv = self.last_event_was_return_or_space()
        else:
            rv = False

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was table sort"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_unmodified_arrow(self):
        """Returns True if the last event is an unmodified arrow."""

        string, mods = self._last_key_and_modifiers()
        if string not in ["Left", "Right", "Up", "Down"]:
            return False

        if (
            mods & 1 << Atspi.ModifierType.CONTROL
            or mods & 1 << Atspi.ModifierType.SHIFT
            or mods & 1 << Atspi.ModifierType.ALT
        ):
            return False

        # TODO: JD - 8 is the value of keybindings.MODIFIER_ORCA, but we need to
        # avoid a circular import.
        return not mods & 1 << 8

    def last_event_was_alt_modified(self):
        """Returns True if the last event was alt-modified."""

        mods = self._last_key_and_modifiers()[-1]
        return mods & 1 << Atspi.ModifierType.ALT

    def last_event_was_backspace(self):
        """Returns True if the last event is BackSpace."""

        return self._last_key_and_modifiers()[0] == "BackSpace"

    def last_event_was_down(self):
        """Returns True if the last event is Down."""

        return self._last_key_and_modifiers()[0] == "Down"

    def last_event_was_f1(self):
        """Returns True if the last event is F1."""

        return self._last_key_and_modifiers()[0] == "F1"

    def last_event_was_left(self):
        """Returns True if the last event is Left."""

        return self._last_key_and_modifiers()[0] == "Left"

    def last_event_was_left_or_right(self):
        """Returns True if the last event is Left or Right."""

        return self._last_key_and_modifiers()[0] in ["Left", "Right"]

    def last_event_was_page_up_or_page_down(self):
        """Returns True if the last event is Page_Up or Page_Down."""

        return self._last_key_and_modifiers()[0] in ["Page_Up", "Page_Down"]

    def last_event_was_right(self):
        """Returns True if the last event is Right."""

        return self._last_key_and_modifiers()[0] == "Right"

    def last_event_was_return(self):
        """Returns True if the last event is Return."""

        return self._last_key_and_modifiers()[0] == "Return"

    def last_event_was_return_or_space(self):
        """Returns True if the last event is Return or space."""

        return self._last_key_and_modifiers()[0] in ["Return", "space", " "]

    def last_event_was_return_tab_or_space(self):
        """Returns True if the last event is Return, Tab, or space."""

        return self._last_key_and_modifiers()[0] in ["Return", "Tab", "space", " "]

    def last_event_was_space(self):
        """Returns True if the last event is space."""

        return self._last_key_and_modifiers()[0] in [" ", "space"]

    def last_event_was_tab(self):
        """Returns True if the last event is Tab."""

        return self._last_key_and_modifiers()[0] == "Tab"

    def last_event_was_up(self):
        """Returns True if the last event is Up."""

        return self._last_key_and_modifiers()[0] == "Up"

    def last_event_was_up_or_down(self):
        """Returns True if the last event is Up or Down."""

        return self._last_key_and_modifiers()[0] in ["Up", "Down"]

    def last_event_was_delete(self):
        """Returns True if the last event is believed to be delete."""

        string, mods = self._last_key_and_modifiers()
        if string in ["Delete", "KP_Delete"]:
            rv = True
        elif string.lower() == "d":
            rv = bool(mods & 1 << Atspi.ModifierType.CONTROL)
        else:
            rv = False

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was delete"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_cut(self):
        """Returns True if the last event is believed to be the cut command."""

        string, mods = self._last_key_and_modifiers()
        if string.lower() != "x":
            return False

        if mods & 1 << Atspi.ModifierType.CONTROL and not mods & 1 << Atspi.ModifierType.SHIFT:
            msg = "INPUT EVENT MANAGER: Last event was cut"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True
        return False

    def last_event_was_copy(self):
        """Returns True if the last event is believed to be the copy command."""

        string, mods = self._last_key_and_modifiers()
        if string.lower() != "c" or not mods & 1 << Atspi.ModifierType.CONTROL:
            rv = False
        elif AXUtilities.is_terminal(self._last_input_event.get_object()):
            rv = mods & 1 << Atspi.ModifierType.SHIFT
        else:
            rv = not mods & 1 << Atspi.ModifierType.SHIFT

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was copy"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_paste(self):
        """Returns True if the last event is believed to be the paste command."""

        string, mods = self._last_key_and_modifiers()
        if string.lower() != "v" or not mods & 1 << Atspi.ModifierType.CONTROL:
            rv = False
        elif AXUtilities.is_terminal(self._last_input_event.get_object()):
            rv = mods & 1 << Atspi.ModifierType.SHIFT
        else:
            rv = not mods & 1 << Atspi.ModifierType.SHIFT

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was paste"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_undo(self):
        """Returns True if the last event is believed to be the undo command."""

        string, mods = self._last_key_and_modifiers()
        if string.lower() != "z":
            return False
        if mods & 1 << Atspi.ModifierType.CONTROL and not mods & 1 << Atspi.ModifierType.SHIFT:
            msg = "INPUT EVENT MANAGER: Last event was undo"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True
        return False

    def last_event_was_redo(self):
        """Returns True if the last event is believed to be the redo command."""

        string, mods = self._last_key_and_modifiers()
        if string.lower() == "z":
            rv = mods & 1 << Atspi.ModifierType.CONTROL and mods & 1 << Atspi.ModifierType.SHIFT
        elif string.lower() == "y":
            # LibreOffice
            rv = mods & 1 << Atspi.ModifierType.CONTROL and not mods & 1 << Atspi.ModifierType.SHIFT
        else:
            rv = False

        if rv:
            msg = "INPUT EVENT MANAGER: Last event was redo"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        return rv

    def last_event_was_select_all(self):
        """Returns True if the last event is believed to be the select all command."""

        string, mods = self._last_key_and_modifiers()
        if string.lower() != "a":
            return False

        if mods & 1 << Atspi.ModifierType.CONTROL and not mods & 1 << Atspi.ModifierType.SHIFT:
            msg = "INPUT EVENT MANAGER: Last event was select all"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True
        return False

    def last_event_was_primary_click(self):
        """Returns True if the last event is a primary mouse click."""

        if not self.last_event_was_mouse_button():
            return False

        if self._last_input_event.button == "1" and self._last_input_event.pressed:
            msg = "INPUT EVENT MANAGER: Last event was primary click"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True
        return False

    def last_event_was_primary_release(self):
        """Returns True if the last event is a primary mouse release."""

        if not self.last_event_was_mouse_button():
            return False

        if self._last_input_event.button == "1" and not self._last_input_event.pressed:
            msg = "INPUT EVENT MANAGER: Last event was primary release"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True
        return False

    def last_event_was_primary_click_or_release(self):
        """Returns True if the last event is a primary mouse click or release."""

        if not self.last_event_was_mouse_button():
            return False

        if self._last_input_event.button == "1":
            msg = "INPUT EVENT MANAGER: Last event was primary click or release"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True
        return False

    def last_event_was_middle_click(self):
        """Returns True if the last event is a middle mouse click."""

        if not self.last_event_was_mouse_button():
            return False

        if self._last_input_event.button == "2" and self._last_input_event.pressed:
            msg = "INPUT EVENT MANAGER: Last event was middle click"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True
        return False

    def last_event_was_middle_release(self):
        """Returns True if the last event is a middle mouse release."""

        if not self.last_event_was_mouse_button():
            return False

        if self._last_input_event.button == "2" and not self._last_input_event.pressed:
            msg = "INPUT EVENT MANAGER: Last event was middle release"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True
        return False

    def last_event_was_secondary_click(self):
        """Returns True if the last event is a secondary mouse click."""

        if not self.last_event_was_mouse_button():
            return False

        if self._last_input_event.button == "3" and self._last_input_event.pressed:
            msg = "INPUT EVENT MANAGER: Last event was secondary click"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True
        return False

    def last_event_was_secondary_release(self):
        """Returns True if the last event is a secondary mouse release."""

        if not self.last_event_was_mouse_button():
            return False

        if self._last_input_event.button == "3" and not self._last_input_event.pressed:
            msg = "INPUT EVENT MANAGER: Last event was secondary release"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True
        return False


_manager = InputEventManager()


def get_manager():
    """Returns the Input Event Manager singleton."""
    return _manager
