#!/usr/bin/env python3
#
# Copyright (c) 2024 Stormux
# Copyright (c) 2010-2012 The Orca Team
# Copyright (c) 2012 Igalia, S.L.
# Copyright (c) 2005-2010 Sun Microsystems Inc.
#
# 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.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu

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

__id__        = "$Id$"
__version__   = "$Revision$"
__date__      = "$Date$"
__copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \
                "Copyright (c) 2011-2016 Igalia, S.L."
__license__   = "LGPL"

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

import math
import time
from typing import Optional, Any
from gi.repository import Gdk
from gi.repository import GLib

from . import debug
from . import keybindings
from . import keynames
from . import messages
from . import cthulhu
from . import cthulhu_modifier_manager
from . import cthulhu_state
from . import script_manager
from . import settings
from .ax_object import AXObject
from .ax_utilities import AXUtilities

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

class InputEvent:

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

        self.type: str = eventType
        self.time: float = time.time()
        self._clickCount: int = 0

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

        return self._clickCount

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

        if count is None:
            return

        self._clickCount = count

class KeyboardEvent(InputEvent):
    stickyKeys: bool = False

    duplicateCount: int = 0
    cthulhuModifierPressed: bool = False

    # Whether last press of the Cthulhu modifier was alone
    lastCthulhuModifierAlone: bool = False
    lastCthulhuModifierAloneTime: Optional[float] = None
    # Whether the current press of the Cthulhu modifier is alone
    currentCthulhuModifierAlone: bool = False
    currentCthulhuModifierAloneTime: Optional[float] = None
    # When the second cthulhu press happened
    secondCthulhuModifierTime: Optional[float] = None
    # Sticky modifiers state, to be applied to the next keyboard event
    cthulhuStickyModifiers: int = 0

    TYPE_UNKNOWN          = "unknown"
    TYPE_PRINTABLE        = "printable"
    TYPE_MODIFIER         = "modifier"
    TYPE_LOCKING          = "locking"
    TYPE_FUNCTION         = "function"
    TYPE_ACTION           = "action"
    TYPE_NAVIGATION       = "navigation"
    TYPE_DIACRITICAL      = "diacritical"
    TYPE_ALPHABETIC       = "alphabetic"
    TYPE_NUMERIC          = "numeric"
    TYPE_PUNCTUATION      = "punctuation"
    TYPE_SPACE            = "space"

    GDK_PUNCTUATION_KEYS = [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_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]

    GDK_ACCENTED_LETTER_KEYS = [Gdk.KEY_Aacute,
                                Gdk.KEY_aacute,
                                Gdk.KEY_Acircumflex,
                                Gdk.KEY_acircumflex,
                                Gdk.KEY_Adiaeresis,
                                Gdk.KEY_adiaeresis,
                                Gdk.KEY_Agrave,
                                Gdk.KEY_agrave,
                                Gdk.KEY_Aring,
                                Gdk.KEY_aring,
                                Gdk.KEY_Atilde,
                                Gdk.KEY_atilde,
                                Gdk.KEY_Ccedilla,
                                Gdk.KEY_ccedilla,
                                Gdk.KEY_Eacute,
                                Gdk.KEY_eacute,
                                Gdk.KEY_Ecircumflex,
                                Gdk.KEY_ecircumflex,
                                Gdk.KEY_Ediaeresis,
                                Gdk.KEY_ediaeresis,
                                Gdk.KEY_Egrave,
                                Gdk.KEY_egrave,
                                Gdk.KEY_Iacute,
                                Gdk.KEY_iacute,
                                Gdk.KEY_Icircumflex,
                                Gdk.KEY_icircumflex,
                                Gdk.KEY_Idiaeresis,
                                Gdk.KEY_idiaeresis,
                                Gdk.KEY_Igrave,
                                Gdk.KEY_igrave,
                                Gdk.KEY_Ntilde,
                                Gdk.KEY_ntilde,
                                Gdk.KEY_Oacute,
                                Gdk.KEY_oacute,
                                Gdk.KEY_Ocircumflex,
                                Gdk.KEY_ocircumflex,
                                Gdk.KEY_Odiaeresis,
                                Gdk.KEY_odiaeresis,
                                Gdk.KEY_Ograve,
                                Gdk.KEY_ograve,
                                Gdk.KEY_Ooblique,
                                Gdk.KEY_ooblique,
                                Gdk.KEY_Otilde,
                                Gdk.KEY_otilde,
                                Gdk.KEY_Uacute,
                                Gdk.KEY_uacute,
                                Gdk.KEY_Ucircumflex,
                                Gdk.KEY_ucircumflex,
                                Gdk.KEY_Udiaeresis,
                                Gdk.KEY_udiaeresis,
                                Gdk.KEY_Ugrave,
                                Gdk.KEY_ugrave,
                                Gdk.KEY_Yacute,
                                Gdk.KEY_yacute]

    def __init__(self, pressed, keycode, keysym, modifiers, text):
        """Creates a new InputEvent of type KEYBOARD_EVENT.

        Arguments:
        - pressed: True if key is pressed, False if released
        - keycode: hardware keycode
        - keysym: keysym value
        - modifiers: modifier mask
        - text: text representation of the key
        """

        super().__init__(KEYBOARD_EVENT)
        self.id = keysym
        self.type = Atspi.EventType.KEY_PRESSED_EVENT if pressed else Atspi.EventType.KEY_RELEASED_EVENT
        self.hw_code = keycode
        self.modifiers = modifiers & Gdk.ModifierType.MODIFIER_MASK
        if modifiers & (1 << Atspi.ModifierType.NUMLOCK):
            self.modifiers |= (1 << Atspi.ModifierType.NUMLOCK)
        self.event_string = text
        self.keyval_name = Gdk.keyval_name(keysym)
        if self.event_string == "":
            self.event_string = self.keyval_name
        self.timestamp = time.time() * 1000  # Convert to milliseconds
        self.is_duplicate = False  # Will be set by InputEventManager
        self._script = None
        self._app = None
        self._window = None
        self._obj = None
        self._handler = None
        self._consumer = None
        self._should_consume = None
        self._consume_reason = None
        self._did_consume = None
        self._result_reason = None
        self._bypassCthulhu = None
        self._is_kp_with_numlock = False

        # Some implementors don't populate this field at all. More often than not,
        # the event_string and the keyval_name coincide for input events.
        if not self.event_string:
            self.event_string = self.keyval_name

        # Some implementors do populate the field, but with the keyname rather than
        # the printable character. This messes us up with punctuation and other symbols.
        if len(self.event_string) > 1 \
           and (self.id in KeyboardEvent.GDK_PUNCTUATION_KEYS or \
                self.id in KeyboardEvent.GDK_ACCENTED_LETTER_KEYS):
            self.event_string = chr(self.id)

        # 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.keyval_name.startswith("KP"):
            if modifiers & (1 << Atspi.ModifierType.NUMLOCK):
                self._is_kp_with_numlock = True

        self.keyType = None
        self.shouldEcho = False
        
        # InputEventManager will call _finalize_initialization after setting
        # script/object/window to ensure shouldConsume uses correct context.

    def _finalize_initialization(self):
        """Finalize initialization after object creation.
        This is separated to allow InputEventManager to set additional properties first."""
        
        if self.is_duplicate:
            KeyboardEvent.duplicateCount += 1
        else:
            KeyboardEvent.duplicateCount = 0

        _isPressed = self.type == Atspi.EventType.KEY_PRESSED_EVENT
        role = AXObject.get_role(self._obj) if self._obj else None
        _mayEcho = _isPressed or role == Atspi.Role.TERMINAL

        if KeyboardEvent.stickyKeys and not self.isCthulhuModifier() \
           and not KeyboardEvent.lastCthulhuModifierAlone:
            doubleEvent = self._getDoubleClickCandidate()
            if doubleEvent and \
               doubleEvent.modifiers & keybindings.CTHULHU_MODIFIER_MASK:
                # this is the second event of a double-click, and sticky Cthulhu
                # affected the first, so copy over the modifiers to the second
                KeyboardEvent.cthulhuStickyModifiers = doubleEvent.modifiers

        if not self.isCthulhuModifier():
            if KeyboardEvent.cthulhuModifierPressed:
                KeyboardEvent.currentCthulhuModifierAlone = False
                KeyboardEvent.currentCthulhuModifierAloneTime = None
            else:
                KeyboardEvent.lastCthulhuModifierAlone = False
                KeyboardEvent.lastCthulhuModifierAloneTime = None

        if self.isNavigationKey():
            self.keyType = KeyboardEvent.TYPE_NAVIGATION
            self.shouldEcho = _mayEcho and settings.enableNavigationKeys
        elif self.isActionKey():
            self.keyType = KeyboardEvent.TYPE_ACTION
            self.shouldEcho = _mayEcho and settings.enableActionKeys
        elif self.is_modifier_key():
            self.keyType = KeyboardEvent.TYPE_MODIFIER
            self.shouldEcho = _mayEcho and settings.enableModifierKeys
            if self.isCthulhuModifier() and not self.is_duplicate:
                now = time.time()
                if KeyboardEvent.lastCthulhuModifierAlone:
                    if _isPressed:
                        KeyboardEvent.secondCthulhuModifierTime = now
                    if (KeyboardEvent.secondCthulhuModifierTime is not None and
                        KeyboardEvent.lastCthulhuModifierAloneTime is not None and
                        KeyboardEvent.secondCthulhuModifierTime <
                        KeyboardEvent.lastCthulhuModifierAloneTime + 0.5):
                        # double-cthulhu, let the real action happen
                        if self.event_string in ["Caps_Lock", "Shift_Lock"]:
                            cthulhu_modifier_manager.getManager().toggleModifier(self)
                        else:
                            self._bypassCthulhu = True
                    if not _isPressed:
                        KeyboardEvent.lastCthulhuModifierAlone = False
                        KeyboardEvent.lastCthulhuModifierAloneTime = False
                else:
                    KeyboardEvent.cthulhuModifierPressed = _isPressed
                    if _isPressed:
                        KeyboardEvent.currentCthulhuModifierAlone = True
                        KeyboardEvent.currentCthulhuModifierAloneTime = now
                    else:
                        KeyboardEvent.lastCthulhuModifierAlone = \
                            KeyboardEvent.currentCthulhuModifierAlone
                        KeyboardEvent.lastCthulhuModifierAloneTime = \
                            KeyboardEvent.currentCthulhuModifierAloneTime
        elif self.isFunctionKey():
            self.keyType = KeyboardEvent.TYPE_FUNCTION
            self.shouldEcho = _mayEcho and settings.enableFunctionKeys
        elif self.isDiacriticalKey():
            self.keyType = KeyboardEvent.TYPE_DIACRITICAL
            self.shouldEcho = _mayEcho and settings.enableDiacriticalKeys
        elif self.isLockingKey():
            self.keyType = KeyboardEvent.TYPE_LOCKING
            self.shouldEcho = settings.presentLockingKeys
            if self.shouldEcho is None:
                self.shouldEcho = not settings.onlySpeakDisplayedText
            self.shouldEcho = self.shouldEcho and _isPressed
        elif self.isAlphabeticKey():
            self.keyType = KeyboardEvent.TYPE_ALPHABETIC
            self.shouldEcho = _mayEcho \
                and (settings.enableAlphabeticKeys or settings.enableEchoByCharacter)
        elif self.isNumericKey():
            self.keyType = KeyboardEvent.TYPE_NUMERIC
            self.shouldEcho = _mayEcho \
                and (settings.enableNumericKeys or settings.enableEchoByCharacter)
        elif self.isPunctuationKey():
            self.keyType = KeyboardEvent.TYPE_PUNCTUATION
            self.shouldEcho = _mayEcho \
                and (settings.enablePunctuationKeys or settings.enableEchoByCharacter)
        elif self.isSpace():
            self.keyType = KeyboardEvent.TYPE_SPACE
            self.shouldEcho = _mayEcho \
                and (settings.enableSpace or settings.enableEchoByCharacter)
        else:
            self.keyType = KeyboardEvent.TYPE_UNKNOWN
            self.shouldEcho = False

        if not self.isLockingKey():
            self.shouldEcho = self.shouldEcho and settings.enableKeyEcho

        if not self.is_modifier_key():
            self.set_click_count()

        if cthulhu_state.bypassNextCommand and _isPressed:
            KeyboardEvent.cthulhuModifierPressed = False

        if KeyboardEvent.cthulhuModifierPressed:
            self.modifiers |= keybindings.CTHULHU_MODIFIER_MASK

        if KeyboardEvent.stickyKeys:
            # apply all recorded sticky modifiers
            self.modifiers |= KeyboardEvent.cthulhuStickyModifiers
            if self.is_modifier_key():
                # add this modifier to the sticky ones
                KeyboardEvent.cthulhuStickyModifiers |= self.modifiers
            else:
                # Non-modifier key, so clear the sticky modifiers. If the user
                # actually double-presses that key, the modifiers of this event
                # will be copied over to the second event, see earlier in this
                # function.
                KeyboardEvent.cthulhuStickyModifiers = 0

        self._should_consume, self._consume_reason = self.shouldConsume()

    def _getDoubleClickCandidate(self):
        lastEvent = cthulhu_state.lastNonModifierKeyEvent
        if isinstance(lastEvent, KeyboardEvent) \
           and lastEvent.event_string == self.event_string \
           and self.time - lastEvent.time <= settings.doubleClickTimeout:
            return lastEvent
        return None

    def set_click_count(self, count=None):
        """Updates the count of the number of clicks a user has made.
        
        If count is provided, sets the click count to that value.
        Otherwise, calculates the click count based on event timing."""

        if count is not None:
            self._clickCount = count
            return

        doubleEvent = self._getDoubleClickCandidate()
        if not doubleEvent:
            self._clickCount = 1
            return

        self._clickCount = doubleEvent.get_click_count()
        if self.is_duplicate:
            return

        if self.type == Atspi.EventType.KEY_RELEASED_EVENT:
            return

        if self._clickCount < 3:
            self._clickCount += 1
            return

        self._clickCount = 1

    def __eq__(self, other):
        if not other:
            return False

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

        return False

    def __str__(self):
        if self._shouldObscure():
            keyid = hw_code = modifiers = event_string = keyval_name = key_type = "*"
        else:
            keyid = self.id
            hw_code = self.hw_code
            modifiers = self.modifiers
            event_string = self.event_string
            keyval_name = self.keyval_name
            key_type = self.keyType

        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"                 event_string=({event_string})\n" \
             + f"                 keyval_name=({keyval_name})\n" \
             + ("                 timestamp=%d\n" % self.timestamp) \
             + f"                 time={time.time():f}\n" \
             + f"                 keyType={key_type}\n" \
             + f"                 clickCount={self._clickCount}\n" \
             + f"                 shouldEcho={self.shouldEcho}\n"

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

        if not self.is_printable_key():
            return False

        if self.modifiers & keybindings.CTRL_MODIFIER_MASK \
           or self.modifiers & keybindings.ALT_MODIFIER_MASK \
           or self.modifiers & keybindings.CTHULHU_MODIFIER_MASK:
            return False

        return True

    def _isReleaseForLastNonModifierKeyEvent(self):
        last = cthulhu_state.lastNonModifierKeyEvent
        if not last:
            return False

        if not last.is_pressed_key() or self.is_pressed_key():
            return False

        if self.id == last.id and self.hw_code == last.hw_code:
            return self.modifiers == last.modifiers

        return False

    def isReleaseFor(self, other):
        """Return True if this is the release event for other."""

        if not other:
            return False

        if not other.is_pressed_key() or self.is_pressed_key():
            return False

        return self.id == other.id \
            and self.hw_code == other.hw_code \
            and self.modifiers == other.modifiers \
            and self.event_string == other.event_string \
            and self.keyval_name == other.keyval_name \
            and self.keyType == other.keyType \
            and self._clickCount == other._clickCount

    def isNavigationKey(self):
        """Return True if this is a navigation key."""

        if self.keyType:
            return self.keyType == KeyboardEvent.TYPE_NAVIGATION

        return self.event_string in \
            ["Left", "Right", "Up", "Down", "Home", "End"]

    def isActionKey(self):
        """Return True if this is an action key."""

        if self.keyType:
            return self.keyType == KeyboardEvent.TYPE_ACTION

        return self.event_string in \
            ["Return", "Escape", "Tab", "BackSpace", "Delete",
             "Page_Up", "Page_Down"]

    def isAlphabeticKey(self):
        """Return True if this is an alphabetic key."""

        if self.keyType:
            return self.keyType == KeyboardEvent.TYPE_ALPHABETIC

        if not len(self.event_string) == 1:
            return False

        return self.event_string.isalpha()

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

        if self.keyType:
            return self.keyType == KeyboardEvent.TYPE_DIACRITICAL

        return self.event_string.startswith("dead_")

    def isFunctionKey(self):
        """Return True if this is a function key."""

        if self.keyType:
            return self.keyType == KeyboardEvent.TYPE_FUNCTION

        return self.event_string in \
            ["F1", "F2", "F3", "F4", "F5", "F6",
             "F7", "F8", "F9", "F10", "F11", "F12"]

    def isLockingKey(self):
        """Return True if this is a locking key."""

        if self.keyType:
            return self.keyType in KeyboardEvent.TYPE_LOCKING

        lockingKeys = ["Caps_Lock", "Shift_Lock", "Num_Lock", "Scroll_Lock"]
        if self.event_string not in lockingKeys:
            return False

        if not cthulhu_state.bypassNextCommand and not self._bypassCthulhu:
            return self.event_string not in settings.cthulhuModifierKeys

        return True

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

        if self.keyType:
            return self.keyType == KeyboardEvent.TYPE_MODIFIER

        if self.isCthulhuModifier():
            return True

        return self.event_string in \
            ['Alt_L', 'Alt_R', 'Control_L', 'Control_R',
             'Shift_L', 'Shift_R', 'Meta_L', 'Meta_R',
             'ISO_Level3_Shift']

    def isNumericKey(self):
        """Return True if this is a numeric key."""

        if self.keyType:
            return self.keyType == KeyboardEvent.TYPE_NUMERIC

        if not len(self.event_string) == 1:
            return False

        return self.event_string.isnumeric()

    def isCthulhuModifier(self, checkBypassMode=True):
        """Return True if this is the Cthulhu modifier key."""

        if checkBypassMode and cthulhu_state.bypassNextCommand:
            return False

        if self.event_string in settings.cthulhuModifierKeys:
            return True

        if self.keyval_name == "KP_0" \
           and "KP_Insert" in settings.cthulhuModifierKeys \
           and self.modifiers & keybindings.SHIFT_MODIFIER_MASK:
            return True

        return False

    def isCthulhuModified(self):
        """Return True if this key is Cthulhu modified."""

        if cthulhu_state.bypassNextCommand:
            return False

        return self.modifiers & keybindings.CTHULHU_MODIFIER_MASK

    def isKeyPadKeyWithNumlockOn(self):
        """Return True if this is a key pad key with numlock on."""

        return self._is_kp_with_numlock

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

        if self.event_string in ["space", " "]:
            return True

        if not len(self.event_string) == 1:
            return False

        return self.event_string.isprintable()

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

        return self.type == Atspi.EventType.KEY_PRESSED_EVENT

    def isPunctuationKey(self):
        """Return True if this is a punctuation key."""

        if self.keyType:
            return self.keyType == KeyboardEvent.TYPE_PUNCTUATION

        if not len(self.event_string) == 1:
            return False

        if self.isAlphabeticKey() or self.isNumericKey():
            return False

        return self.event_string.isprintable() and not self.event_string.isspace()

    def isSpace(self):
        """Return True if this is the space key."""

        if self.keyType:
            return self.keyType == KeyboardEvent.TYPE_SPACE

        return self.event_string in ["space", " "]

    def isFromApplication(self, app):
        """Return True if this is associated with the specified app."""

        return self._app == app

    def isCharacterEchoable(self):
        """Returns True if the script will echo this event as part of
        character echo. We do this to not double-echo a given printable
        character."""

        if not self.is_printable_key():
            return False

        script = cthulhu_state.activeScript
        return script and script.utilities.willEchoCharacter(self)

    def getLockingState(self):
        """Returns True if the event locked a locking key, False if the
        event unlocked a locking key, and None if we do not know or this
        is not a locking key."""

        if not self.isLockingKey():
            return None

        if self.event_string == "Caps_Lock":
            mod = Atspi.ModifierType.SHIFTLOCK
        elif self.event_string == "Shift_Lock":
            mod = Atspi.ModifierType.SHIFT
        elif self.event_string == "Num_Lock":
            mod = Atspi.ModifierType.NUMLOCK
        else:
            return None

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

    def getLockingStateString(self):
        """Returns the string which reflects the locking state we wish to
        include when presenting a locking key."""

        locked = self.getLockingState()
        if locked is None:
            return ''

        if not locked:
            return messages.LOCKING_KEY_STATE_OFF

        return messages.LOCKING_KEY_STATE_ON

    def getKeyName(self):
        """Returns the string to be used for presenting the key to the user."""

        return keynames.getKeyName(self.event_string)

    def get_object(self):
        """Returns the object believed to be associated with this key event."""

        return self._obj
    
    def set_object(self, obj):
        """Sets the object believed to be associated with this key event."""
        
        self._obj = obj
    
    def get_window(self):
        """Returns the window associated with this key event."""
        
        return self._window
    
    def set_window(self, window):
        """Sets the window associated with this key event."""
        
        self._window = window
    
    def get_script(self):
        """Returns the script associated with this key event."""
        
        return self._script
    
    def set_script(self, script):
        """Sets the script associated with this key event."""
        
        self._script = script
        if script:
            self._app = script.app
    
    def get_click_count(self):
        """Returns the click count for this event."""
        
        return self._clickCount
    
    def as_single_line_string(self):
        """Returns a single-line string representation of this event."""
        
        return f"KeyboardEvent({self.keyval_name}, pressed={self.is_pressed_key()}, modifiers={self.modifiers})"

    def getHandler(self):
        """Returns the handler associated with this key event."""

        return self._handler

    def _resolveHandler(self):
        """Resolve handler for this event, returning True if a global handler was used."""

        if not self._handler and self._script:
            self._handler = self._script.keyBindings.getInputHandler(self)

        if not self._handler:
            globalHandler = self._getGlobalHandler()
            if globalHandler:
                self._handler = globalHandler
                return True

        return False

    def shouldConsume(self):
        """Returns True if this event should be consumed."""

        # Debug logging to understand handler matching
        debug.print_log(
            debug.LEVEL_INFO,
            "INPUT EVENT",
            f"shouldConsume: key='{self.event_string}' hw_code={self.hw_code} modifiers={self.modifiers}",
            timestamp=True,
        )

        if not self.timestamp:
            return False, 'No timestamp'

        globalHandlerUsed = False
        if not self._script:
            globalHandler = self._getGlobalHandler()
            if globalHandler:
                self._handler = globalHandler
                self._script = script_manager.get_manager().get_default_script()
                globalHandlerUsed = True
            else:
                debug.print_log(debug.LEVEL_INFO, "INPUT EVENT",
                                "shouldConsume: No active script",
                                reason="no-active-script", timestamp=True)
                return False, 'No active script when received'

        debug.print_log(debug.LEVEL_INFO, "INPUT EVENT",
                        f"shouldConsume: Active script={self._script.__class__.__name__}",
                        timestamp=True)

        if self.is_duplicate:
            return False, 'Is duplicate'

        if cthulhu_state.capturingKeys:
            return False, 'Capturing keys'

        if cthulhu_state.bypassNextCommand:
            return False, 'Bypass next command'

        globalHandlerUsed = globalHandlerUsed or self._resolveHandler()

        if self._handler:
            debug.print_log(debug.LEVEL_INFO, "INPUT EVENT",
                            f"shouldConsume: Handler found: {self._handler.description}",
                            timestamp=True)
        else:
            debug.print_log(debug.LEVEL_INFO, "INPUT EVENT",
                            "shouldConsume: No handler found",
                            reason="no-handler", timestamp=True)

        if self._isSleepModeActive():
            if self._isSleepModeToggleHandler():
                return True, 'Sleep mode toggle command'
            return False, 'Sleep mode active'

        self._script.updateKeyboardEventState(self, self._handler)
        scriptConsumes = self._script.shouldConsumeKeyboardEvent(self, self._handler)
        if globalHandlerUsed:
            scriptConsumes = True
        debug.print_log(debug.LEVEL_INFO, "INPUT EVENT",
                        f"shouldConsume: scriptConsumes={scriptConsumes}",
                        timestamp=True)

        if self._isReleaseForLastNonModifierKeyEvent():
            return scriptConsumes, 'Is release for last non-modifier keyevent'

        if self._script.learnModePresenter.is_active():
            self._consumer = self._script.learnModePresenter.handle_event
            return True, 'In Learn Mode'

        if self.is_modifier_key():
            if not self.isCthulhuModifier():
                return False, 'Non-Cthulhu modifier not in Learn Mode'
            return True, 'Cthulhu modifier'

        if not self._handler:
            if scriptConsumes:
                return True, 'Script consumed without handler'
            return False, 'No handler'

        return scriptConsumes, 'Script indication'

    def _getGlobalHandler(self):
        try:
            plugin_manager = cthulhu.cthulhuApp.getPluginSystemManager()
        except Exception:
            return None
        if not plugin_manager:
            return None
        global_bindings = plugin_manager.get_global_keybindings()
        if not global_bindings:
            return None
        return global_bindings.getInputHandler(self)

    def _isSleepModeActive(self):
        """Returns True if the script for this event is in sleep mode."""

        if not self._script:
            return False

        if "scripts.sleepmode" in self._script.__module__:
            return True

        app = getattr(self._script, "app", None)
        if app is None:
            return False

        try:
            from . import sleep_mode_manager
            manager = sleep_mode_manager.getManager()
            return bool(manager and manager.isActiveForApp(app))
        except Exception:
            return False

    def _isSleepModeToggleHandler(self):
        """Returns True if the resolved handler toggles sleep mode."""

        if not self._handler or not self._handler.function:
            return False

        functionName = getattr(self._handler.function, "__name__", "")
        return "toggleSleepMode" in functionName

    def didConsume(self):
        """Returns True if this event was consumed."""

        if self._did_consume is not None:
            return self._did_consume

        return False

    def isHandledBy(self, method):
        if not self._handler:
            return False

        return method.__func__ == self._handler.function

    def _should_interrupt_presentation_on_press(self):
        if not settings.gameMode:
            return True

        if cthulhu_state.bypassNextCommand and not self.is_modifier_key():
            return False

        if self._handler or self._consumer:
            return True

        if self.isCthulhuModifier():
            return True

        return False

    def _present(self, inputEvent=None):
        if self.is_pressed_key():
            if self._should_interrupt_presentation_on_press():
                self._script.presentationInterrupt()

        if self._script.learnModePresenter.is_active():
            return False

        return self._script.presentKeyboardEvent(self)

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

        startTime = time.time()
        if not self._shouldObscure():
            data = "'%s' (%d)" % (self.event_string, self.hw_code)
        else:
            data = "(obscured)"

        if self.is_duplicate:
            data = '%s DUPLICATE EVENT #%i' % (data, KeyboardEvent.duplicateCount)

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

        tokens = ["HOST_APP:", self._app]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)

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

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

        tokens = ["CONSUME:", self._should_consume, self._consume_reason]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)

        self._did_consume, self._result_reason = self._process()

        if self._should_consume != self._did_consume:
            tokens = ["CONSUMED:", self._did_consume, self._result_reason]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)

        if debug.LEVEL_INFO >= debug.debugLevel and cthulhu_state.activeScript:
            attributes = cthulhu_state.activeScript.getTransferableAttributes()
            for key, value in attributes.items():
                msg = f"INPUT EVENT: {key}: {value}"
                debug.printMessage(debug.LEVEL_INFO, msg, True)

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

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

        return self._did_consume

    def _process(self):
        """Processes this input event."""

        if self._bypassCthulhu:
            if (self.event_string == "Caps_Lock" \
                or self.event_string == "Shift_Lock") \
               and self.type == Atspi.EventType.KEY_PRESSED_EVENT:
                    self.keyType = KeyboardEvent.TYPE_LOCKING
                    self._present()
            return False, 'Bypassed cthulhu modifier'

        cthulhu_state.lastInputEvent = self
        if not self.is_modifier_key():
            cthulhu_state.lastNonModifierKeyEvent = self

        if not self._script:
            return False, 'No active script'

        if self.is_duplicate:
            return False, 'Is duplicate'

        self._present()

        if not self.is_pressed_key():
            return self._should_consume, 'Consumed based on handler'

        if cthulhu_state.capturingKeys:
            return False, 'Capturing keys'

        if self.isCthulhuModifier():
            return True, 'Cthulhu modifier'

        if cthulhu_state.bypassNextCommand:
            if not self.is_modifier_key():
                cthulhu_state.bypassNextCommand = False
                cthulhu_modifier_manager.getManager().refreshCthulhuModifiers("Bypass next command disabled")
            self._script.addKeyGrabs()
            return False, 'Bypass next command'

        if not self._should_consume:
            return False, 'Should not consume'

        if not (self._consumer or self._handler):
            return True, 'Consumed during shouldConsume'

        if self._consumer or self._handler.function:
            GLib.timeout_add(1, self._consume)
            return True, 'Will be consumed'

        return False, 'Unaddressed case'

    def _consume(self):
        startTime = time.time()
        data = "'%s' (%d)" % (self.event_string, self.hw_code)
        msg = f'vvvvv CONSUME {self.type.value_name.upper()}: {data} vvvvv'
        debug.printMessage(debug.LEVEL_INFO, msg, False)

        if self._consumer:
            msg = f'INFO: Consumer is {self._consumer.__name__}'
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            self._consumer(self)
        elif self._handler.function:
            msg = f'INFO: Handler is {self._handler.description}'
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            self._handler.function(self._script, self)
        else:
            msg = 'INFO: No handler or consumer'
            debug.printMessage(debug.LEVEL_INFO, msg, True)

        msg = f'TOTAL PROCESSING TIME: {time.time() - startTime:.4f}'
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        msg = f'^^^^^ CONSUME {self.type.value_name.upper()}: {data} ^^^^^'
        debug.printMessage(debug.LEVEL_INFO, msg, False)

        return False

