#!/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

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

from __future__ import annotations

__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
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union

from . import dbus_service

if TYPE_CHECKING:
    from types import FrameType
    from gi.repository import Gtk
    
    from .settings_manager import SettingsManager
    from .script_manager import ScriptManager
    from .plugin_system_manager import PluginSystemManager
    from .input_event import InputEvent
    from .input_event_manager import InputEventManager
    from .event_manager import EventManager
    from .compositor_state_adapter import CompositorStateAdapter
    from .signal_manager import SignalManager
    from .dynamic_api_manager import DynamicApiManager
    from .speech import Speech
    from .braille import Braille
    from .script import Script

class APIHelper:
    """Helper class for plugin API interactions, including keybindings."""

    def __init__(self, app: Cthulhu) -> None:
        """Initialize the APIHelper.

        Arguments:
        - app: the Cthulhu application
        """
        self.app: Cthulhu = app
        self._gestureBindings: Dict[Optional[str], List[Any]] = {}

    def registerGestureByString(
        self,
        function: Callable[..., Any],
        name: str,
        gestureString: str,
        inputEventType: str = 'default',
        normalizer: str = 'cthulhu',
        learnModeEnabled: bool = True,
        contextName: Optional[str] = None,
        globalBinding: bool = False
    ) -> Optional[Any]:
        """Register a gesture by string."""
        import logging
        logger = logging.getLogger(__name__)
        
        logger.info(f"=== APIHelper.registerGestureByString called ===")
        logger.info(f"gestureString: {gestureString}")
        logger.info(f"name: {name}")
        logger.info(f"contextName: {contextName}")
        
        if not gestureString.startswith("kb:"):
            logger.warning(f"Gesture string doesn't start with 'kb:': {gestureString}")
            return None

        # Extract the key portion from the gesture string
        key = gestureString.split(":", 1)[1]
        logger.info(f"Extracted key: {key}")

        # Handle Cthulhu modifier specially
        if "cthulhu+" in key.lower():
            from . import keybindings
            key_parts = key.lower().split("+")
            logger.info(f"Key parts: {key_parts}")

            # Start with the base Cthulhu modifier
            modifiers = keybindings.CTHULHU_MODIFIER_MASK

            # Extract the final key (without modifiers)
            final_key = key_parts[-1]
            logger.info(f"Final key: {final_key}")

            # Check for additional modifiers and combine them properly
            if "control" in key_parts and "shift" in key_parts:
                # Combine Cthulhu + Control + Shift modifiers
                modifiers = (keybindings.CTHULHU_MODIFIER_MASK | 
                           keybindings.CTRL_MODIFIER_MASK | 
                           keybindings.SHIFT_MODIFIER_MASK)
                logger.info(f"Using CTHULHU+CTRL+SHIFT combined mask: {modifiers}")
            elif "control" in key_parts:
                # Use the pre-defined Cthulhu+Control mask
                modifiers = keybindings.CTHULHU_CTRL_MODIFIER_MASK
                logger.info(f"Using CTHULHU_CTRL_MODIFIER_MASK: {modifiers}")
            elif "shift" in key_parts:
                # Use the pre-defined combined mask rather than trying to OR them
                modifiers = keybindings.CTHULHU_SHIFT_MODIFIER_MASK
                logger.info(f"Using CTHULHU_SHIFT_MODIFIER_MASK: {modifiers}")
            else:
                logger.info(f"Using CTHULHU_MODIFIER_MASK: {modifiers}")

            # Create a keybinding handler
            class GestureHandler:
                def __init__(self, function: Callable[..., Any], description: str, learnModeEnabled: bool = True) -> None:
                    self.function: Callable[..., Any] = function
                    self.description: str = description
                    self.learnModeEnabled: bool = learnModeEnabled

                def __call__(self, script: Any, inputEvent: Any) -> bool:
                    try:
                        logger.info(f"=== Plugin keybinding handler called! ===")
                        return function(script, inputEvent)
                    except Exception as e:
                        import logging
                        logging.getLogger(__name__).error(f"Error in keybinding handler: {e}")
                        return True

            handler = GestureHandler(function, name, learnModeEnabled)
            logger.info(f"Created handler: {handler}")

            # Create the binding object regardless of whether there's an active script
            # This allows plugins to define bindings that will work when a script becomes active
            from . import cthulhu_state
            from . import keybindings
            binding = keybindings.KeyBinding(
                final_key, 
                keybindings.defaultModifierMask,
                modifiers,
                handler)

            logger.info(f"Created binding: keysym={binding.keysymstring}, modifiers={binding.modifiers}, mask={binding.modifier_mask}")

            # Store binding for later reference
            if contextName not in self._gestureBindings:
                self._gestureBindings[contextName] = []
            self._gestureBindings[contextName].append(binding)
            logger.info(f"Stored binding in context '{contextName}'")

            plugin_manager = None
            if contextName and self.app:
                try:
                    plugin_manager = self.app.getPluginSystemManager()
                except Exception:
                    plugin_manager = None
                if plugin_manager:
                    plugin_manager.add_keybinding(contextName, binding, global_binding=globalBinding)

            if contextName and self.app and plugin_manager:
                if not globalBinding and cthulhu_state.activeScript:
                    plugin_manager.activate_keybindings_for_plugin(contextName)
                elif not globalBinding:
                    debug.printMessage(debug.LEVEL_INFO, "No active script available - binding stored for later registration", True)
            else:
                if cthulhu_state.activeScript and not globalBinding:
                    logger.info(f"Adding binding to active script: {cthulhu_state.activeScript}")
                    bindings = cthulhu_state.activeScript.getKeyBindings()
                    bindings.add(binding)
                    
                    # Register key grab at the system level
                    grab_ids = addKeyGrab(binding)
                    logger.info(f"Key grab IDs: {grab_ids}")
                    
                    # For later removal
                    if grab_ids:
                        binding._grab_ids = grab_ids
                else:
                    debug.printMessage(debug.LEVEL_INFO, "No active script available - binding stored for later registration", True)

            debug.printMessage(debug.LEVEL_INFO, f"Created binding: {binding.keysymstring} with modifiers {binding.modifiers}", True)
            logger.info("=== APIHelper.registerGestureByString completed ===")
            return binding
        else:
            logger.warning(f"Key doesn't contain 'cthulhu+': {key}")

        return None

    def unregisterShortcut(self, binding: Any, contextName: Optional[str] = None) -> None:
        """Unregister a previously registered shortcut.

        Arguments:
        - binding: the binding to unregister
        - contextName: the context for this gesture
        """
        removed_via_plugin_manager: bool = False
        if contextName and self.app:
            try:
                plugin_manager = self.app.getPluginSystemManager()
            except Exception:
                plugin_manager = None
            if plugin_manager:
                plugin_manager.remove_keybinding(contextName, binding)
                removed_via_plugin_manager = True
        if not removed_via_plugin_manager:
            # Remove from script's keybindings
            from . import cthulhu_state
            if cthulhu_state.activeScript:
                bindings = cthulhu_state.activeScript.getKeyBindings()
                bindings.remove(binding)

            # Remove key grab at system level
            if hasattr(binding, '_grab_ids'):
                for grab_id in binding._grab_ids:
                    self.app.removeKeyGrab(grab_id)

        # Remove from tracking
        if contextName in self._gestureBindings:
            if binding in self._gestureBindings[contextName]:
                self._gestureBindings[contextName].remove(binding)


