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

from cthulhu import plugin

from gi.repository import GObject, Peas
import glob
import os
import importlib.util
import random
import string
import _thread
from subprocess import Popen, PIPE

settings = None
speech = None
braille = None
input_event = None

def outputMessage( Message):
    if (settings.enableSpeech):
        speech.speak(Message)
    if (settings.enableBraille):
        braille.displayMessage(Message)

class SimplePluginSystem(GObject.Object, Peas.Activatable, plugin.Plugin):
    __gtype_name__ = 'SimplePluginSystem'
    object = GObject.Property(type=GObject.Object)

    def __init__(self):
        plugin.Plugin.__init__(self)
        self.plugin_list = []
        self.loaded = False
        self.plugin_repo = os.path.expanduser('~') + "/.local/share/cthulhu/simple-plugins-enabled/"

    def do_activate(self):
        API = self.object
        global settings
        global speech
        global braille
        global input_event
        settings = API.app.getDynamicApiManager().getAPI('Settings')
        speech = API.app.getDynamicApiManager().getAPI('Speech')
        braille = API.app.getDynamicApiManager().getAPI('Braille')
        input_event = API.app.getDynamicApiManager().getAPI('InputEvent')
        """Required method for plugins"""
        if not self.loaded:
            self.load_plugins()

    def do_deactivate(self):
        """Required method for plugins"""
        # Remove all registered keybindings
        for plugin in self.plugin_list:
            self.unregisterShortcut(plugin['function'], plugin['shortcut'])
        self.loaded = False
        self.plugin_list = []

    def SetupShortcutAndHandle(self, currPluginSetting):
        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 != '':
            print(shortcut)
            currPluginSetting['shortcut'] = shortcut
            self.registerGestureByString(currPluginSetting['function'], _(currPluginSetting['pluginname']), shortcut)
        return currPluginSetting

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

    def initSettings(self):
        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):
        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:
            return self.initSettings()

    def readSettingsFromPlugin(self, currPluginSetting):
        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

        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']
        return currPluginSetting

    def buildPluginSubprocess(self, currPluginSetting):
        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):
        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:\n"
        fun_body += "    pass\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):
        currPluginSetting['functionname'] = ''
        while currPluginSetting['functionname'] == '' or currPluginSetting['functionname'] + 'T' in globals() or currPluginSetting['functionname'] in globals():
            currPluginSetting['functionname'] = self.id_generator()
        return currPluginSetting

    def load_plugins(self):
        if not self.loaded:
            self.plugin_list = glob.glob(self.plugin_repo+'*')
            for currplugin in self.plugin_list:
                currPluginSetting = self.initSettings()
                currPluginSetting = self.getPluginSettings(currplugin, currPluginSetting)

                if not currPluginSetting['valid']:
                    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)
                print(currPluginSetting)
                self.plugin_list.append(currPluginSetting) # store in a list
            self.loaded = True
