#!/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.
#
# Fork of Orca Screen Reader (GNOME)
# Original source: https://gitlab.gnome.org/GNOME/orca

"""The main module for the Cthulhu screen reader."""

__id__        = "$Id$"
__version__   = "$Revision$"
__date__      = "$Date$"
__copyright__ = "Copyright (c) 2004-2009 Sun Microsystems Inc." \
                "Copyright (c) 2010-2011 The Cthulhu Team" \
                "Copyright (c) 2012 Igalia, S.L."
__license__   = "LGPL"

import faulthandler
import gi
import importlib
import os
import re
import signal
import subprocess
import sys

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

try:
    from gi.repository.Gio import Settings
    a11yAppSettings = Settings(schema_id='org.gnome.desktop.a11y.applications')
except Exception:
    a11yAppSettings = None

from . import braille
from . import debug
from . import event_manager
from . import keybindings
from . import learn_mode_presenter
from . import logger
from . import messages
from . import notification_presenter
from . import cthulhu_state
from . import cthulhu_platform
from . import script_manager
from . import settings
from . import settings_manager
from . import speech
from . import sound
from . import mouse_review
from .ax_object import AXObject
from .ax_utilities import AXUtilities
from .input_event import BrailleEvent
from . import cmdnames
from . import plugin_system_manager  # This will now be your pluggy-based implementation
from .plugin_api_helper import APIHelper  # New import for the separated APIHelper
from . import guilabels
from . import acss
from . import text_attribute_names
from . import speechserver
from . import input_event
from . import pronunciation_dict
from . import cthulhu_gtkbuilder
from . import signal_manager
from . import dynamic_api_manager
from . import translation_manager
from . import resource_manager


_eventManager = event_manager.getManager()
_scriptManager = script_manager.getManager()
_settingsManager = settings_manager.getManager()
_logger = logger.getLogger()

def onEnabledChanged(gsetting, key):
    try:
        enabled = gsetting.get_boolean(key)
    except Exception:
        return

    if key == 'screen-reader-enabled' and not enabled:
        shutdown()

def getSettingsManager():
    return _settingsManager

def getLogger():
    return _logger

EXIT_CODE_HANG = 50

# The user-settings module (see loadUserSettings).
#
_userSettings = None

# A subset of the original Xmodmap info prior to our stomping on it.
# Right now, this is just for the user's chosen Cthulhu modifier(s).
#
_originalXmodmap = ""
_cthulhuModifiers = settings.DESKTOP_MODIFIER_KEYS + settings.LAPTOP_MODIFIER_KEYS
_capsLockCleared = False
_restoreCthulhuKeys = False

########################################################################
#                                                                      #
# METHODS TO HANDLE APPLICATION LIST AND FOCUSED OBJECTS               #
#                                                                      #
########################################################################

CARET_TRACKING = "caret-tracking"
FOCUS_TRACKING = "focus-tracking"
FLAT_REVIEW = "flat-review"
MOUSE_REVIEW = "mouse-review"
OBJECT_NAVIGATOR = "object-navigator"
SAY_ALL = "say-all"

def getActiveModeAndObjectOfInterest():
    tokens = ["CTHULHU: Active mode:", cthulhu_state.activeMode,
              "Object of interest:", cthulhu_state.objOfInterest]
    debug.printTokens(debug.LEVEL_INFO, tokens, True)
    return cthulhu_state.activeMode, cthulhu_state.objOfInterest

def emitRegionChanged(obj, startOffset=None, endOffset=None, mode=None):
    """Notifies interested clients that the current region of interest has changed."""

    if startOffset is None:
        startOffset = 0
    if endOffset is None:
        endOffset = startOffset
    if mode is None:
        mode = FOCUS_TRACKING

    try:
        obj.emit("mode-changed::" + mode, 1, "")
    except Exception:
        msg = "CTHULHU: Exception emitting mode-changed notification"
        debug.printMessage(debug.LEVEL_INFO, msg, True)

    if mode != cthulhu_state.activeMode:
        tokens = ["CTHULHU: Switching active mode from", cthulhu_state.activeMode, "to", mode]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)
        cthulhu_state.activeMode = mode

    try:
        tokens = ["CTHULHU: Region of interest:", obj, "(", startOffset, ")", endOffset]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)
        obj.emit("region-changed", startOffset, endOffset)
    except Exception:
        msg = "CTHULHU: Exception emitting region-changed notification"
        debug.printMessage(debug.LEVEL_INFO, msg, True)

    if obj != cthulhu_state.objOfInterest:
        tokens = ["CTHULHU: Switching object of interest from", cthulhu_state.objOfInterest, "to", obj]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)
        cthulhu_state.objOfInterest = obj

