#!/usr/bin/env python3
#
# Copyright (c) 2024 Stormux
#
# 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.
#

"""Simple Plugin System for Cthulhu."""

import glob
import os
import importlib.util
import random
import string
import _thread
import logging
from subprocess import Popen, PIPE
import gettext

from cthulhu.plugin import Plugin, cthulhu_hookimpl

# Set up translation function
_ = gettext.gettext

logger = logging.getLogger(__name__)

# Global variables for API access
settings = None
speech = None
braille = None
input_event = None

def outputMessage(Message):
    """Output a message via speech and/or braille depending on settings."""
    if (settings.enableSpeech):
        speech.speak(Message)
    if (settings.enableBraille):
        braille.displayMessage(Message)

class SimplePluginSystem(Plugin):
    """Simple plugin system implementation for Cthulhu.
    
    This plugin allows loading and managing simple script-based plugins
    from a designated directory.
    """
    
    def __init__(self, *args, **kwargs):
        """Initialize the plugin system."""
        super().__init__(*args, **kwargs)
        logger.info("SimplePluginSystem plugin initialized")
        self.plugin_list = []
        self.loaded = False
        self.plugin_repo = os.path.expanduser('~') + "/.local/share/cthulhu/simple-plugins-enabled/"
        self._signal_handler_id = None

    @cthulhu_hookimpl
    def activate(self, plugin=None):
        """Activate the plugin system."""
        # Skip if this activation call isn't for us
        if plugin is not None and plugin is not self:
            return
            
        logger.info("Activating SimplePluginSystem plugin")
        try:
            global settings
            global speech
            global braille
            global input_event
            
            settings = self.app.getDynamicApiManager().getAPI('Settings')
            speech = self.app.getDynamicApiManager().getAPI('Speech')
            braille = self.app.getDynamicApiManager().getAPI('Braille')
            input_event = self.app.getDynamicApiManager().getAPI('InputEvent')
            
            if not self.loaded:
                self.load_plugins()
                
        except Exception as e:
            logger.error(f"Error activating SimplePluginSystem plugin: {e}")

    @cthulhu_hookimpl
    def deactivate(self, plugin=None):
        """Deactivate the plugin system."""
        # Skip if this deactivation call isn't for us
        if plugin is not None and plugin is not self:
            return
            
        logger.info("Deactivating SimplePluginSystem plugin")
        try:
            # Remove all registered keybindings
            for plugin in self.plugin_list:
                self.unregisterShortcut(plugin['function'], plugin['shortcut'])
            self.loaded = False
            self.plugin_list = []
        except Exception as e:
            logger.error(f"Error deactivating SimplePluginSystem plugin: {e}")

    def SetupShortcutAndHandle(self, currPluginSetting):
        """Set up keyboard shortcuts for a plugin."""
        shortcut = ''
        # just the modifier
        if not currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
            shortcut = 'kb:cthulhu+' + currPluginSetting['key']
        # cthulhu + alt
        if not currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
            shortcut = 'kb:cthulhu+alt+' + currPluginSetting['key']
        # cthulhu + CTRL
        if not currPluginSetting['shiftkey'] and currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
            shortcut = 'kb:cthulhu+control+' + currPluginSetting['key']
        # cthulhu + alt + CTRL
        if not currPluginSetting['shiftkey'] and currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
            shortcut = 'kb:cthulhu+alt+control+ ' + currPluginSetting['key']
       # cthulhu + shift
        if currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and not currPluginSetting['altkey']:
            shortcut = 'kb:cthulhu+shift+' + currPluginSetting['key']
        # alt + shift
        if currPluginSetting['shiftkey'] and not currPluginSetting['ctrlkey'] and currPluginSetting['altkey']:
            shortcut = 'kb:alt+shift+' + currPluginSetting['key']
        if shortcut != '':
            logger.debug(f"Registering shortcut: {shortcut}")
            currPluginSetting['shortcut'] = shortcut
            try:
                # Try to use the translation function, fall back to plain text if it fails
                plugin_name = _(currPluginSetting['pluginname'])
            except Exception:
                # If translation fails, use the original name
                plugin_name = currPluginSetting['pluginname']
                logger.warning(f"Translation failed for plugin: {currPluginSetting['pluginname']}")
                
            self.registerGestureByString(currPluginSetting['function'], plugin_name, shortcut)
        return currPluginSetting

    def id_generator(self, size=7, chars=string.ascii_letters):
        """Generate a random ID string."""
        return ''.join(random.choice(chars) for _ in range(size))

    def initSettings(self):
        """Initialize default settings for a plugin."""
        currPluginSetting = {
            'pluginname': '',
            'functionname': '',
            'key': '',
            'shiftkey': False,
            'ctrlkey': False,
            'altkey': False,
            'startnotify': False,
            'stopnotify': False,
            'blockcall': False,
            'error': False,
            'exec': False,
            'parameters': '',
            'function': None,
            'inputeventhandler': None,
            'valid': False,
            'supressoutput': False,
            'shortcut': ''
        }
        return currPluginSetting

    def getPluginSettings(self, filepath, currPluginSetting):
        """Parse plugin settings from filename and content."""
        try:
            currPluginSetting['file'] = filepath
            fileName, fileExtension = os.path.splitext(filepath)
            if (fileExtension and (fileExtension != '')):  # if there is an extension
                currPluginSetting['loadable'] = (fileExtension.lower() == '.py')  # only python is loadable
            filename = os.path.basename(filepath)  # filename
            filename = os.path.splitext(filename)[0]  # remove extension if we have one
            # remove pluginname seperated by __-__
            filenamehelper = filename.split('__-__')
            filename = filenamehelper[len(filenamehelper) - 1]
            currPluginSetting['permission'] = os.access(filepath, os.X_OK)
            currPluginSetting['pluginname'] = 'NoNameAvailable'
            if len(filenamehelper) == 2:
                currPluginSetting['pluginname'] = filenamehelper[0]
            # now get shortcuts seperated by __+__
            filenamehelper = filename.split('__+__')
            if len([y for y in filenamehelper if 'parameters_' in y.lower()]) == 1 and\
            len([y for y in filenamehelper if 'parameters_' in y.lower()][0]) > 11:
                currPluginSetting['parameters'] = [y for y in filenamehelper if 'parameters_' in y.lower()][0][11:]
            if len([y for y in filenamehelper if 'key_' in y.lower()]) == 1 and\
            len([y for y in filenamehelper if 'key_' in y.lower()][0]) > 4:
                currPluginSetting['key'] = [y for y in filenamehelper if 'key_' in y.lower()][0][4]
            if currPluginSetting['key'] == '':
                settcurrPluginSetting = 'shift' in map(str.lower, filenamehelper)
            currPluginSetting['ctrlkey'] = 'control' in map(str.lower, filenamehelper)
            currPluginSetting['altkey'] = 'alt' in map(str.lower, filenamehelper)
            currPluginSetting['startnotify'] = 'startnotify' in map(str.lower, filenamehelper)
            currPluginSetting['stopnotify'] = 'stopnotify' in map(str.lower, filenamehelper)
            currPluginSetting['blockcall'] = 'blockcall' in map(str.lower, filenamehelper)
            currPluginSetting['error'] = 'error' in map(str.lower, filenamehelper)
            currPluginSetting['supressoutput'] = 'supressoutput' in map(str.lower, filenamehelper)
            currPluginSetting['exec'] = 'exec' in map(str.lower, filenamehelper)    
            currPluginSetting['loadmodule'] = 'loadmodule' in map(str.lower, filenamehelper) 
            currPluginSetting = self.readSettingsFromPlugin(currPluginSetting)
            if not currPluginSetting['loadmodule']:
                if not currPluginSetting['permission']:  # subprocessing only works with exec permission
                    return self.initSettings()
            if currPluginSetting['loadmodule'] and not currPluginSetting['loadable']:  # sorry.. its not loadable only .py is loadable
                return self.initSettings()
            if (len(currPluginSetting['key']) > 1):  # no shortcut
                if not currPluginSetting['exec']:  # and no exec -> the plugin make no sense because it isnt hooked anywhere
                    return self.initSettings()  # so not load it (sets valid = False)
                else:
                    currPluginSetting['key'] = ''  # there is a strange key, but exec? ignore the key..
            currPluginSetting['valid'] = True  # we could load everything
            return currPluginSetting
        except Exception as e:
            logger.error(f"Error getting plugin settings: {e}")
            return self.initSettings()

    def readSettingsFromPlugin(self, currPluginSetting):
        """Read settings from plugin file content."""
        if not os.access(currPluginSetting['file'], os.R_OK):
            return currPluginSetting
        fileName, fileExtension = os.path.splitext(currPluginSetting['file'])
        if (fileExtension and (fileExtension != '')):  # if there is an extension
            if (fileExtension.lower() != '.py') and \
            (fileExtension.lower() != '.sh'):
                return currPluginSetting
        else:
            return currPluginSetting

        try:
            with open(currPluginSetting['file'], "r") as pluginFile:
                for line in pluginFile:
                    currPluginSetting['shiftkey'] = ('sopsproperty:shift' in line.lower().replace(" ", "")) or currPluginSetting['shiftkey']
                    currPluginSetting['ctrlkey'] = ('sopsproperty:control' in line.lower().replace(" ", "")) or currPluginSetting['ctrlkey']
                    currPluginSetting['altkey'] = ('sopsproperty:alt' in line.lower().replace(" ", "")) or currPluginSetting['altkey']
                    currPluginSetting['startnotify'] = ('sopsproperty:startnotify' in line.lower().replace(" ", "")) or currPluginSetting['startnotify']
                    currPluginSetting['stopnotify'] = ('sopsproperty:stopnotify' in line.lower().replace(" ", "")) or currPluginSetting['stopnotify']
                    currPluginSetting['blockcall'] = ('sopsproperty:blockcall' in line.lower().replace(" ", "")) or currPluginSetting['blockcall']
                    currPluginSetting['error'] = ('sopsproperty:error' in line.lower().replace(" ", "")) or currPluginSetting['error']
                    currPluginSetting['supressoutput'] = ('sopsproperty:supressoutput' in line.lower().replace(" ", "")) or currPluginSetting['supressoutput']
                    currPluginSetting['exec'] = ('sopsproperty:exec' in line.lower().replace(" ", "")) or currPluginSetting['exec']
                    currPluginSetting['loadmodule'] = ('sopsproperty:loadmodule' in line.lower().replace(" ", "")) or currPluginSetting['loadmodule']
        except Exception as e:
            logger.error(f"Error reading plugin file: {e}")
            
        return currPluginSetting

    def buildPluginSubprocess(self, currPluginSetting):
        """Build a function to execute a plugin as a subprocess."""
        currplugin = "\'\"" + currPluginSetting['file'] + "\" " + currPluginSetting['parameters'] + "\'"
        pluginname = currPluginSetting['pluginname']
        if currPluginSetting['blockcall']:
            pluginname = "blocking " + pluginname
        fun_body = "global " + currPluginSetting['functionname'] + "\n"
        fun_body += "def " + currPluginSetting['functionname'] + "(script=None, inputEvent=None):\n"
        if currPluginSetting['startnotify']:
            fun_body += "  outputMessage('start " + pluginname + "')\n"    
        fun_body += "  p = Popen(" + currplugin + ", stdout=PIPE, stderr=PIPE, shell=True)\n"
        fun_body += "  stdout, stderr = p.communicate()\n"
        fun_body += "  message = ''\n"
        fun_body += "  if not " + str(currPluginSetting['supressoutput']) + " and stdout:\n"
        fun_body += "    message += str(stdout, \"utf-8\")\n"
        fun_body += "  if " + str(currPluginSetting['error']) + " and stderr:\n"
        fun_body += "    message += ' error: ' + str(stderr, \"utf-8\")\n"
        fun_body += "  outputMessage(message)\n"
        if currPluginSetting['stopnotify']:
            fun_body += "  outputMessage('finish " + pluginname + "')\n"
        fun_body += "  return True\n\n"
        fun_body += "global " + currPluginSetting['functionname'] + "T\n"
        fun_body += "def " + currPluginSetting['functionname'] + "T(script=None, inputEvent=None):\n"
        fun_body += "  _thread.start_new_thread(" + currPluginSetting['functionname'] + ",(script, inputEvent))\n\n"
        return fun_body

    def buildPluginExec(self, currPluginSetting):
        """Build a function to execute a plugin as a Python module."""
        pluginname = currPluginSetting['pluginname']
        if currPluginSetting['blockcall']:
            pluginname = "blocking " + pluginname
        fun_body = "global " + currPluginSetting['functionname'] + "\n"
        fun_body += "def " + currPluginSetting['functionname'] + "(script=None, inputEvent=None):\n"
        if currPluginSetting['startnotify']:
            fun_body += "  outputMessage('start " + pluginname + "')\n"
        fun_body += "  try:\n"  
        fun_body += "    spec = importlib.util.spec_from_file_location(\"" + currPluginSetting['functionname'] + "\",\"" + currPluginSetting['file'] + "\")\n"
        fun_body += "    " + currPluginSetting['functionname'] + "Module = importlib.util.module_from_spec(spec)\n"
        fun_body += "    spec.loader.exec_module(" + currPluginSetting['functionname'] + "Module)\n"
        fun_body += "  except Exception as e:\n"
        fun_body += "    logger.error(f\"Error executing plugin {pluginname}: {e}\")\n"
        if currPluginSetting['error']:
            fun_body += "    outputMessage(\"Error while executing " + pluginname + "\")\n"
        if currPluginSetting['stopnotify']:
            fun_body += "  outputMessage('finish " + pluginname + "')\n"
        fun_body += "  return True\n\n"
        fun_body += "global " + currPluginSetting['functionname'] + "T\n"
        fun_body += "def " + currPluginSetting['functionname'] + "T(script=None, inputEvent=None):\n"
        fun_body += "  _thread.start_new_thread(" + currPluginSetting['functionname'] + ",(script, inputEvent))\n\n"
        return fun_body

    def getFunctionName(self, currPluginSetting):
        """Generate a unique function name for a plugin."""
        currPluginSetting['functionname'] = ''
        while currPluginSetting['functionname'] == '' or currPluginSetting['functionname'] + 'T' in globals() or currPluginSetting['functionname'] in globals():
            currPluginSetting['functionname'] = self.id_generator()
        return currPluginSetting

    def registerGestureByString(self, function, description, shortcut):
        """Register a keyboard shortcut for a function.
        
        This is a compatibility wrapper for the new plugin system.
        """
        try:
            if self.app:
                api_helper = self.app.getAPIHelper()
                if api_helper:
                    api_helper.registerGestureByString(
                        function, 
                        description, 
                        shortcut, 
                        'default', 
                        'cthulhu', 
                        True,
                        contextName=self.module_name
                    )
                    logger.debug(f"Registered shortcut {shortcut} for {description}")
                    return True
                else:
                    logger.error("Could not get APIHelper")
            else:
                logger.error("No app reference available")
        except Exception as e:
            logger.error(f"Error registering shortcut {shortcut}: {e}")
            
    def unregisterShortcut(self, function, shortcut):
        """Unregister a keyboard shortcut for a function.
        
        This is a compatibility wrapper for the new plugin system.
        """
        try:
            if self.app:
                api_helper = self.app.getAPIHelper()
                if api_helper and hasattr(api_helper, 'unregisterShortcut'):
                    api_helper.unregisterShortcut(shortcut)
                    logger.debug(f"Unregistered shortcut {shortcut}")
                    return True
                else:
                    logger.error("Could not get APIHelper or unregisterShortcut method")
            else:
                logger.error("No app reference available")
        except Exception as e:
            logger.error(f"Error unregistering shortcut {shortcut}: {e}")
    
    def load_plugins(self):
        """Load and setup all plugins in the plugin repository."""
        if not self.loaded:
            try:
                logger.info(f"Loading plugins from {self.plugin_repo}")
                plugin_files = glob.glob(self.plugin_repo + '*')
                self.plugin_list = []  # Reset the plugin list to avoid confusion
                
                for currplugin in plugin_files:
                    try:
                        # Ensure currplugin is a valid path string
                        if not isinstance(currplugin, (str, bytes, os.PathLike)):
                            logger.error(f"Invalid plugin path: {type(currplugin)}")
                            continue
                            
                        currPluginSetting = self.initSettings()
                        currPluginSetting = self.getPluginSettings(currplugin, currPluginSetting)

                        if not currPluginSetting['valid']:
                            logger.debug(f"Skipping invalid plugin: {currplugin}")
                            continue
                            
                        currPluginSetting = self.getFunctionName(currPluginSetting)
                        
                        if currPluginSetting['loadmodule']:
                            exec(self.buildPluginExec(currPluginSetting))  # load as python module
                        else:
                            exec(self.buildPluginSubprocess(currPluginSetting))  # run as subprocess
                            
                        if currPluginSetting['blockcall']:
                            currPluginSetting['function'] = globals()[currPluginSetting['functionname']]  # non threaded
                        else:
                            currPluginSetting['function'] = globals()[currPluginSetting['functionname'] + "T"]  # T = Threaded
                    
                        if currPluginSetting['exec']:  # exec on load if we want
                            currPluginSetting['function']()

                        if not currPluginSetting['key'] == '':
                            currPluginSetting = self.SetupShortcutAndHandle(currPluginSetting)
                            
                        logger.debug(f"Loaded plugin: {currPluginSetting['pluginname']}")
                        self.plugin_list.append(currPluginSetting)  # store in a list
                    except Exception as e:
                        logger.error(f"Error loading plugin {currplugin}: {e}")
                        
                self.loaded = True
                logger.info(f"Loaded {len(self.plugin_list)} plugins")
            except Exception as e:
                logger.error(f"Error in load_plugins: {e}")