import gi
import importlib
import os
import signal
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

from . import braille
from . import debug
from . import event_manager
from . import compositor_state_adapter
from . import keybindings
from . import learn_mode_presenter
from . import logger
from . import mako_notification_monitor
from . import messages
from . import notification_presenter
from . import focus_manager
from . import cthulhu_modifier_manager
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 sound_theme_manager
from . import mouse_review
from .ax_object import AXObject
from .ax_utilities import AXUtilities
# Lazy import to avoid circular dependency
# from .input_event import BrailleEvent
from . import cmdnames
from . import plugin_system_manager  # This will now be your pluggy-based implementation
from . import guilabels
from . import acss
from . import text_attribute_names
from . import speechserver
from . import input_event
from . import input_event_manager
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


# Old global variables removed - now using cthulhuApp.* instead

EXIT_CODE_HANG: int = 50

# The user-settings module (see loadUserSettings).
#
_userSettings: Optional[Any] = None

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

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

def getActiveModeAndObjectOfInterest() -> tuple[str, Any]:
    return focus_manager.get_manager().get_active_mode_and_object_of_interest()

def emitRegionChanged(obj: Any, startOffset: Optional[int] = None, endOffset: Optional[int] = None, mode: Optional[str] = None) -> None:
    """Notifies interested clients that the current region of interest has changed."""
    focus_manager.get_manager().emit_region_changed(obj, startOffset, endOffset, mode)

