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

"""Plugin System Manager for Cthulhu using pluggy."""

import os
import inspect
import sys
import tarfile
import shutil
import importlib.util
import importlib.machinery
import logging
from enum import IntEnum

# Import pluggy if available
try:
    import pluggy
    PLUGGY_AVAILABLE = True
except ImportError:
    PLUGGY_AVAILABLE = False
    print("Warning: pluggy not available. Using fallback plugin system.")

try:
    from gi.repository import GObject
    from gi.repository import Atspi
    GOBJECT_AVAILABLE = True
except ImportError:
    GOBJECT_AVAILABLE = False
    print("Warning: GObject not available. Some functionality may be limited.")

from cthulhu import resource_manager

logger = logging.getLogger(__name__)

class PluginType(IntEnum):
    """Types of plugins we support, depending on their directory location."""
    # SYSTEM: provides system wide plugins
    SYSTEM = 1
    # USER: provides per user plugin
    USER = 2

    def __str__(self):
        if self.value == PluginType.SYSTEM:
            return "System plugin"
        elif self.value == PluginType.USER:
            return "User plugin"

    def get_root_dir(self):
        """Returns the directory where this type of plugins can be found."""
        if self.value == PluginType.SYSTEM:
            current_file = inspect.getfile(inspect.currentframe())
            current_dir = os.path.dirname(os.path.realpath(os.path.abspath(current_file)))
            return os.path.join(current_dir, 'plugins')
        elif self.value == PluginType.USER:
            return os.path.expanduser('~/.local/share/cthulhu/plugins')


class PluginInfo:
    """Information about a plugin."""
    
    def __init__(self, name, module_name, module_dir, metadata=None):
        """Initialize a PluginInfo object.
        
        Args:
            name: The plugin name.
            module_name: The module name.
            module_dir: The module directory.
            metadata: The plugin metadata.
        """
        self.name = name
        self.module_name = module_name
        self.module_dir = module_dir
        self.metadata = metadata or {}
        self.builtin = False
        self.hidden = False
        self.module = None
        self.instance = None
        self.loaded = False
    
    def get_module_name(self):
        """Get the module name.
        
        Returns:
            The module name.
        """
        return self.module_name
    
    def get_name(self):
        """Get the plugin name.
        
        Returns:
            The plugin name.
        """
        return self.metadata.get('name', self.name)
    
    def get_version(self):
        """Get the plugin version.
        
        Returns:
            The plugin version.
        """
        return self.metadata.get('version', '0.0.0')
    
    def get_description(self):
        """Get the plugin description.
        
        Returns:
            The plugin description.
        """
        return self.metadata.get('description', '')
    
    def get_authors(self):
        """Get the plugin authors.
        
        Returns:
            A list of plugin authors.
        """
        authors = self.metadata.get('authors', [])
        if isinstance(authors, str):
            authors = [authors]
        return authors
    
    def get_website(self):
        """Get the plugin website.
        
        Returns:
            The plugin website.
        """
        return self.metadata.get('website', '')
    
    def get_copyright(self):
        """Get the plugin copyright.
        
        Returns:
            The plugin copyright.
        """
        return self.metadata.get('copyright', '')
    
    def get_module_dir(self):
        """Get the plugin module directory.
        
        Returns:
            The plugin module directory.
        """
        return self.module_dir
    
    def get_data_dir(self):
        """Get the plugin data directory.
        
        Returns:
            The plugin data directory.
        """
        return self.module_dir
    
    def get_dependencies(self):
        """Get the plugin dependencies.
        
        Returns:
            A list of plugin dependencies.
        """
        return self.metadata.get('dependencies', [])
    
    def get_help_uri(self):
        """Get the plugin help URI.
        
        Returns:
            The plugin help URI.
        """
        return self.metadata.get('help_uri', '')
    
    def get_icon_name(self):
        """Get the plugin icon name.
        
        Returns:
            The plugin icon name.
        """
        return self.metadata.get('icon_name', '')
    
    def get_settings(self):
        """Get the plugin settings.
        
        Returns:
            The plugin settings.
        """
        return None  # To be implemented
    
    def is_builtin(self):
        """Check if the plugin is built-in.
        
        Returns:
            True if the plugin is built-in, False otherwise.
        """
        return self.builtin
    
    def is_hidden(self):
        """Check if the plugin is hidden.
        
        Returns:
            True if the plugin is hidden, False otherwise.
        """
        return self.hidden
    
    def is_available(self):
        """Check if the plugin is available.
        
        Returns:
            True if the plugin is available, False otherwise.
        """
        return True
    
    def is_loaded(self):
        """Check if the plugin is loaded.
        
        Returns:
            True if the plugin is loaded, False otherwise.
        """
        return self.loaded