def setActiveWindow(frame, app=None, alsoSetLocusOfFocus=False, notifyScript=False):
    tokens = ["CTHULHU: Request to set active window to", frame]
    if app is not None:
        tokens.extend(["in", app])
    debug.printTokens(debug.LEVEL_INFO, tokens, True)

    if frame == cthulhu_state.activeWindow:
        msg = "CTHULHU: Setting activeWindow to existing activeWindow"
        debug.printMessage(debug.LEVEL_INFO, msg, True)
    elif frame is None:
        cthulhu_state.activeWindow = None
    else:
        real_app, real_frame = AXObject.find_real_app_and_window_for(frame, app)
        if real_frame != frame:
            tokens = ["CTHULHU: Correcting active window to", real_frame, "in", real_app]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            cthulhu_state.activeWindow = real_frame
        else:
            cthulhu_state.activeWindow = frame

    if alsoSetLocusOfFocus:
        setLocusOfFocus(None, cthulhu_state.activeWindow, notifyScript=notifyScript)

def setLocusOfFocus(event, obj, notifyScript=True, force=False):
    """Sets the locus of focus (i.e., the object with visual focus) and
    notifies the script of the change should the script wish to present
    the change to the user.

    Arguments:
    - event: if not None, the Event that caused this to happen
    - obj: the Accessible with the new locus of focus.
    - notifyScript: if True, propagate this event
    - force: if True, don't worry if this is the same object as the
      current locusOfFocus
    """

    if not force and obj == cthulhu_state.locusOfFocus:
        msg = "CTHULHU: Setting locusOfFocus to existing locusOfFocus"
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        return

    if event and (cthulhu_state.activeScript and not cthulhu_state.activeScript.app):
        app = AXObject.get_application(event.source)
        script = _scriptManager.getScript(app, event.source)
        _scriptManager.setActiveScript(script, "Setting locusOfFocus")

    oldFocus = cthulhu_state.locusOfFocus
    if AXObject.is_dead(oldFocus):
        oldFocus = None

    if obj is None:
        msg = "CTHULHU: New locusOfFocus is null (being cleared)"
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        cthulhu_state.locusOfFocus = None
        return

    if cthulhu_state.activeScript:
        tokens = ["CTHULHU: Active script is:", cthulhu_state.activeScript]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)
        if cthulhu_state.activeScript.utilities.isZombie(obj):
            tokens = ["ERROR: New locusOfFocus (", obj, ") is zombie. Not updating."]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            return
        if AXObject.is_dead(obj):
            tokens = ["ERROR: New locusOfFocus (", obj, ") is dead. Not updating."]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            return

    tokens = ["CTHULHU: Changing locusOfFocus from", oldFocus, "to", obj, ". Notify:", notifyScript]
    debug.printTokens(debug.LEVEL_INFO, tokens, True)
    cthulhu_state.locusOfFocus = obj

    if not notifyScript:
        return

    if not cthulhu_state.activeScript:
        msg = "CTHULHU: Cannot notify active script because there isn't one"
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        return

    cthulhu_state.activeScript.locusOfFocusChanged(event, oldFocus, cthulhu_state.locusOfFocus)

########################################################################
#                                                                      #
# METHODS FOR PRE-PROCESSING AND MASSAGING BRAILLE EVENTS.             #
#                                                                      #
########################################################################

def _processBrailleEvent(event):
    """Called whenever a  key is pressed on the Braille display.

    Arguments:
    - command: the BrlAPI event for the key that was pressed.

    Returns True if the event was consumed; otherwise False
    """

    consumed = False

    # Braille key presses always interrupt speech.
    #
    event = BrailleEvent(event)
    if event.event['command'] not in braille.dontInteruptSpeechKeys:
        speech.stop()
    cthulhu_state.lastInputEvent = event

    try:
        consumed = _eventManager.processBrailleEvent(event)
    except Exception:
        debug.printException(debug.LEVEL_SEVERE)

    # TODO - JD: Is this still possible?
    if not consumed and learn_mode_presenter.getPresenter().is_active():
        consumed = True

    return consumed