def setActiveWindow(frame: Any, app: Optional[Any] = None, alsoSetLocusOfFocus: bool = False, notifyScript: bool = False) -> None:
    real_app: Optional[Any] = app
    real_frame: Any = frame
    if frame is not None and hasattr(AXObject, "find_real_app_and_window_for"):
        real_app, real_frame = AXObject.find_real_app_and_window_for(frame, app)
    focus_manager.get_manager().set_active_window(
        real_frame, real_app, set_window_as_focus=alsoSetLocusOfFocus, notify_script=notifyScript)

def setLocusOfFocus(event: Optional[Any], obj: Any, notifyScript: bool = True, force: bool = False) -> None:
    """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
    """

    focus_manager.get_manager().set_locus_of_focus(
        event, obj, notify_script=notifyScript, force=force)

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

def _processBrailleEvent(event: Any) -> bool:
    """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: bool = False

    # Braille key presses always interrupt speech.
    #
    # Lazy import to avoid circular dependency
    from .input_event import BrailleEvent
    event = BrailleEvent(event)
    if event.event['command'] not in braille.dontInteruptSpeechKeys:
        speech.stop()
    cthulhu_state.lastInputEvent = event

    try:
        consumed = cthulhuApp.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: Any, device: Any) -> None:
    """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: str = "CTHULHU: Keyboard change detected, re-creating the xmodmap"
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        cthulhu_modifier_manager.getManager().refreshCthulhuModifiers("Keyboard change detected.")

def loadUserSettings(script: Optional[Any] = None, inputEvent: Optional[Any] = None, skipReloadMessage: bool = False) -> bool:
    """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()

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

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

    if not script:
        script = cthulhuApp.scriptManager.get_default_script()

    cthulhuApp.settingsManager.loadAppSettings(script)

    if cthulhuApp.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 cthulhuApp.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 cthulhuApp.settingsManager.getSetting('enableMouseReview'):
        mouse_review.getReviewer().activate()
    else:
        mouse_review.getReviewer().deactivate()

    if cthulhuApp.settingsManager.getSetting('enableSound'):
        player.init()

    cthulhu_modifier_manager.getManager().refreshCthulhuModifiers("Loading user settings.")

    # Activate core systems FIRST before loading plugins
    cthulhuApp.compositorStateAdapter.activate()
    cthulhuApp.scriptManager.activate()
    cthulhuApp.eventManager.activate()

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

    # NOW load plugins after script system is ready
    activePluginsSetting = cthulhuApp.settingsManager.getSetting('activePlugins') or []
    debug.printMessage(
        debug.LEVEL_INFO,
        f"CTHULHU: Active plugins defaults: {settings.activePlugins}",
        True,
    )
    debug.printMessage(
        debug.LEVEL_INFO,
        f"CTHULHU: Active plugins from settings manager: {activePluginsSetting}",
        True,
    )
    activePlugins = list(activePluginsSetting)
    debug.printMessage(debug.LEVEL_INFO, f'CTHULHU: Loading active plugins: {activePlugins}', True)
    cthulhuApp.getPluginSystemManager().setActivePlugins(activePlugins)

    # Refresh keybindings to include plugin bindings (after script manager is active)
    cthulhuApp.getPluginSystemManager().refresh_active_script_keybindings()

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

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

    return True

def _showPreferencesUI(script: Any, prefs: dict[str, Any]) -> None:
    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: str = 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: Optional[Any] = None, inputEvent: Optional[Any] = None) -> bool:
    """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: dict[str, Any] = {}
    for key in settings.userCustomizableSettings:
        prefs[key] = cthulhuApp.settingsManager.getSetting(key)

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

    return True

def showPreferencesGUI(script: Optional[Any] = None, inputEvent: Optional[Any] = None) -> bool:
    """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: dict[str, Any] = cthulhuApp.settingsManager.getGeneralSettings(cthulhuApp.settingsManager.profile)
    script = cthulhuApp.scriptManager.get_default_script()
    _showPreferencesUI(script, prefs)

    return True

def addKeyGrab(binding: Any) -> list[int]:
    """ Add a key grab for the given key binding."""

    manager = input_event_manager.get_manager()
    return manager.add_grabs_for_keybinding(binding)

def removeKeyGrab(id: int) -> None:
    """ Remove the key grab for the given key binding."""

    manager = input_event_manager.get_manager()
    manager.remove_grab_by_id(id)

def mapModifier(keycode: int) -> int:
    manager = input_event_manager.get_manager()
    return manager.map_keycode_to_modifier(keycode)

def quitCthulhu(script: Optional[Any] = None, inputEvent: Optional[Any] = None) -> bool:
    """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: Optional[Any] = None, inputEvent: Optional[Any] = None) -> 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: bool = False