class CthulhuHookSpecs:
    """Hook specifications for Cthulhu plugins."""
    
    def activate(self):
        """Called when the plugin is activated."""
        pass
    
    def deactivate(self):
        """Called when the plugin is deactivated."""
        pass


if GOBJECT_AVAILABLE:
    class API(GObject.GObject):
        """Interface that gives access to all the objects of Cthulhu."""
        def __init__(self, app):
            GObject.GObject.__init__(self)
            self.app = app
else:
    class API:
        """Interface that gives access to all the objects of Cthulhu."""
        def __init__(self, app):
            self.app = app


class PluginSystemManager:
    """Cthulhu Plugin Manager using pluggy."""
    
    def __init__(self, app):
        """Initialize the plugin system manager.
        
        Args:
            app: The Cthulhu application instance.
        """
        self.app = app
        
        # Initialize plugin manager
        if PLUGGY_AVAILABLE:
            self.plugin_manager = pluggy.PluginManager("cthulhu")
            hook_spec = pluggy.HookspecMarker("cthulhu")
            for name, method in inspect.getmembers(CthulhuHookSpecs, inspect.isfunction):
                setattr(CthulhuHookSpecs, name, hook_spec(method))
            self.plugin_manager.add_hookspecs(CthulhuHookSpecs)
        else:
            self.plugin_manager = None
        
        # List of plugin infos
        self._plugins = {}  # module_name -> PluginInfo
        self._active_plugins = []
        self._ignore_plugin_module_path = []
        
        self._setup_plugins_dir()
        
        if self.app:
            self.gsettingsManager = self.app.getSettingsManager()
    
    @property
    def plugins(self):
        """Get the list of all plugins.
        
        Returns:
            A list of PluginInfo objects.
        """
        return list(self._plugins.values())
    
    def _setup_plugins_dir(self):
        """Set up plugin directories."""
        # Ensure plugin directories exist
        system_plugins_dir = PluginType.SYSTEM.get_root_dir()
        user_plugins_dir = PluginType.USER.get_root_dir()
        
        os.makedirs(system_plugins_dir, exist_ok=True)
        os.makedirs(user_plugins_dir, exist_ok=True)
    
    def getApp(self):
        """Get the application instance.
        
        Returns:
            The Cthulhu application instance.
        """
        return self.app
    
    @classmethod
    def getPluginType(cls, pluginInfo):
        """Get the plugin type for a plugin.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            The plugin type (PluginType.SYSTEM or PluginType.USER).
        """
        plugin_dir = pluginInfo.get_module_dir()
        if plugin_dir.startswith(PluginType.SYSTEM.get_root_dir()):
            return PluginType.SYSTEM
        return PluginType.USER
    
    def getExtension(self, pluginInfo):
        """Get the plugin extension.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            The plugin instance or None if not found.
        """
        if not pluginInfo:
            return None
        
        return pluginInfo.instance
    
    def rescanPlugins(self):
        """Rescan for plugins in the plugin directories."""
        old_plugins = self._plugins.copy()
        self._plugins = {}
        
        # Scan system plugins
        self._scan_plugins_in_directory(PluginType.SYSTEM.get_root_dir(), PluginType.SYSTEM)
        
        # Scan user plugins
        self._scan_plugins_in_directory(PluginType.USER.get_root_dir(), PluginType.USER)
        
        # Preserve loaded state from old plugins
        for name, old_info in old_plugins.items():
            if name in self._plugins and old_info.loaded:
                self._plugins[name].loaded = True
                self._plugins[name].instance = old_info.instance
                self._plugins[name].module = old_info.module
        
        # Garbage collect
        if PLUGGY_AVAILABLE:
            plugins_to_remove = set(old_plugins.keys()) - set(self._plugins.keys())
            for name in plugins_to_remove:
                if old_plugins[name].instance:
                    try:
                        self.plugin_manager.unregister(old_plugins[name].instance)
                    except Exception as e:
                        logger.error(f"Error unregistering plugin {name}: {e}")
    
    def _scan_plugins_in_directory(self, directory, plugin_type):
        """Scan for plugins in a directory.
        
        Args:
            directory: The directory to scan.
            plugin_type: The plugin type.
        """
        if not os.path.exists(directory) or not os.path.isdir(directory):
            return
        
        for item in os.listdir(directory):
            plugin_dir = os.path.join(directory, item)
            if not os.path.isdir(plugin_dir):
                continue
            
            if plugin_dir in self._ignore_plugin_module_path:
                continue
            
            # Look for plugin files
            plugin_file = os.path.join(plugin_dir, "plugin.py")
            metadata_file = os.path.join(plugin_dir, "plugin.info")
            
            if os.path.isfile(plugin_file):
                # Extract plugin info
                module_name = os.path.basename(plugin_dir)
                metadata = self._load_plugin_metadata(metadata_file)
                
                plugin_info = PluginInfo(
                    metadata.get('name', module_name),
                    module_name,
                    plugin_dir,
                    metadata
                )
                
                plugin_info.builtin = (plugin_type == PluginType.SYSTEM and 
                                     metadata.get('builtin', 'false') == 'true')
                plugin_info.hidden = metadata.get('hidden', 'false') == 'true'
                
                self._plugins[module_name] = plugin_info
    
    def _load_plugin_metadata(self, metadata_file):
        """Load plugin metadata from a file.
        
        Args:
            metadata_file: The metadata file path.
        
        Returns:
            A dictionary of metadata.
        """
        metadata = {}
        
        if os.path.isfile(metadata_file):
            try:
                with open(metadata_file, 'r') as f:
                    for line in f:
                        line = line.strip()
                        if not line or line.startswith('#'):
                            continue
                        
                        if '=' in line:
                            key, value = line.split('=', 1)
                            metadata[key.strip()] = value.strip()
            except Exception as e:
                logger.error(f"Error loading plugin metadata: {e}")
        
        return metadata
    
    def getPluginInfoByName(self, pluginName, pluginType=PluginType.USER):
        """Get plugin information by name.
        
        Args:
            pluginName: The plugin name.
            pluginType: The plugin type.
        
        Returns:
            The plugin information or None if not found.
        """
        plugin_info = self._plugins.get(pluginName)
        if plugin_info and self.getPluginType(plugin_info) == pluginType:
            return plugin_info
        return None
    
    def getActivePlugins(self):
        """Get the list of active plugin names.
        
        Returns:
            A list of active plugin names.
        """
        return self._active_plugins
    
    def setActivePlugins(self, activePlugins):
        """Set the list of active plugins and sync their state.
        
        Args:
            activePlugins: A list of plugin names to activate.
        """
        self._active_plugins = activePlugins
        self.syncAllPluginsActive()
    
    def isPluginBuildIn(self, pluginInfo):
        """Check if a plugin is built-in.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            True if the plugin is built-in, False otherwise.
        """
        return pluginInfo.is_builtin()
    