########################################################################
#                                                                      #
# METHODS FOR HANDLING INITIALIZATION, SHUTDOWN, AND USE.              #
#                                                                      #
########################################################################

def deviceChangeHandler(deviceManager, device):
    """New keyboards being plugged in stomp on our changes to the keymappings,
       so we have to re-apply"""
    source = device.get_source()
    if source == Gdk.InputSource.KEYBOARD:
        msg = "CTHULHU: Keyboard change detected, re-creating the xmodmap"
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        _createCthulhuXmodmap()

def updateKeyMap(keyboardEvent):
    """Unsupported convenience method to call sad hacks which should go away."""

    global _restoreCthulhuKeys
    if keyboardEvent.isPressedKey():
        return

    if keyboardEvent.event_string in settings.cthulhuModifierKeys \
       and cthulhu_state.bypassNextCommand:
        _restoreXmodmap()
        _restoreCthulhuKeys = True
        return

    if _restoreCthulhuKeys and not cthulhu_state.bypassNextCommand:
        _createCthulhuXmodmap()
        _restoreCthulhuKeys = False

def _setXmodmap(xkbmap):
    """Set the keyboard map using xkbcomp."""
    p = subprocess.Popen(['xkbcomp', '-w0', '-', os.environ['DISPLAY']],
        stdin=subprocess.PIPE, stdout=None, stderr=None)
    p.communicate(xkbmap)

def _setCapsLockAsCthulhuModifier(enable):
    """Enable or disable use of the caps lock key as an Cthulhu modifier key."""
    interpretCapsLineProg = re.compile(
        r'^\s*interpret\s+Caps[_+]Lock[_+]AnyOfOrNone\s*\(all\)\s*{\s*$', re.I)
    normalCapsLineProg = re.compile(
        r'^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Lock\s*\)\s*;\s*$', re.I)
    interpretShiftLineProg = re.compile(
        r'^\s*interpret\s+Shift[_+]Lock[_+]AnyOf\s*\(\s*Shift\s*\+\s*Lock\s*\)\s*{\s*$', re.I)
    normalShiftLineProg = re.compile(
        r'^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Shift\s*\)\s*;\s*$', re.I)
    disabledModLineProg = re.compile(
        r'^\s*action\s*=\s*NoAction\s*\(\s*\)\s*;\s*$', re.I)
    normalCapsLine = '        action= LockMods(modifiers=Lock);'
    normalShiftLine = '        action= LockMods(modifiers=Shift);'
    disabledModLine = '        action= NoAction();'
    lines = _originalXmodmap.decode('UTF-8').split('\n')
    foundCapsInterpretSection = False
    foundShiftInterpretSection = False
    modified = False
    for i, line in enumerate(lines):
        if not foundCapsInterpretSection and not foundShiftInterpretSection:
            if interpretCapsLineProg.match(line):
                foundCapsInterpretSection = True
            elif interpretShiftLineProg.match(line):
                foundShiftInterpretSection = True
        elif foundCapsInterpretSection:
            if enable:
                if normalCapsLineProg.match(line):
                    lines[i] = disabledModLine
                    modified = True
            else:
                if disabledModLineProg.match(line):
                    lines[i] = normalCapsLine
                    modified = True
            if line.find('}'):
                foundCapsInterpretSection = False
        else: # foundShiftInterpretSection
            if enable:
                if normalShiftLineProg.match(line):
                    lines[i] = disabledModLine
                    modified = True
            else:
                if disabledModLineProg.match(line):
                    lines[i] = normalShiftLine
                    modified = True
            if line.find('}'):
                foundShiftInterpretSection = False
    if modified:
        _setXmodmap(bytes('\n'.join(lines), 'UTF-8'))

def _createCthulhuXmodmap():
    """Makes an Cthulhu-specific Xmodmap so that the keys behave as we
    need them to do. This is especially the case for the Cthulhu modifier.
    """

    global _capsLockCleared

    if "Caps_Lock" in settings.cthulhuModifierKeys \
       or "Shift_Lock" in settings.cthulhuModifierKeys:
        _setCapsLockAsCthulhuModifier(True)
        _capsLockCleared = True
    elif _capsLockCleared:
        _setCapsLockAsCthulhuModifier(False)
        _capsLockCleared = False