def init() -> bool:
    """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 cthulhuApp.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)

    # Activate settings manager before loading user settings
    debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Activating settings manager', True)
    cthulhuApp.settingsManager.activate()

    loadUserSettings()

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

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

    return True

def _start_dbus_service() -> bool:
    """Starts the D-Bus remote controller service in an idle callback."""
    debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Starting D-Bus remote controller', True)
    try:
        # Start the D-Bus service
        dbus_service.get_remote_controller().start()

        # Register speech and verbosity manager
        debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Registering SpeechManager D-Bus module', True)
        from . import speech_and_verbosity_manager
        speech_manager = speech_and_verbosity_manager.getManager()
        dbus_service.get_remote_controller().register_decorated_module("SpeechManager", speech_manager)

        # Register typing echo presenter
        debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Registering TypingEchoManager D-Bus module', True)
        from . import typing_echo_presenter
        typing_echo_manager = typing_echo_presenter.getManager()
        dbus_service.get_remote_controller().register_decorated_module("TypingEchoManager", typing_echo_manager)

        # Register plugin system manager
        debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Registering PluginSystemManager D-Bus module', True)
        plugin_manager = cthulhuApp.getPluginSystemManager()
        dbus_service.get_remote_controller().register_decorated_module("PluginSystemManager", plugin_manager)

    except Exception as e:
        msg: str = f"CTHULHU: Failed to start D-Bus service: {e}"
        debug.printMessage(debug.LEVEL_SEVERE, msg, True)
    return False  # Remove the idle callback

def start() -> None:
    """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)
    
    # Start D-Bus remote controller service after ATSPI is ready
    cthulhuApp.getMakoNotificationMonitor().start()
    GObject.idle_add(_start_dbus_service)
    
    Atspi.event_main()