def isPluginHidden(self, pluginInfo):
        """Check if a plugin is hidden.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            True if the plugin is hidden, False otherwise.
        """
        return pluginInfo.is_hidden()
    
    def getPluginAuthors(self, pluginInfo):
        """Get the plugin authors.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            A list of plugin authors.
        """
        return pluginInfo.get_authors()
    
    def getPluginCopyright(self, pluginInfo):
        """Get the plugin copyright.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            The plugin copyright.
        """
        return pluginInfo.get_copyright()
    
    def getPluginDataDir(self, pluginInfo):
        """Get the plugin data directory.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            The plugin data directory.
        """
        return pluginInfo.get_data_dir()
    
    def getPluginDependencies(self, pluginInfo):
        """Get the plugin dependencies.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            A list of plugin dependencies.
        """
        return pluginInfo.get_dependencies()
    
    def getPluginDescription(self, pluginInfo):
        """Get the plugin description.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            The plugin description.
        """
        return pluginInfo.get_description()
    
    def getPlugingetHelpUri(self, pluginInfo):
        """Get the plugin help URI.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            The plugin help URI.
        """
        return pluginInfo.get_help_uri()
    
    def getPluginIconName(self, pluginInfo):
        """Get the plugin icon name.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            The plugin icon name.
        """
        return pluginInfo.get_icon_name()
    
    def getPluginModuleDir(self, pluginInfo):
        """Get the plugin module directory.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            The plugin module directory.
        """
        return pluginInfo.get_module_dir()
    
    def getPluginModuleName(self, pluginInfo):
        """Get the plugin module name.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            The plugin module name.
        """
        return pluginInfo.get_module_name()
    
    def getPluginName(self, pluginInfo):
        """Get the plugin name.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            The plugin name.
        """
        return pluginInfo.get_name()
    
    def getPluginSettings(self, pluginInfo):
        """Get the plugin settings.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            The plugin settings.
        """
        return pluginInfo.get_settings()
    
    def getPluginVersion(self, pluginInfo):
        """Get the plugin version.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            The plugin version.
        """
        return pluginInfo.get_version()
    
    def getPluginWebsite(self, pluginInfo):
        """Get the plugin website.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            The plugin website.
        """
        return pluginInfo.get_website()
    
    def isPluginAvailable(self, pluginInfo):
        """Check if a plugin is available.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            True if the plugin is available, False otherwise.
        """
        try:
            return pluginInfo.is_available()
        except:
            return False
    
    def isPluginLoaded(self, pluginInfo):
        """Check if a plugin is loaded.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            True if the plugin is loaded, False otherwise.
        """
        try:
            return pluginInfo.is_loaded()
        except:
            return False
    
    def getIgnoredPlugins(self):
        """Get the list of ignored plugin paths.
        
        Returns:
            A list of ignored plugin paths.
        """
        return self._ignore_plugin_module_path
    
    def setIgnoredPlugins(self, pluginModulePath, ignored):
        """Set a plugin path as ignored or not.
        
        Args:
            pluginModulePath: The plugin module path.
            ignored: Whether to ignore the plugin.
        """
        if pluginModulePath.endswith('/'):
            pluginModulePath = pluginModulePath[:-1]
        
        if ignored:
            if pluginModulePath not in self.getIgnoredPlugins():
                self._ignore_plugin_module_path.append(pluginModulePath)
        else:
            if pluginModulePath in self.getIgnoredPlugins():
                self._ignore_plugin_module_path.remove(pluginModulePath)
    
    def setPluginActive(self, pluginInfo, active):
        """Set the active state of a plugin.
        
        Args:
            pluginInfo: The plugin information.
            active: Whether to activate the plugin.
        """
        if self.isPluginBuildIn(pluginInfo):
            active = True
        
        pluginName = self.getPluginModuleName(pluginInfo)
        
        if active:
            if pluginName not in self.getActivePlugins():
                if self.loadPlugin(pluginInfo):
                    self._active_plugins.append(pluginName)
        else:
            if pluginName in self.getActivePlugins():
                if self.unloadPlugin(pluginInfo):
                    self._active_plugins.remove(pluginName)
    
    def isPluginActive(self, pluginInfo):
        """Check if a plugin is active.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            True if the plugin is active, False otherwise.
        """
        if self.isPluginBuildIn(pluginInfo):
            return True
        
        if self.isPluginLoaded(pluginInfo):
            return True
        
        active_plugin_names = self.getActivePlugins()
        return self.getPluginModuleName(pluginInfo) in active_plugin_names
    
    def syncAllPluginsActive(self, ForceAllPlugins=False):
        """Sync the active state of all plugins.
        
        Args:
            ForceAllPlugins: Whether to force all plugins to sync.
        """
        self.unloadAllPlugins(ForceAllPlugins)
        self.loadAllPlugins(ForceAllPlugins)
    
    def loadAllPlugins(self, ForceAllPlugins=False):
        """Load all active plugins.
        
        Args:
            ForceAllPlugins: Whether to force all plugins to load.
        """
        for pluginInfo in self.plugins:
            if self.isPluginActive(pluginInfo) or ForceAllPlugins:
                self.loadPlugin(pluginInfo)
    
    def loadPlugin(self, pluginInfo):
        """Load a plugin.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            True if the plugin was loaded successfully, False otherwise.
        """
        resourceManager = self.getApp().getResourceManager()
        moduleName = pluginInfo.get_module_name()
        
        try:
            if pluginInfo not in self.plugins:
                print(f"Plugin missing: {moduleName}")
                return False
            
            # Add resource context
            resourceManager.addResourceContext(moduleName)
            
            # If already loaded, nothing to do
            if pluginInfo.loaded:
                return True
            
            # Import plugin module
            plugin_file = os.path.join(pluginInfo.get_module_dir(), "plugin.py")
            spec = importlib.util.spec_from_file_location(moduleName, plugin_file)
            module = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(module)
            pluginInfo.module = module
            
            # Find Plugin class
            plugin_class = None
            for attr_name in dir(module):
                attr = getattr(module, attr_name)
                if (inspect.isclass(attr) and 
                    attr.__module__ == module.__name__ and
                    hasattr(attr, 'activate')):
                    plugin_class = attr
                    break
            
            if not plugin_class:
                print(f"No plugin class found in {moduleName}")
                return False
            
            # Create plugin instance
            plugin_instance = plugin_class()
            pluginInfo.instance = plugin_instance
            
            # Initialize plugin
            if hasattr(plugin_instance, 'setApp'):
                plugin_instance.setApp(self.getApp())
            
            if hasattr(plugin_instance, 'setPluginInfo'):
                plugin_instance.setPluginInfo(pluginInfo)
            
            # Register with pluggy
            if PLUGGY_AVAILABLE and self.plugin_manager:
                self.plugin_manager.register(plugin_instance)
            
            # Activate plugin
            if hasattr(plugin_instance, 'activate'):
                plugin_instance.activate()
            
            pluginInfo.loaded = True
            return True
            
        except Exception as e:
            print(f'loadPlugin: {e}')
            import traceback
            traceback.print_exc()
            return False
    
    def unloadAllPlugins(self, ForceAllPlugins=False):
        """Unload all inactive plugins.
        
        Args:
            ForceAllPlugins: Whether to force all plugins to unload.
        """
        for pluginInfo in self.plugins:
            if not self.isPluginActive(pluginInfo) or ForceAllPlugins:
                self.unloadPlugin(pluginInfo)
    
    def unloadPlugin(self, pluginInfo):
        """Unload a plugin.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            True if the plugin was unloaded successfully, False otherwise.
        """
        resourceManager = self.getApp().getResourceManager()
        moduleName = pluginInfo.get_module_name()
        
        try:
            if pluginInfo not in self.plugins:
                print(f"Plugin missing: {moduleName}")
                return False
            
            if self.isPluginBuildIn(pluginInfo):
                return False
            
            # If not loaded, nothing to do
            if not pluginInfo.loaded:
                return True
            
            # Deactivate plugin
            plugin_instance = pluginInfo.instance
            if plugin_instance:
                if hasattr(plugin_instance, 'deactivate'):
                    plugin_instance.deactivate()
                
                # Unregister from pluggy
                if PLUGGY_AVAILABLE and self.plugin_manager:
                    self.plugin_manager.unregister(plugin_instance)
            
            # Clean up
            pluginInfo.instance = None
            pluginInfo.loaded = False
            
            # Remove resource context
            resourceManager.removeResourceContext(moduleName)
            
            return True
            
        except Exception as e:
            print(f'unloadPlugin: {e}')
            return False
    
    def installPlugin(self, pluginFilePath, pluginType=PluginType.USER):
        """Install a plugin from a tarball.
        
        Args:
            pluginFilePath: The plugin file path.
            pluginType: The plugin type.
        
        Returns:
            True if the plugin was installed successfully, False otherwise.
        """
        if not self.isValidPluginFile(pluginFilePath):
            return False
        
        pluginFolder = pluginType.get_root_dir()
        if not pluginFolder.endswith('/'):
            pluginFolder += '/'
        
        if not os.path.exists(pluginFolder):
            os.makedirs(pluginFolder)
        else:
            if not os.path.isdir(pluginFolder):
                return False
        
        try:
            with tarfile.open(pluginFilePath) as tar:
                tar.extractall(path=pluginFolder)
        except Exception as e:
            print(e)
            return False
        
        pluginModulePath = self.getModuleDirByPluginFile(pluginFilePath)
        if pluginModulePath:
            pluginModulePath = pluginFolder + pluginModulePath
            self.setIgnoredPlugins(pluginModulePath[:-1], False)  # without ending /
            print(f'install: {pluginFilePath}')
            self.callPackageTriggers(pluginModulePath, 'onPostInstall')
        
        self.rescanPlugins()
        return True
    
    def getModuleDirByPluginFile(self, pluginFilePath):
        """Get the module directory from a plugin file.
        
        Args:
            pluginFilePath: The plugin file path.
        
        Returns:
            The module directory, or an empty string if not found.
        """
        if not isinstance(pluginFilePath, str):
            return ''
        
        if not pluginFilePath:
            return ''
        
        if not os.path.exists(pluginFilePath):
            return ''
        
        try:
            with tarfile.open(pluginFilePath) as tar:
                tarMembers = tar.getmembers()
                for tarMember in tarMembers:
                    if tarMember.isdir():
                        return tarMember.name
        except Exception as e:
            print(e)
        
        return ''
    
    def isValidPluginFile(self, pluginFilePath):
        """Check if a file is a valid plugin package.
        
        Args:
            pluginFilePath: The plugin file path.
        
        Returns:
            True if the file is a valid plugin package, False otherwise.
        """
        if not isinstance(pluginFilePath, str):
            return False
        
        if not pluginFilePath:
            return False
        
        if not os.path.exists(pluginFilePath):
            return False
        
        pluginFolder = ''
        pluginFileExists = False
        packageFileExists = False
        
        try:
            with tarfile.open(pluginFilePath) as tar:
                tarMembers = tar.getmembers()
                for tarMember in tarMembers:
                    if tarMember.isdir():
                        if not pluginFolder:
                            pluginFolder = tarMember.name
                    
                    if tarMember.isfile():
                        if tarMember.name.endswith('.plugin'):
                            pluginFileExists = True
                        if tarMember.name.endswith('package.py'):
                            packageFileExists = True
                    
                    if not tarMember.name.startswith(pluginFolder):
                        return False
        except Exception as e:
            print(e)
            return False
        
        return pluginFileExists
    
    def uninstallPlugin(self, pluginInfo):
        """Uninstall a plugin.
        
        Args:
            pluginInfo: The plugin information.
        
        Returns:
            True if the plugin was uninstalled successfully, False otherwise.
        """
        if self.isPluginBuildIn(pluginInfo):
            return False
        
        # Do we want to allow removing system plugins?
        if self.getPluginType(pluginInfo) == PluginType.SYSTEM:
            return False
        
        pluginFolder = pluginInfo.get_data_dir()
        if not pluginFolder.endswith('/'):
            pluginFolder += '/'
        
        if not os.path.isdir(pluginFolder):
            return False
        
        if self.isPluginActive(pluginInfo):
            self.setPluginActive(pluginInfo, False)
            # TODO: Update active plugins in settings
        
        self.callPackageTriggers(pluginFolder, 'onPreUninstall')
        
        try:
            shutil.rmtree(pluginFolder, ignore_errors=True)
        except Exception as e:
            print(e)
            return False
        
        self.setIgnoredPlugins(pluginFolder, True)
        self.rescanPlugins()
        
        return True
    
    def callPackageTriggers(self, pluginPath, trigger):
        """Call package trigger functions.
        
        Args:
            pluginPath: The plugin path.
            trigger: The trigger name.
        """
        if not os.path.exists(pluginPath):
            return
        
        if not pluginPath.endswith('/'):
            pluginPath += '/'
        
        packageModulePath = pluginPath + 'package.py'
        if not os.path.isfile(packageModulePath):
            return
        
        if not os.access(packageModulePath, os.R_OK):
            return
        
        package = self.getApp().getAPIHelper().importModule('package', packageModulePath)
        
        if trigger == 'onPostInstall':
            try:
                package.onPostInstall(pluginPath, self.getApp())
            except Exception as e:
                print(e)
        elif trigger == 'onPreUninstall':
            try:
                package.onPreUninstall(pluginPath, self.getApp())
            except Exception as e:
                print(e)