def _storeXmodmap(keyList):
    """Save the original xmodmap for the keys in keyList before we alter it.

    Arguments:
    - keyList: A list of named keys to look for.
    """

    global _originalXmodmap
    _originalXmodmap = subprocess.check_output(['xkbcomp', os.environ['DISPLAY'], '-'])

def _restoreXmodmap(keyList=[]):
    """Restore the original xmodmap values for the keys in keyList.

    Arguments:
    - keyList: A list of named keys to look for. An empty list means
      to restore the entire saved xmodmap.
    """

    msg = "CTHULHU: Attempting to restore original xmodmap"
    debug.printMessage(debug.LEVEL_INFO, msg, True)

    global _capsLockCleared
    _capsLockCleared = False
    p = subprocess.Popen(['xkbcomp', '-w0', '-', os.environ['DISPLAY']],
        stdin=subprocess.PIPE, stdout=None, stderr=None)
    p.communicate(_originalXmodmap)

    msg = "CTHULHU: Original xmodmap restored"
    debug.printMessage(debug.LEVEL_INFO, msg, True)

def setKeyHandling(new):
    """Toggle use of the new vs. legacy key handling mode.
    """
    _eventManager.setKeyHandling(new)

def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False):
    """Loads (and reloads) the user settings module, reinitializing
    things such as speech if necessary.

    Returns True to indicate the input event has been consumed.
    """

    debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Loading User Settings', True)

    global _userSettings

    # Shutdown the output drivers and give them a chance to die.

    player = sound.getPlayer()
    player.shutdown()
    speech.shutdown()
    braille.shutdown()

    _scriptManager.deactivate()
    cthulhuApp.getSignalManager().emitSignal('load-setting-begin')

    reloaded = False
    if _userSettings:
        _profile = _settingsManager.getSetting('activeProfile')[1]
        try:
            _userSettings = _settingsManager.getGeneralSettings(_profile)
            _settingsManager.setProfile(_profile)
            reloaded = True
        except ImportError:
            debug.printException(debug.LEVEL_INFO)
        except Exception:
            debug.printException(debug.LEVEL_SEVERE)
    else:
        _profile = _settingsManager.profile
        try:
            _userSettings = _settingsManager.getGeneralSettings(_profile)
        except ImportError:
            debug.printException(debug.LEVEL_INFO)
        except Exception:
            debug.printException(debug.LEVEL_SEVERE)

    if not script:
        script = _scriptManager.getDefaultScript()

    _settingsManager.loadAppSettings(script)

    if _settingsManager.getSetting('enableSpeech'):
        msg = 'CTHULHU: About to enable speech'
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        try:
            speech.init()
            if reloaded and not skipReloadMessage:
                script.speakMessage(messages.SETTINGS_RELOADED)
        except Exception:
            debug.printException(debug.LEVEL_SEVERE)
    else:
        msg = 'CTHULHU: Speech is not enabled in settings'
        debug.printMessage(debug.LEVEL_INFO, msg, True)

    if _settingsManager.getSetting('enableBraille'):
        msg = 'CTHULHU: About to enable braille'
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        try:
            braille.init(_processBrailleEvent)
        except Exception:
            debug.printException(debug.LEVEL_WARNING)
            msg = 'CTHULHU: Could not initialize connection to braille.'
            debug.printMessage(debug.LEVEL_WARNING, msg, True)
    else:
        msg = 'CTHULHU: Braille is not enabled in settings'
        debug.printMessage(debug.LEVEL_INFO, msg, True)


    if _settingsManager.getSetting('enableMouseReview'):
        mouse_review.getReviewer().activate()
    else:
        mouse_review.getReviewer().deactivate()

    if _settingsManager.getSetting('enableSound'):
        player.init()

    global _cthulhuModifiers
    custom = [k for k in settings.cthulhuModifierKeys if k not in _cthulhuModifiers]
    _cthulhuModifiers += custom
    # Handle the case where a change was made in the Cthulhu Preferences dialog.
    #
    if _originalXmodmap:
        _restoreXmodmap(_cthulhuModifiers)

    _storeXmodmap(_cthulhuModifiers)
    _createCthulhuXmodmap()

    activePlugins = list(_settingsManager.getSetting('activePlugins'))
    cthulhuApp.getPluginSystemManager().setActivePlugins(activePlugins)

    _scriptManager.activate()
    _eventManager.activate()

    cthulhuApp.getSignalManager().emitSignal('load-setting-completed')

    debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: User Settings Loaded', True)

    return True