def die(exitCode: int = 1) -> None:
    pid: int = 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: Optional[int] = None, frame: Optional[FrameType] = None) -> None:
    msg: str = '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: Optional[Any] = None, inputEvent: Optional[Any] = None) -> bool:
    """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)

    # Shutdown D-Bus remote controller service early
    debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Shutting down D-Bus remote controller', True)
    dbus_service.get_remote_controller().shutdown()

    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')
    sound_theme_manager.getManager().playStopSound(wait=True, timeoutSeconds=1)
    cthulhuApp.getPluginSystemManager().unloadAllPlugins(ForceAllPlugins=True)
    cthulhuApp.getMakoNotificationMonitor().stop()

    # 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.
    cthulhuApp.compositorStateAdapter.deactivate()
    cthulhuApp.eventManager.deactivate()
    cthulhuApp.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
    cthulhu_modifier_manager.getManager().unsetCthulhuModifiers("Shutting down.")

    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: int = 0

def shutdownOnSignal(signum: int, frame: Optional[FrameType]) -> None:
    global exitCount

    signalString: str = f'({signal.strsignal(signum)})'
    msg: str = 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: bool = 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: int, frame: Optional[FrameType]) -> None:
    signalString: str = f'({signal.strsignal(signum)})'
    msg: str = f"CTHULHU: Shutting down and exiting due to signal={signum} {signalString}"
    debug.printMessage(debug.LEVEL_SEVERE, msg, True)
    debug.printStack(debug.LEVEL_SEVERE)
    cthulhu_modifier_manager.getManager().unsetCthulhuModifiers("Crashed")
    sys.exit(1)

def main() -> int:
    """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: str = f"CTHULHU: Launching version {cthulhu_platform.version}"
    if cthulhu_platform.revision:
        msg += f" (rev {cthulhu_platform.revision})"

    sessionType: str = os.environ.get('XDG_SESSION_TYPE') or ""
    sessionDesktop: str = os.environ.get('XDG_SESSION_DESKTOP') or ""
    session: str = "%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 cthulhuApp.settingsManager.isAccessibilityEnabled():
        cthulhuApp.settingsManager.setAccessibility(True)

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

    script = cthulhu_state.activeScript
    sound_theme_manager.getManager().playStartSound(wait=False)
    soundFailureReason = sound.getSoundSystemFailureReason()
    if soundFailureReason:
        debug.printMessage(
            debug.LEVEL_INFO,
            f"CTHULHU: Startup sound failed. Continuing without sound. Reason: {soundFailureReason}",
            True
        )
    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
            cthulhuApp.scriptManager.activate_script_for_context(app, window, "startup: launch")

            focusedObject = AXUtilities.get_focused_object(window)
            tokens = ["CTHULHU: Focused object is:", focusedObject]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            if focusedObject:
                setLocusOfFocus(None, focusedObject)
                cthulhuApp.scriptManager.activate_script_for_context(
                    AXObject.get_application(focusedObject), focusedObject, "startup: 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__: Dict[str, Tuple[Any, ...]] = {
        "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, ()),
        "active-script-changed": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,)), # New signal to indicate active script change
    }
    def __init__(self) -> None:
        GObject.Object.__init__(self)
        # add members
        self.resourceManager: resource_manager.ResourceManager = resource_manager.ResourceManager(self)
        self.settingsManager: SettingsManager = settings_manager.SettingsManager(self) # Directly instantiate
        self.eventManager: EventManager = event_manager.EventManager(self)         # Directly instantiate
        self.compositorStateAdapter: CompositorStateAdapter = compositor_state_adapter.CompositorStateAdapter()
        self.eventManager.set_compositor_state_adapter(self.compositorStateAdapter)
        self.scriptManager: ScriptManager = script_manager.ScriptManager(self)       # Directly instantiate
        script_manager._manager = self.scriptManager
        self.logger: logger.Logger = logger.Logger()                                 # Directly instantiate
        self.signalManager: SignalManager = signal_manager.SignalManager(self)
        self.dynamicApiManager: DynamicApiManager = dynamic_api_manager.DynamicApiManager(self)
        self.translationManager: TranslationManager = translation_manager.TranslationManager(self)
        self.debugManager: Any = debug
        self.APIHelper: APIHelper = APIHelper(self)
        self.makoNotificationMonitor: mako_notification_monitor.MakoNotificationMonitor = \
            mako_notification_monitor.MakoNotificationMonitor(self)
        self.createCompatAPI()
        self.pluginSystemManager: PluginSystemManager = plugin_system_manager.PluginSystemManager(self)
        # Scan for available plugins at startup
        self.pluginSystemManager.rescanPlugins()

    def getAPIHelper(self) -> APIHelper:
        return self.APIHelper

    def getPluginSystemManager(self) -> PluginSystemManager:
        return self.pluginSystemManager

    def getMakoNotificationMonitor(self) -> mako_notification_monitor.MakoNotificationMonitor:
        return self.makoNotificationMonitor

    def getDynamicApiManager(self) -> DynamicApiManager:
        return self.dynamicApiManager

    def getSignalManager(self) -> SignalManager:
        return self.signalManager

    def getEventManager(self) -> EventManager:
        return self.eventManager

    def getCompositorStateAdapter(self) -> CompositorStateAdapter:
        return self.compositorStateAdapter

    def getSettingsManager(self) -> SettingsManager:
        return self.settingsManager

    def getScriptManager(self) -> ScriptManager:
        return self.scriptManager

    def get_scriptManager(self) -> ScriptManager:
        return self.scriptManager

    def getDebugManager(self) -> Any:
        return self.debugManager

    def getTranslationManager(self) -> TranslationManager:
        return self.translationManager

    def getResourceManager(self) -> resource_manager.ResourceManager:
        return self.resourceManager

    def getLogger(self) -> logger.Logger: # New getter for the logger
        return self.logger

    def addKeyGrab(self, binding: Any) -> List[int]:
        return addKeyGrab(binding)

    def removeKeyGrab(self, grab_id: int) -> None:
        return removeKeyGrab(grab_id)

    def run(self, cacheValues: bool = True) -> int:
        return main()

    def stop(self) -> None:
        pass

    def createCompatAPI(self) -> None:
        # 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', self.logger) # Use instance
        self.getDynamicApiManager().registerAPI('SettingsManager', self.settingsManager) # Use instance
        self.getDynamicApiManager().registerAPI('ScriptManager', self.scriptManager) # Use instance
        self.getDynamicApiManager().registerAPI('EventManager', self.eventManager) # Use instance
        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('MakoNotificationMonitor', self.makoNotificationMonitor)
        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)
        self.getDynamicApiManager().registerAPI('APIHelper', self.APIHelper)

cthulhuApp: Cthulhu = Cthulhu()

def getManager() -> Cthulhu:
    return cthulhuApp

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