class APIHelper:
    """API Helper for plugin system."""
    
    def __init__(self, app):
        """Initialize the API helper.
        
        Args:
            app: The Cthulhu application instance.
        """
        self.app = app
        self.cthulhuKeyBindings = None
    
    def outputMessage(self, Message, interrupt=False):
        """Output a message.
        
        Args:
            Message: The message to output.
            interrupt: Whether to interrupt current speech.
        """
        settings = self.app.getDynamicApiManager().getAPI('Settings')
        braille = self.app.getDynamicApiManager().getAPI('Braille')
        speech = self.app.getDynamicApiManager().getAPI('Speech')
        
        if speech is not None:
            if settings.enableSpeech:
                if interrupt:
                    speech.cancel()
                if Message:
                    speech.speak(Message)
        
        if braille is not None:
            if settings.enableBraille:
                braille.displayMessage(Message)
    
    def createInputEventHandler(self, function, name, learnModeEnabled=True):
        """Create an input event handler.
        
        Args:
            function: The function to call.
            name: The handler name.
            learnModeEnabled: Whether learn mode is enabled.
        
        Returns:
            The input event handler.
        """
        EventManager = self.app.getDynamicApiManager().getAPI('EventManager')
        newInputEventHandler = EventManager.input_event.InputEventHandler(function, name, learnModeEnabled)
        return newInputEventHandler
    
    def registerGestureByString(self, function, name, gestureString, profile, application, learnModeEnabled=True, contextName=None):
        """Register a gesture by string.
        
        Args:
            function: The function to call.
            name: The gesture name.
            gestureString: The gesture string.
            profile: The profile.
            application: The application.
            learnModeEnabled: Whether learn mode is enabled.
            contextName: The context name.
        
        Returns:
            The registered gestures.
        """
        gestureList = gestureString.split(',')
        registeredGestures = []
        
        for gesture in gestureList:
            if gesture.startswith('kb:'):
                shortcutString = gesture[3:]
                registuredGesture = self.registerShortcutByString(
                    function, name, shortcutString, profile, application, learnModeEnabled, contextName=contextName
                )
                if registuredGesture:
                    registeredGestures.append(registuredGesture)
        
        return registeredGestures
    
    def registerShortcutByString(self, function, name, shortcutString, profile, application, learnModeEnabled=True, contextName=None):
        """Register a shortcut by string.
        
        Args:
            function: The function to call.
            name: The shortcut name.
            shortcutString: The shortcut string.
            profile: The profile.
            application: The application.
            learnModeEnabled: Whether learn mode is enabled.
            contextName: The context name.
        
        Returns:
            The registered shortcut.
        """
        keybindings = self.app.getDynamicApiManager().getAPI('Keybindings')
        settings = self.app.getDynamicApiManager().getAPI('Settings')
        resourceManager = self.app.getResourceManager()
        
        clickCount = 0
        cthulhuKey = False
        shiftKey = False
        ctrlKey = False
        altKey = False
        key = ''
        
        shortcutList = shortcutString.split('+')
        for shortcutElement in shortcutList:
            shortcutElementLower = shortcutElement.lower()
            if shortcutElementLower == 'press':
                clickCount += 1
            elif shortcutElement == 'cthulhu':
                cthulhuKey = True
            elif shortcutElementLower == 'shift':
                shiftKey = True
            elif shortcutElementLower == 'control':
                ctrlKey = True
            elif shortcutElementLower == 'alt':
                altKey = True
            else:
                key = shortcutElementLower
        
        if clickCount == 0:
            clickCount = 1
        
        if self.cthulhuKeyBindings is None:
            self.cthulhuKeyBindings = keybindings.KeyBindings()
        
        tryFunction = resource_manager.TryFunction(function)
        newInputEventHandler = self.createInputEventHandler(tryFunction.runInputEvent, name, learnModeEnabled)
        
        currModifierMask = keybindings.NO_MODIFIER_MASK
        if cthulhuKey:
            currModifierMask = currModifierMask | 1 << keybindings.MODIFIER_CTHULHU
        if shiftKey:
            currModifierMask = currModifierMask | 1 << Atspi.ModifierType.SHIFT
        if altKey:
            currModifierMask = currModifierMask | 1 << Atspi.ModifierType.ALT
        if ctrlKey:
            currModifierMask = currModifierMask | 1 << Atspi.ModifierType.CONTROL
        
        newKeyBinding = keybindings.KeyBinding(
            key, keybindings.defaultModifierMask, currModifierMask, newInputEventHandler, clickCount
        )
        self.cthulhuKeyBindings.add(newKeyBinding)
        
        settings.keyBindingsMap["default"] = self.cthulhuKeyBindings
        
        if contextName:
            resourceContext = resourceManager.getResourceContext(contextName)
            if resourceContext:
                resourceEntry = resource_manager.ResourceEntry(
                    'keyboard', newKeyBinding, function, tryFunction, shortcutString
                )
                resourceContext.addGesture(profile, application, newKeyBinding, resourceEntry)
        
        return newKeyBinding
    
    def unregisterShortcut(self, KeyBindingToRemove, contextName=None):
        """Unregister a shortcut.
        
        Args:
            KeyBindingToRemove: The key binding to remove.
            contextName: The context name.
        
        Returns:
            True if the shortcut was unregistered, False otherwise.
        """
        ok = False
        keybindings = self.app.getDynamicApiManager().getAPI('Keybindings')
        settings = self.app.getDynamicApiManager().getAPI('Settings')
        resourceManager = self.app.getResourceManager()
        
        if self.cthulhuKeyBindings is None:
            self.cthulhuKeyBindings = keybindings.KeyBindings()
        
        try:
            self.cthulhuKeyBindings.remove(KeyBindingToRemove)
            settings.keyBindingsMap["default"] = self.cthulhuKeyBindings
            ok = True
        except KeyError:
            pass
        
        if contextName:
            resourceContext = resourceManager.getResourceContext(contextName)
            if resourceContext:
                resourceContext.removeGesture(KeyBindingToRemove)
        
        return ok
    
    def importModule(self, moduleName, moduleLocation):
        """Import a module from a file location.
        
        Args:
            moduleName: The module name.
            moduleLocation: The module location.
        
        Returns:
            The imported module.
        """
        spec = importlib.util.spec_from_file_location(moduleName, moduleLocation)
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        return module
