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

__id__        = "$Id$"
__version__   = "$Revision$"
__date__      = "$Date$"
__copyright__ = "Copyright (c) 2011. Cthulhu Team."
__license__   = "LGPL"

import importlib

from . import debug
from . import cthulhu_state
from .ax_object import AXObject
from .ax_utilities import AXUtilities
from .scripts import apps, toolkits

class ScriptManager:

    def __init__(self):
        debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Initializing", True)
        self.appScripts = {}
        self.toolkitScripts = {}
        self.customScripts = {}
        self._appModules = apps.__all__
        self._toolkitModules = toolkits.__all__
        self._defaultScript = None
        self._scriptPackages = \
            ["cthulhu-scripts",
             "cthulhu.scripts",
             "cthulhu.scripts.apps",
             "cthulhu.scripts.toolkits"]
        self._appNames = \
            {'Icedove': 'Thunderbird',
             'Nereid': 'Banshee',
             'gnome-calculator': 'gcalctool',
             'gtk-window-decorator': 'switcher',
             'marco': 'switcher',
             'xfce4-notifyd': 'notification-daemon',
             'mate-notification-daemon': 'notification-daemon',
             'metacity': 'switcher',
             'pluma': 'gedit',
            }
        self._toolkitNames = \
            {'WebKitGTK': 'WebKitGtk', 'GTK': 'gtk'}

        self.setActiveScript(None, "__init__")
        self._active = False
        debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Initialized", True)

    def activate(self):
        """Called when this script manager is activated."""

        debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Activating", True)
        self._defaultScript = self.getScript(None)
        self._defaultScript.registerEventListeners()
        self.setActiveScript(self._defaultScript, "activate")
        self._active = True
        debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Activated", True)

    def deactivate(self):
        """Called when this script manager is deactivated."""

        debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Deactivating", True)
        if self._defaultScript:
            self._defaultScript.deregisterEventListeners()
        self._defaultScript = None
        self.setActiveScript(None, "deactivate")
        self.appScripts = {}
        self.toolkitScripts = {}
        self.customScripts = {}
        self._active = False
        debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Deactivated", True)

    def getModuleName(self, app):
        """Returns the module name of the script to use for application app."""

        if app is None:
            msg = "SCRIPT MANAGER: Cannot get module name for null app"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return None

        name = AXObject.get_name(app)
        if not name:
            msg = "SCRIPT MANAGER: Cannot get module name for nameless app"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return None

        altNames = list(self._appNames.keys())
        if name.endswith(".py") or name.endswith(".bin"):
            name = name.split('.')[0]
        elif name.startswith("org.") or name.startswith("com."):
            name = name.split('.')[-1]

        names = [n for n in altNames if n.lower() == name.lower()]
        if names:
            name = self._appNames.get(names[0])
        else:
            for nameList in (self._appModules, self._toolkitModules):
                names = [n for n in nameList if n.lower() == name.lower()]
                if names:
                    name = names[0]
                    break

        tokens = ["SCRIPT MANAGER: Mapped", app, "to", name]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)
        return name

    def _toolkitForObject(self, obj):
        """Returns the name of the toolkit associated with obj."""

        name = AXObject.get_attribute(obj, 'toolkit')
        return self._toolkitNames.get(name, name)

    def _scriptForRole(self, obj):
        if AXUtilities.is_terminal(obj):
            return 'terminal'

        return ''

    def _newNamedScript(self, app, name):
        """Attempts to locate and load the named module. If successful, returns
        a script based on this module."""

        if not (app and name):
            return None

        script = None
        for package in self._scriptPackages:
            moduleName = '.'.join((package, name))
            try:
                module = importlib.import_module(moduleName)
            except ImportError:
                continue
            except OSError:
                debug.examineProcesses()

            tokens = ["SCRIPT MANAGER: Found", moduleName]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            try:
                if hasattr(module, 'getScript'):
                    script = module.getScript(app)
                else:
                    script = module.Script(app)
                break
            except Exception as error:
                tokens = ["SCRIPT MANAGER: Could not load", moduleName, ":", error]
                debug.printTokens(debug.LEVEL_INFO, tokens, True)

        return script

    def _createScript(self, app, obj=None):
        """For the given application, create a new script instance."""

        moduleName = self.getModuleName(app)
        script = self._newNamedScript(app, moduleName)
        if script:
            return script

        objToolkit = self._toolkitForObject(obj)
        script = self._newNamedScript(app, objToolkit)
        if script:
            return script

        toolkitName = AXObject.get_application_toolkit_name(app)
        if app and toolkitName:
            script = self._newNamedScript(app, toolkitName)

        if not script:
            script = self.getDefaultScript(app)
            tokens = ["SCRIPT MANAGER: Default script created for", app, "(obj: ", obj, ")"]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)

        return script

    def getDefaultScript(self, app=None):
        if not app and self._defaultScript:
            return self._defaultScript

        from .scripts import default
        script = default.Script(app)

        if not app:
            self._defaultScript = script

        return script

    def sanityCheckScript(self, script):
        if not self._active:
            return script

        if AXUtilities.is_application_in_desktop(script.app):
            return script

        newScript = self._getScriptForAppReplicant(script.app)
        if newScript:
            return newScript

        tokens = ["WARNING: Failed to get a replacement script for", script.app]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)
        return script

    def getScriptForMouseButtonEvent(self, event):
        isActive = AXUtilities.is_active(cthulhu_state.activeWindow)
        tokens = ["SCRIPT MANAGER:", cthulhu_state.activeWindow, "is active:", isActive]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)

        if isActive and cthulhu_state.activeScript:
            return cthulhu_state.activeScript

        script = self.getDefaultScript()
        activeWindow = script.utilities.activeWindow()
        if not activeWindow:
            return script

        focusedObject = AXUtilities.get_focused_object(activeWindow)
        if focusedObject:
            return self.getScript(AXObject.get_application(focusedObject), focusedObject)

        return self.getScript(AXObject.get_application(activeWindow), activeWindow)

    def getScript(self, app, obj=None, sanityCheck=False):
        """Get a script for an app (and make it if necessary).  This is used
        instead of a simple calls to Script's constructor.

        Arguments:
        - app: the Python app

        Returns an instance of a Script.
        """

        customScript = None
        appScript = None
        toolkitScript = None

        roleName = self._scriptForRole(obj)
        if roleName:
            customScripts = self.customScripts.get(app, {})
            customScript = customScripts.get(roleName)
            if not customScript:
                customScript = self._newNamedScript(app, roleName)
                customScripts[roleName] = customScript
            self.customScripts[app] = customScripts

        objToolkit = self._toolkitForObject(obj)
        if objToolkit:
            toolkitScripts = self.toolkitScripts.get(app, {})
            toolkitScript = toolkitScripts.get(objToolkit)
            if not toolkitScript:
                toolkitScript = self._createScript(app, obj)
                toolkitScripts[objToolkit] = toolkitScript
            self.toolkitScripts[app] = toolkitScripts

        try:
            if not app:
                appScript = self.getDefaultScript()
            elif app in self.appScripts:
                appScript = self.appScripts[app]
            else:
                appScript = self._createScript(app, None)
                self.appScripts[app] = appScript
        except Exception as error:
            tokens = ["SCRIPT MANAGER: Exception getting app script for", app, ":", error]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            appScript = self.getDefaultScript()

        if customScript:
            return customScript

        # Only defer to the toolkit script for this object if the app script
        # is based on a different toolkit.
        if toolkitScript and not (AXUtilities.is_frame(obj) or AXUtilities.is_status_bar(obj)) \
           and not issubclass(appScript.__class__, toolkitScript.__class__):
            return toolkitScript

        if app and sanityCheck:
            appScript = self.sanityCheckScript(appScript)

        return appScript

    def setActiveScript(self, newScript, reason=None):
        """Set the new active script.

        Arguments:
        - newScript: the new script to be made active.
        """

        if cthulhu_state.activeScript == newScript:
            return

        if cthulhu_state.activeScript:
            cthulhu_state.activeScript.deactivate()

        cthulhu_state.activeScript = newScript
        if not newScript:
            return

        newScript.activate()
        tokens = ["SCRIPT MANAGER: Setting active script to", newScript, "reason:", reason]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)

    def _getScriptForAppReplicant(self, app):
        if not self._active:
            return None

        pid = AXObject.get_process_id(app)
        if pid == -1:
            return None

        items = self.appScripts.items()
        for a, script in items:
            if AXObject.get_process_id(a) != pid:
                continue
            if a != app and AXUtilities.is_application_in_desktop(a):
                if script.app is None:
                    script.app = a
                tokens = ["SCRIPT MANAGER: Script for app replicant:", script, script.app]
                debug.printTokens(debug.LEVEL_INFO, tokens, True)
                return script

        return None

    def reclaimScripts(self):
        """Compares the list of known scripts to the list of known apps,
        deleting any scripts as necessary.
        """

        msg = "SCRIPT MANAGER: Checking and cleaning up scripts."
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        appList = list(self.appScripts.keys())
        for app in appList:
            if AXUtilities.is_application_in_desktop(app):
                continue

            try:
                appScript = self.appScripts.pop(app)
            except KeyError:
                tokens = ["SCRIPT MANAGER:", app, "not found in appScripts"]
                debug.printTokens(debug.LEVEL_INFO, tokens, True)
                continue

            tokens = ["SCRIPT MANAGER: Old script for app found:", appScript, appScript.app]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)

            newScript = self._getScriptForAppReplicant(app)
            if newScript:
                tokens = ["SCRIPT MANAGER: Transferring attributes:", newScript, newScript.app]
                debug.printTokens(debug.LEVEL_INFO, tokens, True)
                attrs = appScript.getTransferableAttributes()
                for attr, value in attrs.items():
                    tokens = ["SCRIPT MANAGER: Setting", attr, "to", value]
                    debug.printTokens(debug.LEVEL_INFO, tokens, True)
                    setattr(newScript, attr, value)

            del appScript

            try:
                toolkitScripts = self.toolkitScripts.pop(app)
            except KeyError:
                pass
            else:
                for toolkitScript in toolkitScripts.values():
                    del toolkitScript

            try:
                customScripts = self.customScripts.pop(app)
            except KeyError:
                pass
            else:
                for customScript in customScripts.values():
                    del customScript

            del app

_manager = ScriptManager()

def getManager():
    return _manager
