# Orca
#
# Copyright 2024-2026 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-arguments
# pylint: disable=too-many-positional-arguments

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

from __future__ import annotations

import os
import sys
import time

import gi

import orca

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

_DOUBLE_CLICK_TIMEOUT: float = float(os.environ.get("ORCA_TEST_DOUBLE_CLICK_TIMEOUT") or 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._paused: bool = False
        self._previous_non_modifier_key_event: input_event.KeyboardEvent | None = None
        self._previous_braille_event: input_event.BrailleEvent | None = None

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

        ax_device_manager.get_manager().start_key_watcher(
            self._on_key_pressed,
            self._on_key_released,
            self.process_keyboard_event,
        )

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

        ax_device_manager.get_manager().stop_key_watcher()

    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 process_braille_event(self, event: Atspi.Event) -> bool:
        """Processes this Braille event."""

        braille_event = input_event.BrailleEvent(event)
        result = braille_event.process()
        if isinstance(self._last_input_event, input_event.BrailleEvent):
            self._previous_braille_event = self._last_input_event
        else:
            self._previous_braille_event = None
        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."""

        if isinstance(event, input_event.RemoteControllerTestingEvent):
            return

        # 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, clear_cache=False
            ) and 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._previous_non_modifier_key_event = self._last_non_modifier_key_event
            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, within: float | None = None) -> bool:
        """Returns True if the last event was a keyboard event, optionally within recent seconds."""

        if not isinstance(self._last_input_event, input_event.KeyboardEvent):
            return False
        if within is None:
            return True
        return time.time() - self._last_input_event.time <= within

    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, ignore_modifiers=False):
        """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() and not ignore_modifiers:
            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

        # The release of a modified key has different modifiers than the press if the user
        # releases the modifier first; match by key identity in that case.
        return self.is_release_for(self._last_non_modifier_key_event, event, ignore_modifiers=True)

    def previous_event_equals_or_is_release_for_event(self, event):
        """Returns True if the previous event equals the provided event, or is the release."""

        if isinstance(event, input_event.BrailleEvent):
            return self._previous_braille_event is event

        if self._previous_non_modifier_key_event is None:
            return False

        if self._previous_non_modifier_key_event is event:
            return True

        if event == self._previous_non_modifier_key_event:
            return True

        # The release of a modified key has different modifiers than the press if the user
        # releases the modifier first; match by key identity in that case.
        return self.is_release_for(
            self._previous_non_modifier_key_event,
            event,
            ignore_modifiers=True,
        )

    def get_last_input_event(self) -> input_event.InputEvent | None:
        """Returns the most recently processed input event (of any type), or None."""

        self._ensure_allowed_call("get_last_input_event")

        return self._last_input_event

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

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

        if orca.is_orca(filename):
            return

        msg = f"INPUT EVENT MANAGER: Refusing {method_name} call from outside Orca: {filename}"
        debug.print_message(debug.LEVEL_WARNING, msg, True)
        raise PermissionError(msg)

    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_not_in_current_object(self, within: float | None = None):
        """Returns True if the last event occurred in an object other than the one with focus."""

        if not self.last_event_was_keyboard(within):
            return False

        assert isinstance(self._last_input_event, input_event.KeyboardEvent)
        obj = self._last_input_event.get_object()
        if obj is None:
            return False

        rv = obj != focus_manager.get_manager().get_locus_of_focus()
        if rv:
            tokens = ["INPUT EVENT MANAGER: Last event was in", obj, "which does not have focus"]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
        return rv

    def last_event_was_ctrl_tab(self):
        """Returns True if the last event is Ctrl+Tab."""

        string, mods = self._last_key_and_modifiers()
        rv = string == "Tab" and bool(mods & 1 << Atspi.ModifierType.CONTROL)
        if rv:
            msg = "INPUT EVENT MANAGER: Last event was Ctrl+Tab"
            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_escape(self):
        """Returns True if the last event is Escape."""

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

    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