def _showPreferencesUI(script, prefs):
    if cthulhu_state.cthulhuOS:
        cthulhu_state.cthulhuOS.showGUI()
        return

    try:
        module = importlib.import_module('.cthulhu_gui_prefs', 'cthulhu')
    except Exception:
        debug.printException(debug.LEVEL_SEVERE)
        return

    uiFile = os.path.join(cthulhu_platform.datadir,
                          cthulhu_platform.package,
                          "ui",
                          "cthulhu-setup.ui")

    cthulhu_state.cthulhuOS = module.CthulhuSetupGUI(uiFile, "cthulhuSetupWindow", prefs)
    cthulhu_state.cthulhuOS.init(script)
    cthulhu_state.cthulhuOS.showGUI()

def showAppPreferencesGUI(script=None, inputEvent=None):
    """Displays the user interface to configure the settings for a
    specific applications within Cthulhu and set up those app-specific
    user preferences using a GUI.

    Returns True to indicate the input event has been consumed.
    """

    prefs = {}
    for key in settings.userCustomizableSettings:
        prefs[key] = _settingsManager.getSetting(key)

    script = script or cthulhu_state.activeScript
    _showPreferencesUI(script, prefs)

    return True

def showPreferencesGUI(script=None, inputEvent=None):
    """Displays the user interface to configure Cthulhu and set up
    user preferences using a GUI.

    Returns True to indicate the input event has been consumed.
    """

    prefs = _settingsManager.getGeneralSettings(_settingsManager.profile)
    script = _scriptManager.getDefaultScript()
    _showPreferencesUI(script, prefs)

    return True

def addKeyGrab(binding):
    """ Add a key grab for the given key binding."""

    if cthulhu_state.device is None:
        return []

    ret = []
    for kd in binding.keyDefs():
        ret.append(cthulhu_state.device.add_key_grab(kd, None))
    return ret

def removeKeyGrab(id):
    """ Remove the key grab for the given key binding."""

    if cthulhu_state.device is None:
        return

    cthulhu_state.device.remove_key_grab(id)

def mapModifier(keycode):
    if cthulhu_state.device is None:
        return

    return cthulhu_state.device.map_modifier(keycode)

def quitCthulhu(script=None, inputEvent=None):
    """Quit Cthulhu. Check if the user wants to confirm this action.
    If so, show the confirmation GUI otherwise just shutdown.

    Returns True to indicate the input event has been consumed.
    """

    shutdown()

    return True

def showFindGUI(script=None, inputEvent=None):
    """Displays the user interface to perform an Cthulhu Find.

    Returns True to indicate the input event has been consumed.
    """

    try:
        module = importlib.import_module('.cthulhu_gui_find', 'cthulhu')
        module.showFindUI()
    except Exception:
        debug.printException(debug.LEVEL_SEVERE)

# If True, this module has been initialized.
#
_initialized = False

def init():
    """Initialize the cthulhu module, which initializes the speech and braille
    modules.  Also builds up the application list, registers for AT-SPI events,
    and creates scripts for all known applications.

    Returns True if the initialization procedure has run, or False if this
    module has already been initialized.
    """

    debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Initializing', True)

    global _initialized

    if _initialized and _settingsManager.isScreenReaderServiceEnabled():
        debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Already initialized', True)
        return False

    # Do not hang on initialization if we can help it.
    #
    if settings.timeoutCallback and (settings.timeoutTime > 0):
        signal.signal(signal.SIGALRM, settings.timeoutCallback)
        signal.alarm(settings.timeoutTime)

    loadUserSettings()

    if settings.timeoutCallback and (settings.timeoutTime > 0):
        signal.alarm(0)

    _initialized = True
    # In theory, we can do this through dbus. In practice, it fails to
    # work sometimes. Until we know why, we need to leave this as-is
    # so that we respond when gnome-control-center is used to stop Cthulhu.
    if a11yAppSettings:
        a11yAppSettings.connect('changed', onEnabledChanged)

    debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Initialized', True)

    return True

