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

"""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")
from gi.repository import Atspi

import math
import time
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_state
from . import script_manager
from . import settings
from .ax_object import AXObject
from .ax_utilities import AXUtilities

KEYBOARD_EVENT     = "keyboard"
BRAILLE_EVENT      = "braille"
MOUSE_BUTTON_EVENT = "mouse:button"

class InputEvent:

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

        self.type = eventType
        self.time = time.time()
        self._clickCount = 0

    def getClickCount(self):
        """Return the count of the number of clicks a user has made."""

        return self._clickCount

    def setClickCount(self):
        """Updates the count of the number of clicks a user has made."""

        pass

def _getXkbStickyKeysState():
    from subprocess import check_output

    try:
        output = check_output(['xkbset', 'q'])
        for line in output.decode('ASCII', errors='ignore').split('\n'):
            if line.startswith('Sticky-Keys = '):
                return line.endswith('On')
    except Exception:
        pass
    return False

class KeyboardEvent(InputEvent):

    stickyKeys = _getXkbStickyKeysState()

    duplicateCount = 0
    cthulhuModifierPressed = False

    # Whether last press of the Cthulhu modifier was alone
    lastCthulhuModifierAlone = False
    lastCthulhuModifierAloneTime = None
    # Whether the current press of the Cthulhu modifier is alone
    currentCthulhuModifierAlone = False
    currentCthulhuModifierAloneTime = None
    # When the second cthulhu press happened
    secondCthulhuModifierTime = None
    # Sticky modifiers state, to be applied to the next keyboard event
    cthulhuStickyModifiers = 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, event):
        """Creates a new InputEvent of type KEYBOARD_EVENT.

        Arguments:
        - event: the AT-SPI keyboard event
        """

        super().__init__(KEYBOARD_EVENT)
        self.id = event.id
        self.type = event.type
        self.hw_code = event.hw_code
        self.modifiers = event.modifiers & Gdk.ModifierType.MODIFIER_MASK
        if event.modifiers & (1 << Atspi.ModifierType.NUMLOCK):
            self.modifiers |= (1 << Atspi.ModifierType.NUMLOCK)
        self.event_string = event.event_string
        self.keyval_name = Gdk.keyval_name(event.id)
        if self.event_string  == "":
            self.event_string = self.keyval_name
        self.timestamp = event.timestamp
        self.is_duplicate = self in [cthulhu_state.lastInputEvent,
                                     cthulhu_state.lastNonModifierKeyEvent]
        self._script = cthulhu_state.activeScript
        self._app = None
        self._window = cthulhu_state.activeWindow
        self._obj = cthulhu_state.locusOfFocus
        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 event.modifiers & (1 << Atspi.ModifierType.NUMLOCK):
                self._is_kp_with_numlock = True

        if self._script:
            self._app = self._script.app
            if not self._window:
                cthulhu.setActiveWindow(self._script.utilities.activeWindow())
                self._window = cthulhu_state.activeWindow
                tokens = ["INPUT EVENT: Updated window and active window to", self._window]
                debug.printTokens(debug.LEVEL_INFO, tokens, True)

        if self._window and self._app != AXObject.get_application(self._window):
            self._script = script_manager.getManager().getScript(
                AXObject.get_application(self._window))
            self._app = self._script.app
            tokens = ["INPUT EVENT: Updated script to", self._script]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)

        if self.is_duplicate:
            KeyboardEvent.duplicateCount += 1
        else:
            KeyboardEvent.duplicateCount = 0

        self.keyType = None

        _isPressed = event.type == Atspi.EventType.KEY_PRESSED_EVENT
        role = AXObject.get_role(self._obj)
        _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.isModifierKey():
            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 <
                        KeyboardEvent.lastCthulhuModifierAloneTime + 0.5):
                        # double-cthulhu, let the real action happen
                        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.isModifierKey():
            self.setClickCount()

        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.isModifierKey():
                # 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 setClickCount(self):
        """Updates the count of the number of clicks a user has made."""

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

        self._clickCount = doubleEvent.getClickCount()
        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.isPrintableKey():
            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.isPressedKey() or self.isPressedKey():
            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.isPressedKey() or self.isPressedKey():
            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 isModifierKey(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 isPrintableKey(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 isPressedKey(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.isPrintableKey():
            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 getObject(self):
        """Returns the object believed to be associated with this key event."""

        return self._obj

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

        return self._handler

    def _getUserHandler(self):
        # TODO - JD: This should go away once plugin support is in place.
        try:
            bindings = settings.keyBindingsMap.get(self._script.__module__)
        except Exception:
            bindings = None
        if not bindings:
            try:
                bindings = settings.keyBindingsMap.get("default")
            except Exception:
                bindings = None

        try:
            handler = bindings.getInputHandler(self)
        except Exception:
            handler = None

        return handler

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

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

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

        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'

        self._handler = self._getUserHandler() \
            or self._script.keyBindings.getInputHandler(self)

        # TODO - JD: Right now we need to always call consumesKeyboardEvent()
        # because that method is updating state, even in instances where there
        # is no handler.
        scriptConsumes = self._script.consumesKeyboardEvent(self)

        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.isModifierKey():
            if not self.isCthulhuModifier():
                return False, 'Non-Cthulhu modifier not in Learn Mode'
            return True, 'Cthulhu modifier'

        if not self._handler:
            return False, 'No handler'

        return scriptConsumes, 'Script indication'

    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 _present(self, inputEvent=None):
        if self.isPressedKey():
            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._lock_mod()
                    self.keyType = KeyboardEvent.TYPE_LOCKING
                    self._present()
            return False, 'Bypassed cthulhu modifier'

        cthulhu_state.lastInputEvent = self
        if not self.isModifierKey():
            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.isPressedKey():
            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.isModifierKey():
                cthulhu_state.bypassNextCommand = False
            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 False, 'No consumer or handler'

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

        return False, 'Unaddressed case'

    def _lock_mod(self):
        def lock_mod(modifiers, modifier):
            def lockit():
                try:
                    if modifiers & modifier:
                        lock = Atspi.KeySynthType.UNLOCKMODIFIERS
                        debug.printMessage(debug.LEVEL_INFO, "Unlocking capslock", True)
                    else:
                        lock = Atspi.KeySynthType.LOCKMODIFIERS
                        debug.printMessage(debug.LEVEL_INFO, "Locking capslock", True)
                    Atspi.generate_keyboard_event(modifier, "", lock)
                    debug.printMessage(debug.LEVEL_INFO, "Done with capslock", True)
                except Exception:
                    debug.printMessage(debug.LEVEL_INFO, "Could not trigger capslock, " \
                        "at-spi2-core >= 2.32 is needed for triggering capslock", True)
                    pass
            return lockit
        if self.event_string == "Caps_Lock":
            modifier = 1 << Atspi.ModifierType.SHIFTLOCK
        elif self.event_string == "Shift_Lock":
            modifier = 1 << Atspi.ModifierType.SHIFT
        else:
            tokens = ["Unknown locking key", self.event_string]
            debug.printTokens(debug.LEVEL_WARNING, tokens, True)
            return
        debug.printMessage(debug.LEVEL_INFO, "Scheduling capslock", True)
        GLib.timeout_add(1, lock_mod(self.modifiers, modifier))

    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 setClickCount(self):
        """Updates the count of the number of clicks a user has made."""

        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 InputEventHandler:

    def __init__(self, function, description, learnModeEnabled=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

    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