class BrailleEvent(InputEvent):

    def __init__(self, event):
        """Creates a new InputEvent of type BRAILLE_EVENT.

        Arguments:
        - event: the integer BrlTTY command for this event.
        """
        super().__init__(BRAILLE_EVENT)
        self.event = event

class MouseButtonEvent(InputEvent):

    try:
        display = Gdk.Display.get_default()
        seat = Gdk.Display.get_default_seat(display)
        _pointer = seat.get_pointer()
    except Exception:
        _pointer = None

    def __init__(self, event):
        """Creates a new InputEvent of type MOUSE_BUTTON_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 = cthulhu_state.activeScript
        self.window = cthulhu_state.activeWindow
        self.obj = None

        if self.pressed:
            self._validateCoordinates()

        if not self._script:
            return

        if not self._script.utilities.canBeActiveWindow(self.window):
            self.window = self._script.utilities.activeWindow()

        if not self.window:
            return

        self.obj = self._script.utilities.descendantAtPoint(
            self.window, self.x, self.y, event.any_data)

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

        screen, x, y = self._pointer.get_position()
        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. "
            f"Updating to ({x}, {y})"
        )
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        self.x, self.y = x, y

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

        if count is not None:
            self._clickCount = count
            return

        if not self.pressed:
            return

        lastInputEvent = cthulhu_state.lastInputEvent
        if not isinstance(lastInputEvent, MouseButtonEvent):
            self._clickCount = 1
            return

        if self.time - lastInputEvent.time < settings.doubleClickTimeout \
            and lastInputEvent.button == self.button:
            if self._clickCount < 2:
                self._clickCount += 1
                return

        self._clickCount = 1


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

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


class InputEventHandler:

    def __init__(self, function, description, learnModeEnabled=True, enabled=True):
        """Creates a new InputEventHandler instance.  All bindings
        (e.g., key bindings and braille bindings) will be handled
        by an instance of an InputEventHandler.

        Arguments:
        - function: the function to call with an InputEvent instance as its
                    sole argument.  The function is expected to return True
                    if it consumes the event; otherwise it should return
                    False
        - description: a localized string describing what this InputEvent
                       does
        - learnModeEnabled: if True, the description will be spoken and
                            brailled if learn mode is enabled.  If False,
                            the function will be called no matter what.
        """

        self.function = function
        self.description = description
        self.learnModeEnabled = learnModeEnabled
        self._enabled = enabled

    def is_enabled(self):
        """Returns True if this handler is enabled."""
        return self._enabled

    def set_enabled(self, enabled):
        """Sets enabled state of this handler."""
        self._enabled = enabled

    def __eq__(self, other):
        """Compares one input handler to another."""

        if not other:
            return False

        return (self.function == other.function)

    def processInputEvent(self, script, inputEvent):
        """Processes an input event.

        This function is expected to return True if it consumes the
        event; otherwise it is expected to return False.

        Arguments:
        - script:     the script (if any) associated with this event
        - inputEvent: the input event to pass to the function bound
                      to this InputEventHandler instance.
        """

        consumed = False
        try:
            consumed = self.function(script, inputEvent)
        except Exception:
            debug.printException(debug.LEVEL_SEVERE)

        return consumed
