#! /usr/bin/python3
# -*- coding: utf-8 -*-
# nautilus-scripts-manager
#
# nautilus-scripts-manager is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3.
#
# nautilus-scripts-manager 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 General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with nautilus-scripts-manager; if not, write to the Free Software Foundation,
# Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
#
# Copyright © 2011-2014 Pietro Battiston <me@pietrobattiston.it>

import os, sys, re
from gi.repository import Pango, Gtk, GLib
from optparse import OptionParser, OptionGroup

__version__ = '2.0'

########################## CONFIGURATION #######################################

not_installed_dir = os.path.dirname(os.path.realpath(__file__))
if os.path.exists(not_installed_dir + '/stuff/nautilus-scripts-manager.svg'):
    STUFF_DIR = not_installed_dir + '/stuff'
    LOCALE_DIR = not_installed_dir + '/locale'
else:
    for directory in [sys.prefix, sys.prefix + '/local']:
        installed_root_dir = directory + '/share'
        if os.path.exists(installed_root_dir + '/nautilus-scripts-manager/stuff'):
            STUFF_DIR = installed_root_dir + '/nautilus-scripts-manager/stuff'
            LOCALE_DIR = installed_root_dir + '/locale'
            break

SCRIPTS_OWN_FOLDER = os.path.join(GLib.get_user_data_dir(),
                                  'nautilus',
                                  'scripts')
if not os.path.exists(SCRIPTS_OWN_FOLDER):
    os.mkdir(SCRIPTS_OWN_FOLDER)

# DISTRIBUTORS: change that:
SCRIPTS_SYSTEM_FOLDER = "/usr/share/nautilus-scripts"

########################## END OF CONFIGURATION ################################


########################## LOCALIZATION ########################################

import locale
import gettext

APP = 'nautilus-scripts-manager'

gettext.install(APP, localedir=LOCALE_DIR)

# For Gtk.Builders:
locale.bindtextdomain(APP, LOCALE_DIR)

########################## END OF LOCALIZATION #################################

###################### ARGUMENTS PARSING #######################################

parser = OptionParser(usage="usage: %prog [options]\n(with no options, the graphic interface will start)", version="%prog " + str(__version__))

# Commands:
commands_group = OptionGroup(parser, "Commands",
                                    "(at most) one of this options must be used. If none is used, the graphic interface starts.")

commands_group.add_option("-e", "--enable", action="store", type="str", help="enable script ENABLE")
commands_group.add_option("-d", "--disable", action="store", type="str", help="disable script DISABLE")
commands_group.add_option("-l", "--list-enabled", action="store_true", help="list enabled scripts")
commands_group.add_option("-a", "--list-available", action="store_true", help="list available scripts")

parser.add_option_group(commands_group)

# Options:
parser.add_option("-p", "--position", action="store", type="str", default="", help="In conjunction with -e or -d (see below): establish the position of the script (can be just a name, or a path with slashes - quote it if it contains spaces).")

(options, args) = parser.parse_args()

commands = dict(zip(list(range(4)), ('enable', 'disable', 'list_enabled', 'list_available')))

choices = [getattr(options, command) for command in commands.values()]

if list(map(bool, choices)).count('True') > 1:
    parser.error(_('Please select at most one command.'))

###################### END OF ARGUMENTS PARSING ################################

try:
    from gi.repository import Gdk
    s = Gdk.Screen.get_default()
    assert(s)
    GRAPHIC = True
except AssertionError:
    GRAPHIC = False
    if not any(choices):
        parser.error( _("Graphic interface not available, please select a command.") )
        sys.exit()

def path_subpath(path):
    if path.startswith(SCRIPTS_SYSTEM_FOLDER):
        return os.path.relpath(path, SCRIPTS_SYSTEM_FOLDER)
    else:
        # Notice that subpaths are still unique - they start with "/" if
        # and only if they are outside SCRIPTS_SYSTEM_FOLDER (and they are
        # more practical than full paths).
        return path