def start():
    """Starts Cthulhu."""

    debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Starting', True)

    if not _initialized:
        init()

    # Do not hang on startup if we can help it.
    #
    if settings.timeoutCallback and (settings.timeoutTime > 0):
        signal.signal(signal.SIGALRM, settings.timeoutCallback)
        signal.alarm(settings.timeoutTime)

    if settings.timeoutCallback and (settings.timeoutTime > 0):
        signal.alarm(0)

    # Event handlers for input devices being plugged in/unplugged.
    # Used to re-create the Xmodmap when a new keyboard is plugged in.
    # Necessary, because plugging in a new keyboard resets the Xmodmap
    # and stomps our changes
    display = Gdk.Display.get_default()
    devmanager=display.get_device_manager()
    devmanager.connect("device-added", deviceChangeHandler)
    devmanager.connect("device-removed", deviceChangeHandler)

    Gdk.notify_startup_complete()
    msg = 'CTHULHU: Startup complete notification made'
    debug.printMessage(debug.LEVEL_INFO, msg, True)

    debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Starting Atspi main event loop', True)
    Atspi.event_main()

def die(exitCode=1):
    pid = os.getpid()
    if exitCode == EXIT_CODE_HANG:
        # Someting is hung and we wish to abort.
        os.kill(pid, signal.SIGKILL)
        return

    shutdown()
    sys.exit(exitCode)
    if exitCode > 1:
        os.kill(pid, signal.SIGTERM)

def timeout(signum=None, frame=None):
    msg = 'TIMEOUT: something has hung. Aborting.'
    debug.printMessage(debug.LEVEL_SEVERE, msg, True)
    debug.printStack(debug.LEVEL_SEVERE)
    debug.examineProcesses(force=True)
    die(EXIT_CODE_HANG)

def shutdown(script=None, inputEvent=None):
    """Exits Cthulhu.  Unregisters any event listeners and cleans up.

    Returns True if the shutdown procedure ran or False if this module
    was never initialized.
    """

    debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Shutting down', True)

    global _initialized

    if not _initialized:
        return False

    # Try to say goodbye, but be defensive if something has hung.
    #
    if settings.timeoutCallback and (settings.timeoutTime > 0):
        signal.signal(signal.SIGALRM, settings.timeoutCallback)
        signal.alarm(settings.timeoutTime)

    cthulhu_state.activeScript.presentationInterrupt()

    cthulhuApp.getSignalManager().emitSignal('stop-application-completed')
    cthulhuApp.getPluginSystemManager().unloadAllPlugins(ForceAllPlugins=True)

    # Deactivate the event manager first so that it clears its queue and will not
    # accept new events. Then let the script manager unregister script event listeners.
    _eventManager.deactivate()
    _scriptManager.deactivate()

    # Shutdown all the other support.
    #
    if settings.enableSpeech:
        speech.shutdown()
    if settings.enableBraille:
        braille.shutdown()
    if settings.enableSound:
        player = sound.getPlayer()
        player.shutdown()

    if settings.timeoutCallback and (settings.timeoutTime > 0):
        signal.alarm(0)

    _initialized = False
    _restoreXmodmap(_cthulhuModifiers)

    debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Quitting Atspi main event loop', True)
    Atspi.event_quit()
    debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Shutdown complete', True)

    return True

exitCount = 0
def shutdownOnSignal(signum, frame):
    global exitCount

    signalString = f'({signal.strsignal(signum)})'
    msg = f"CTHULHU: Shutting down and exiting due to signal={signum} {signalString}"
    debug.printMessage(debug.LEVEL_INFO, msg, True)

    # Well...we'll try to exit nicely, but if we keep getting called,
    # something bad is happening, so just quit.
    #
    if exitCount:
        die(signum)
    else:
        exitCount += 1

    # Try to do a graceful shutdown if we can.
    #
    if settings.timeoutCallback and (settings.timeoutTime > 0):
        signal.signal(signal.SIGALRM, settings.timeoutCallback)
        signal.alarm(settings.timeoutTime)

    try:
        if _initialized:
            shutdown()
        else:
            # We always want to try to shutdown speech since the
            # speech servers are very persistent about living.
            #
            speech.shutdown()
            shutdown()
        cleanExit = True
    except Exception:
        cleanExit = False

    if settings.timeoutCallback and (settings.timeoutTime > 0):
        signal.alarm(0)

    if not cleanExit:
        die(EXIT_CODE_HANG)

