#! /usr/bin/python
# 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 (C) 2009 Pietro Battiston <toobaz@email.it>

import os, sys, re, exceptions
import pango
from optparse import OptionParser, OptionGroup

__version__ = '1.5'

########################## 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.expanduser('~/.gnome2/nautilus-scripts/')
if not os.path.exists(SCRIPTS_OWN_FOLDER):
    os.mkdir(SCRIPTS_OWN_FOLDER)

# DISTRIBUTORS: change that (don't forget the slash):
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, unicode=True)

# 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(range(4), ('enable', 'disable', 'list_enabled', 'list_available')))

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

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

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

try:
    import gtk
    GRAPHIC = True
except:
    GRAPHIC = False
    if not any(choices):
        parser.error( _("Graphic interface not available, please select a command.") )
        sys.exit()



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

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

class ScriptsManager():
    def __init__(self):
        self.load_scripts()
        print
        if any(choices):
            choice = 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, STUFF_DIR + '/UI.glade')
        
        cells = []
        
        def celldatafunction(column, cell, model, it):
            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.name, self.retrieve_default_path(script.name), False])
            elif script.links[1:]:
                root = self.ui.store.append(None, [True, script.name, '', script.stale])
                for link in script.links:
                    self.ui.store.insert(root, 1000, [True, script.name, link, script.stale])
            else:
                self.ui.store.append(None, [True, script.name, 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 script

    def enable(self, position=None):
        full_dest = 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 = 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 += '/' + dirs.pop(0)
            if not os.path.exists(SCRIPTS_OWN_FOLDER + checked):
                os.mkdir(SCRIPTS_OWN_FOLDER + checked)
        
        print full_src, '->', full_dest
        try:
            os.symlink(full_dest, full_src)
        except exceptions.OsError, 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.MESSAGE_ERROR, buttons=gtk.BUTTONS_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 = 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 = {}
        
        if not os.path.exists(SCRIPTS_SYSTEM_FOLDER):
            self.error(_("Directory %s doesn't exist, can't continue.") % SCRIPTS_SYSTEM_FOLDER)
        
        for script_name in os.listdir(SCRIPTS_SYSTEM_FOLDER):
            script_path = SCRIPTS_SYSTEM_FOLDER + script_name
            # Check if executable:
            if os.access(script_path, os.X_OK):
               self.scripts[script_name] = Script(script_name)
        
        self.load_links()

    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 = folder + script_link
            if not os.path.islink(link_path):
                # Mmh... something else
                if os.path.isdir(link_path):
                    # Recurse
                    self.load_links(folder + script_link + '/')
                continue

            link_target = os.readlink(link_path)
            name = os.path.basename(link_target)
            position = link_path[len(SCRIPTS_OWN_FOLDER):]
            
            if not name in self.scripts:
                scripts_folder = SCRIPTS_SYSTEM_FOLDER
                self.warn(_("target %(link_target)s of link %(link_path)s is missing or outside %(scripts_folder)s.") % locals())
                self.scripts[name] = Script(name)
            
            script = self.scripts[name]
            
            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):
        """
        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)
        
        script_file = open(SCRIPTS_SYSTEM_FOLDER + name)
        
        while True:
            line = script_file.readline()
            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 = ''
            
            # Remove extension
            parts = name.rpartition('.')
            if parts[0]:
                name = parts[0]
            # 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()