class Script(object):
    """
    An installed script.
    """
    def __init__(self, path):
        self.path = path
        self.name = os.path.basename(path)
        self.subpath = path_subpath(path)
        self.stale = not os.path.exists(self.path)
        self.links = []

    def __repr__(self):
        links = ", ".join(map((lambda string : '"' + string + '"'), self.links))
        if not links:
            return blue(self.subpath)
        return _("%(path)s, linked as %(links)s") % {'path' : blue(self.subpath), 'links' : links}

class ScriptsManager():
    def __init__(self):
        self.load_scripts()
        
        if any(choices):
            choice = list(map(bool, choices)).index(True)
            actor = getattr(self, commands[choice])
            # print args
            actor(*args)
        else:
            self.gui()

    def gui(self):
        from nautilus_scripts_manager_ui import Ui
        
        self.ui = Ui(APP, os.path.join(STUFF_DIR, 'UI.glade'))
        
        cells = []
        
        def celldatafunction(column, cell, model, it, data):
            if model[it][3]:
                cell.set_property('sensitive', False)
                cell.set_property('style', Pango.Style.ITALIC)
            else:
                cell.set_property('sensitive', True)
                cell.set_property('style', Pango.Style.NORMAL)

        cells.append(Gtk.CellRendererToggle())
        activated = Gtk.TreeViewColumn(_('Active'), cells[-1])
        activated.add_attribute(cells[-1], 'active', 0)
        cells[-1].connect('toggled', self.change_active)
        
        cells.append(Gtk.CellRendererText())
        name = Gtk.TreeViewColumn(_('Script'), cells[-1])
        name.add_attribute(cells[-1], 'text', 1)
        name.set_cell_data_func(cells[-1], celldatafunction)
        
        cells.append(Gtk.CellRendererText())
        cells[-1].set_property('editable', True)
        position = Gtk.TreeViewColumn(_('Position'), cells[-1])
        position.add_attribute(cells[-1], 'text', 2)
        position.set_cell_data_func(cells[-1], celldatafunction)
        cells[-1].connect('edited', self.change_position)
                
        for col in [activated, name, position]:
            self.ui.list.append_column(col)

        self.populate_list()
    
        self.ui.note.set_text(_("All scripts must be installed in %s.")
                              % SCRIPTS_SYSTEM_FOLDER + "\n" +
                              _("To make changes effective, you may have to "
                                "close and restart Nautilus."))
    
        resp = self.ui.dialog.run()
        if resp == 1:
            self.ui.about.run()
            self.ui.about.hide()
            self.ui.dialog.run()

    def populate_list(self):
        self.ui.store.clear()
        
        for script in self.scripts.values():
            if not script.links:
                self.ui.store.append(None, [False, script.subpath, self.retrieve_default_path(script.name, script.path), False])
            elif script.links[1:]:
                root = self.ui.store.append(None, [True, script.subpath, '', script.stale])
                for link in script.links:
                    self.ui.store.insert(root, 1000, [True, script.subpath, link, script.stale])
            else:
                self.ui.store.append(None, [True, script.subpath, script.links[0], script.stale])

    def valid(self, position):
        # fixme? Is everything fine for a link name?
        return bool(position)
    
    def change_position(self, renderer, row, new_pos):
        selection_iter = self.ui.store.get_iter(row)
        active = self.ui.store.get_value(selection_iter, 0)
        script = self.ui.store.get_value(selection_iter, 1)
        position = self.ui.store.get_value(selection_iter, 2)
        
        if self.valid(new_pos) and position != new_pos:
            if new_pos in self.links:
                if self.links[new_pos] == script:
                    # The position is already used by _this_ script; apparently,
                    # it had more than one link. Let's fake everything's fine
                    # (notice the script is certainly active):
                    self.ui.store.set(selection_iter, 2, new_pos)
                    self.reload()
                    return
                
                # The position is used by another script.
                owner = self.links[new_pos]
                self.error(_("The position %(new_pos)s is already used by script %(owner)s.") % locals())
                return

            # The position is free (and valid).
            if active:
                # The old one must be deactivated.
                options.disable = script
                self.disable(position=position)
            
            # Activate the new one.
            options.enable = script
            self.enable(position=new_pos)

            self.ui.store.set(selection_iter, 0, True)
            
            self.ui.store.set(selection_iter, 2, new_pos)
                            
    
    def change_active(self, renderer, row, child=False):
        selection_iter = self.ui.store.get_iter(row)
        active, script, position, stale = self.ui.store[selection_iter]