def crashOnSignal(signum, frame):
    signalString = f'({signal.strsignal(signum)})'
    msg = f"CTHULHU: Shutting down and exiting due to signal={signum} {signalString}"
    debug.printMessage(debug.LEVEL_SEVERE, msg, True)
    debug.printStack(debug.LEVEL_SEVERE)
    _restoreXmodmap(_cthulhuModifiers)
    sys.exit(1)

def main():
    """The main entry point for Cthulhu.  The exit codes for Cthulhu will
    loosely be based on signals, where the exit code will be the
    signal used to terminate Cthulhu (if a signal was used).  Otherwise,
    an exit code of 0 means normal completion and an exit code of 50
    means Cthulhu exited because of a hang."""

    msg = f"CTHULHU: Launching version {cthulhu_platform.version}"
    if cthulhu_platform.revision:
        msg += f" (rev {cthulhu_platform.revision})"

    sessionType = os.environ.get('XDG_SESSION_TYPE') or ""
    sessionDesktop = os.environ.get('XDG_SESSION_DESKTOP') or ""
    session = "%s %s".strip() % (sessionType, sessionDesktop)
    if session:
        msg += f" session: {session}"
    debug.printMessage(debug.LEVEL_INFO, msg, True)

    if debug.debugFile and os.path.exists(debug.debugFile.name):
        faulthandler.enable(file=debug.debugFile, all_threads=True)
    else:
        faulthandler.enable(all_threads=False)

    # Method to call when we think something might be hung.
    #
    settings.timeoutCallback = timeout

    # Various signal handlers we want to listen for.
    #
    signal.signal(signal.SIGHUP, shutdownOnSignal)
    signal.signal(signal.SIGINT, shutdownOnSignal)
    signal.signal(signal.SIGTERM, shutdownOnSignal)
    signal.signal(signal.SIGQUIT, shutdownOnSignal)
    signal.signal(signal.SIGSEGV, crashOnSignal)

    debug.printMessage(debug.LEVEL_INFO, "CTHULHU: Enabling accessibility (if needed).", True)
    if not _settingsManager.isAccessibilityEnabled():
        _settingsManager.setAccessibility(True)

    debug.printMessage(debug.LEVEL_INFO, "CTHULHU: Initializing.", True)
    init()
    debug.printMessage(debug.LEVEL_INFO, "CTHULHU: Initialized.", True)

    script = cthulhu_state.activeScript
    cthulhuApp.getSignalManager().emitSignal('start-application-completed')
    if script:
        window = script.utilities.activeWindow()

        if window and not cthulhu_state.locusOfFocus:
            app = AXObject.get_application(window)
            setActiveWindow(window, app, alsoSetLocusOfFocus=True, notifyScript=True)

            # setActiveWindow does some corrective work needed thanks to
            # mutter-x11-frames. So retrieve the window just in case.
            window = cthulhu_state.activeWindow
            script = _scriptManager.getScript(app, window)
            _scriptManager.setActiveScript(script, "Launching.")

            focusedObject = AXUtilities.get_focused_object(window)
            tokens = ["CTHULHU: Focused object is:", focusedObject]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            if focusedObject:
                setLocusOfFocus(None, focusedObject)
                script = _scriptManager.getScript(
                    AXObject.get_application(focusedObject), focusedObject)
                _scriptManager.setActiveScript(script, "Found focused object.")

    try:
        msg = "CTHULHU: Starting ATSPI registry."
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        start() # waits until we stop the registry
    except Exception:
        msg = "CTHULHU: Exception starting ATSPI registry."
        debug.printMessage(debug.LEVEL_SEVERE, msg, True)
        die(EXIT_CODE_HANG)
    return 0