#        print active, script, position, stale

        if stale and not child:
            resp = self.ui.stale_dialog.run()
            self.ui.stale_dialog.hide()
            if resp == -4:
                return
        
        if self.ui.store.iter_has_child(selection_iter):
            # Notice this is not necessarily active, it has redundant links, but
            # they may have been just disactivated.
            for i in range(self.ui.store.iter_n_children(selection_iter)):
                selection_path = self.ui.store.get_path(selection_iter)
                child_path = self.ui.store.get_path(self.ui.store.iter_nth_child(selection_iter, i))
                if self.ui.store[child_path][0] == active:
                    # Change the state of all children in the same state
                    self.change_active(renderer, child_path, True)
        elif active:
            options.disable = script
            self.disable(position=position)        
        else:
            # If it is not active, it can't be stale.
            options.enable = script
            self.enable(position=position)        

        self.ui.store.set(selection_iter, 0, not active)

        if stale:
            parent = self.ui.store.iter_parent(selection_iter)
            self.ui.store.remove(selection_iter)
            self.links.pop(position)
            if parent:
                if not self.ui.store.iter_has_child(parent):
                    self.ui.store.remove(parent)
                    self.scripts.pop(script)
            else:
                self.scripts.pop(script)
    
    def list_enabled(self):
        for script in self.scripts:
            if self.scripts[script].links:
                print(self.scripts[script])
    
    def list_available(self):
        for script in self.scripts:
            print(self.scripts[script])

    def enable(self, position=None):
        if options.enable.startswith('/'):
            full_dest = options.enable
        else:
            full_dest = os.path.join(SCRIPTS_SYSTEM_FOLDER, options.enable)
        
        if not os.path.exists(full_dest):
            print(_("Script %s not found.") % full_dest)
            sys.exit(2)

        if position:
            pass
        elif options.position:
            position = options.position
        else:
            # OK, so
            # - we aren't using a GUI (or we'd have "position" as argument)
            # - we didn't get a "position" argument via command line
            # In this case, if the script is already linked, don't try to link
            # it again:
            if self.scripts[options.enable].links:
                print(_("Script %(script_name)s is already linked from "
                        "%(link)s (use argument -p to add a link in a new "
                        "position).") %
                        {'script_name': options.enable,
                         'link': self.scripts[options.enable].links.__repr__()[1:-1]})
                sys.exit(6)

            position = self.retrieve_default_path(options.enable)

        full_src = os.path.join(SCRIPTS_OWN_FOLDER, position)
        
        # print full_src
        
        if os.path.exists(full_src):
            print(_("Position %s is already taken") % full_src)
            sys.exit(1)

        dirs = position.split('/')
        
        # print "dirs", dirs
        
        checked = ''
        
        while len(dirs)>1:
            checked = os.path.join(checked, dirs.pop(0))
            if not os.path.exists(os.path.join(SCRIPTS_OWN_FOLDER, checked)):
                os.mkdir(os.path.join(SCRIPTS_OWN_FOLDER, checked))
        
        print(full_src, '->', full_dest)
        try:
            os.symlink(full_dest, full_src)
        except OSError as m:
            self.error(_("The path %s already exists (and is not a link)!") % full_src)

    def error(self, msg):
        try:
            self.ui
            dialog = Gtk.MessageDialog(self.ui.dialog, type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, message_format=msg)
            dialog.set_title(_("Error"))
            dialog.run()
            dialog.destroy()
        except:
            print(bold(_("Error: ") + red(msg)))

    def warn(self, msg):
        # Those don't appear on the GUI.
        print(bold(_("Warning: ")) + red(msg))
    
    
    def disable(self, position=None):
        if position:
            pass
        elif options.position:
            position = options.position
        else:
            try:
                links = self.scripts[options.disable].links
                if not links:
                    print(_("Script %s is not enabled!") % options.disable)
                    sys.exit(7)
                    
                for a_position in self.scripts[options.disable].links:
                    # Recurse (remove _all_ links).
                    self.disable(a_position)
                return
            
            except KeyError:
                print(_("Script %s is unkown.") % options.disable)
                sys.exit(3)
        
        full_path = os.path.join(SCRIPTS_OWN_FOLDER, position)

        # At this point, we know "full_path" exists.
        if not os.path.islink(full_path):
            print(_("%s is not a link, I won't remove it") % full_path)
            sys.exit(5)
        
        os.unlink(full_path)
        print(_("Link %s removed.") % full_path)

        # If in the path there were folders now useless, delete them:
        dirname = os.path.dirname(full_path)
        
        while not os.listdir(dirname) and not SCRIPTS_OWN_FOLDER.startswith(os.path.realpath(dirname)):
            os.rmdir(dirname)
            dirname = os.path.dirname(dirname)
    
    def load_scripts(self):
        """Find all system-wise installed nautilus scripts.
        """
        self.scripts = {}
                
        self.load_scripts_from_folder()
        
        self.load_links()
    
    def load_scripts_from_folder(self, subdir=''):
        
        fullpath = os.path.join(SCRIPTS_SYSTEM_FOLDER, subdir)
    
        if not os.path.isdir(fullpath):
            return
        
        print("load from", fullpath)
        for script_name in os.listdir(fullpath):
            script_path = os.path.join(fullpath,
                                       script_name)
            # Check if dir:
            if os.path.isdir( script_path ):
                # Yes, recurse:
                self.load_scripts_from_folder(os.path.join(subdir,
                                                           script_name))
                continue
            # Check if executable:
            if os.access(script_path, os.X_OK):
                script = Script(script_path)
                self.scripts[script.subpath] = script

    def load_links(self, folder=SCRIPTS_OWN_FOLDER):
        """Find all scripts instances of scripts installed by the user.
        """
#        print "scanning", folder

        for script_link in os.listdir(folder):
            link_path = os.path.join(folder, script_link)
            if not os.path.islink(link_path):
                # Mmh... something else
                if os.path.isdir(link_path):
                    # Recurse
                    self.load_links(os.path.join(folder, script_link))
                continue

            link_target = os.readlink(link_path)
            position = link_path[len(SCRIPTS_OWN_FOLDER)+1:]
            
            subpath = path_subpath(link_target)
            
            if subpath in self.scripts:
                script = self.scripts[subpath]
            else:
                scripts_folder = SCRIPTS_SYSTEM_FOLDER
                # The script was not detected by the previous call to
                # load_scripts_from_folder()
                self.warn(_("target %(link_target)s of link %(link_path)s is "
                            "missing or outside %(scripts_folder)s.")
                            % locals())
                script = Script(link_target)
                self.scripts[script.subpath] = script
            
            script.links.append(position)
        
        self.links = {}
        for script in self.scripts:
            for link in self.scripts[script].links:
                self.links[link] = script

    def retrieve_default_path(self, name, path=''):
        """
        The default path is basically the script name:
        - translated
        - with spaces
        - possibly with a path containing subfolders for better organization.
        
        The default path is searched in the following ways (in order):
        - translated version provided by the script
        - translated version provided in default_paths_dict
        - English version provided by the script
        - English version provided in default_paths_dict
        - name of the script where every instance of "xY" is replaced by "x Y",
          every "_" with " " and extension - if existing - is removed.
        """
        import locale
        
        try:
            lang = locale.getdefaultlocale()[0].split('_')[0]
        except:
            lang = locale.getdefaultlocale()[0]
                
        try:
            self.re_name_en
        except:
            # ... so that this is done only once per script execution:
            self.re_name_en = re.compile("#* *Name=")
            self.re_name_loc = re.compile("#* *Name\[%s\]=" % lang)
        
        if not path:
            path = os.path.join(SCRIPTS_SYSTEM_FOLDER, name)
        script_file = open( path )
        
        while True:
            try:
                line = script_file.readline()
            except UnicodeDecodeError:
                # This is probably a binary file!
                break
            if not line:
                break
            match = self.re_name_loc.match(line)
            if match:
                script_provided_position_loc = line[match.end():].strip()
                # Who can ask for anything more?
                script_file.close()
                return script_provided_position_loc
            match = self.re_name_en.match(line)
            if match:
                script_provided_position_en = line[match.end():].strip()

        script_file.close()
        # No authoritative localization. Authoritative english version?
        try:
            return script_provided_position_en
        except NameError:
            pass

        # No authoritative name. Hardcoded localization?
        if name in paths:
            if lang in paths[name]:
                return paths[name][lang]
        # No. Authoritative English position?
        try:
            return script_provided_path_en
        except:
            # No. Hardcoded English position?
            if name in paths:
                if 'en' in paths[name]:
                    return paths[name]['en']
            
            # No. Just manipulate the filename:
            previous = ''
            new_name = ''
            
            relpath = os.path.join(path[len(SCRIPTS_SYSTEM_FOLDER):])
            # Remove extension
            parts = name.rpartition('.')
            if parts[0] and parts[2]:
                relpath = relpath[:-len(parts[2])-1]
            name = relpath
            # Remove underscores
            name = name.replace('_', ' ')

            # Uncamelcasize
            for letter in name:
                if previous and previous.islower() and letter.isupper():
                    new_name += ' '
                new_name += letter
                previous = letter

            return new_name
    
    def reload(self):
        # The only tricky thing is that the user may have changed a position and
        # then deactivated the line: we must remember this position through the
        # reload.
        positions = {}
        for row in range(len(self.ui.store)):
            selection_iter = self.ui.store.get_iter(row)
            script = self.ui.store.get_value(selection_iter, 1)
            position = self.ui.store.get_value(selection_iter, 2)
            
            positions[script] = position
        
        self.load_scripts()
        self.populate_list()
        
        for row in range(len(self.ui.store)):
            selection_iter = self.ui.store.get_iter(row)
            active = self.ui.store.get_value(selection_iter, 0)

            if not active:
                script = self.ui.store.get_value(selection_iter, 1)
                position = self.ui.store.get_value(selection_iter, 2)
                self.ui.store.set(selection_iter, 2, position)

            

# WARNING: the best thing is to insert translated string into the script itself,
# in this way (the "#" too!):
#
#Name=Name of the script in English
#Name[it]=Nome dello script in italiano
#Name[fr]=Nom du script en francais
#
# ... and so on. Only if it is not possible (or while you wait for it to happen)
# you can them to me and I'll insert them in the following dictionary.

paths = {
        'ConvertAudioFile': {'en': 'Audio files converter',
                             'it': 'Convertitore di file audio'}
        }

def bold(string):
    return "\033[1m%s\033[0m" % string        

def red(string):
    return "\033[22;31m%s\033[0m" % string
    
def blue(string):
    return "\033[01;34m%s\033[0m" % string
    

if __name__ == '__main__':
    SM = ScriptsManager()