class Cthulhu(GObject.Object):
    # basic signals
    __gsignals__ = {
        "start-application-completed":  (GObject.SignalFlags.RUN_LAST, None, ()),
        "stop-application-completed":  (GObject.SignalFlags.RUN_LAST, None, ()),
        "load-setting-begin": (GObject.SignalFlags.RUN_LAST, None, ()),
        "load-setting-completed": (GObject.SignalFlags.RUN_LAST, None, ()),
        "setup-inputeventhandlers-completed": (GObject.SignalFlags.RUN_LAST, None, ()), # compat signal for register input event handlers
        "request-cthulhu-preferences": (GObject.SignalFlags.RUN_LAST, None, ()),
        "request-application-preferences": (GObject.SignalFlags.RUN_LAST, None, ()),
    }
    def __init__(self):
        GObject.Object.__init__(self)
        # add members
        self.resourceManager = resource_manager.ResourceManager(self)
        self.APIHelper = APIHelper(self)
        self.eventManager = _eventManager
        self.settingsManager = _settingsManager
        self.scriptManager = _scriptManager
        self.signalManager = signal_manager.SignalManager(self)
        self.dynamicApiManager = dynamic_api_manager.DynamicApiManager(self)
        self.translationManager = translation_manager.TranslationManager(self)
        self.debugManager = debug
        self.createCompatAPI()
        self.pluginSystemManager = plugin_system_manager.PluginSystemManager(self)
    def getAPIHelper(self):
        return self.APIHelper
    def getPluginSystemManager(self):
        return self.pluginSystemManager
    def getDynamicApiManager(self):
        return self.dynamicApiManager
    def getSignalManager(self):
        return self.signalManager
    def getEventManager(self):
        return self.eventManager
    def getSettingsManager(self):
        return self.settingsManager
    def getScriptManager(self):
        return self.scriptManager
    def getDebugManager(self):
        return self.debugManager
    def getTranslationManager(self):
        return self.translationManager
    def getResourceManager(self):
        return self.resourceManager
    def run(self, cacheValues=True):
        return main(cacheValues)
    def stop(self):
        pass
    def createCompatAPI(self):
        # for now add compatibility layer using Dynamic API
        # should be removed step by step
        # use clean objects, getters and setters instead

        self.getDynamicApiManager().registerAPI('Logger', _logger)
        self.getDynamicApiManager().registerAPI('SettingsManager', settings_manager)
        self.getDynamicApiManager().registerAPI('ScriptManager', script_manager)
        self.getDynamicApiManager().registerAPI('EventManager', event_manager)
        self.getDynamicApiManager().registerAPI('Speech', speech)
        self.getDynamicApiManager().registerAPI('Sound', sound)
        self.getDynamicApiManager().registerAPI('Braille', braille)
        self.getDynamicApiManager().registerAPI('Debug', debug)
        self.getDynamicApiManager().registerAPI('Messages', messages)
        self.getDynamicApiManager().registerAPI('Cmdnames', cmdnames)
        self.getDynamicApiManager().registerAPI('NotificationPresenter', notification_presenter)
        self.getDynamicApiManager().registerAPI('CthulhuState', cthulhu_state)
        self.getDynamicApiManager().registerAPI('CthulhuPlatform', cthulhu_platform)
        self.getDynamicApiManager().registerAPI('Settings', settings)
        self.getDynamicApiManager().registerAPI('Keybindings', keybindings)
        self.getDynamicApiManager().registerAPI('GuiLabels', guilabels)
        self.getDynamicApiManager().registerAPI('Acss', acss)
        self.getDynamicApiManager().registerAPI('TextAttributeNames', text_attribute_names)
        self.getDynamicApiManager().registerAPI('PronunciationDict', pronunciation_dict)
        self.getDynamicApiManager().registerAPI('InputEvent', input_event)
        self.getDynamicApiManager().registerAPI('SpeechServer', speechserver)
        self.getDynamicApiManager().registerAPI('CthulhuGtkbuilder', cthulhu_gtkbuilder)
        self.getDynamicApiManager().registerAPI('AXObject', AXObject)
        self.getDynamicApiManager().registerAPI('AXUtilities', AXUtilities)
        self.getDynamicApiManager().registerAPI('LearnModePresenter', learn_mode_presenter)
        # cthulhu lets say, special compat handling....
        self.getDynamicApiManager().registerAPI('EmitRegionChanged', emitRegionChanged)
        self.getDynamicApiManager().registerAPI('LoadUserSettings', loadUserSettings)

cthulhuApp = Cthulhu()

def getManager():
    return cthulhuApp

if __name__ == "__main__":
    GObject.threads_init()
    sys.exit(cthulhuApp.run())
