'''
PyMOL Plugins Engine

(c) 2011-2012 Thomas Holder, PyMOL OS Fellow
License: BSD-2-Clause

'''

import os
import sys
import pymol
from pymol import cmd
from .legacysupport import *

# variables

PYMOLPLUGINSRC = os.path.expanduser('~/.pymolpluginsrc.py')

preferences = {
    'verbose': False,
    'instantsave': True,
}

autoload = {}

plugins = {}

# API functions

def is_verbose(debug=0):
    verbose = pref_get('verbose')
    if debug and verbose < 0:
        return True
    return verbose and pymol.invocation.options.show_splash

def get_startup_path(useronly=False):
    if useronly:
        # assume last item is always from installation directory
        return startup.__path__[:-1]
    return startup.__path__

def set_startup_path(p, autosave=True):
    if isinstance(p, list):
        startup.__path__[:-1] = p
        if autosave:
            set_pref_changed()
    else:
        print ' Error: set_startup_path failed'

def pref_set(k, v):
    preferences[k] = v
    set_pref_changed()

def pref_get(k, d=None):
    return preferences.get(k, d)

def pref_save(filename=PYMOLPLUGINSRC, quiet=1):
    import pprint
    repr = pprint.pformat

    try:
        f = open(cmd.exp_path(filename), 'w')
    except IOError:
        print ' Plugin-Error: Cannot write Plugins resource file to', filename
        return

    print >> f, '# AUTOGENERATED FILE'
    print >> f, 'try:'
    print >> f, '  import', __name__
    print >> f, '  ' + __name__ + '.autoload =', repr(autoload)
    print >> f, '  ' + __name__ + '.preferences =', repr(preferences)
    print >> f, '  ' + __name__ + '.set_startup_path(', repr(get_startup_path(True)), ', False)'
    print >> f, 'except:'
    print >> f, '  import os'
    print >> f, '  print "Error while loading " + os.path.abspath(__script__)'
    f.close()

    if not int(quiet):
        print ' Plugin settings saved!'

def set_pref_changed():
    if pref_get('instantsave', True):
        verbose = pref_get('verbose', False)
        pref_save(quiet=not verbose)

def addmenuitem(label, command=None, menuName='Plugin'):
    '''
    Generic replacement for MegaWidgets menu item adding
    '''
    labels1 = [menuName] + label.split('|')
    labels2 = ['|'.join(labels1[0:i]) for i in range(1, len(labels1))]
    pmgapp = get_pmgapp()
    if pmgapp is not None:
        for i in range(1, len(labels2)):
            try:
                pmgapp.menuBar.addcascademenu(labels2[i-1], labels2[i], label=labels1[i])
            except ValueError:
                pass
        if labels1[-1] == '-':
            pmgapp.menuBar.addmenuitem(labels2[-1], 'separator')
        else:
            pmgapp.menuBar.addmenuitem(labels2[-1], 'command', label=labels1[-1],
                    command=command)

def plugin_load(name, quiet=1):
    '''
DESCRIPTION

    Load plugin from command line.
    '''
    if len(plugins) == 0:
        initialize(-2)
    if name not in plugins:
        print ' Error: no such plugin'
        return
    info = plugins[name]
    if info.loaded:
        if not int(quiet):
            print ' info: plugin already loaded'
        return
    info.load()

# helper functions and classes

class PluginInfo(object):
    '''
    Hold all information about a plugin.

    A instance with mod_name=None is considered a temporary instance which
    cannot be loaded (during installation, for extraction of metadata, ...)
    '''
    def __init__(self, name, filename, mod_name=None):
        self.name = name
        self.mod_name = mod_name
        self.filename = filename

        # set on loading
        self.loadtime = None
        self.commands = []

        # register
        if not self.is_temporary:
            plugins[name] = self

    def __repr__(self):
        return '%s(%s)' % (self.__class__.__name__, self.name)

    @property
    def autoload(self):
        return autoload.get(self.name, True)

    @autoload.setter
    def autoload(self, value):
        autoload[self.name] = bool(value)
        set_pref_changed()

    @property
    def module(self):
        return sys.modules.get(self.mod_name)

    @property
    def loaded(self):
        return self.loadtime is not None

    @property
    def is_temporary(self):
        return self.mod_name is None

    def get_metadata(self):
        '''
        Parse plugin file for metadata (hash-commented block at beginning of file).
        '''
        metadata = dict()
        f = open(self.filename)
        for line in f:
            if line.strip() == '':
                continue
            if not line.startswith('#'):
                break
            if ':' in line:
                key, value = line[1:].split(':', 1)
                metadata[key.strip()] = value.strip()
        f.close()
        self.get_metadata = lambda: metadata
        return metadata

    def get_version(self):
        '''
        Get version as string. If no version available, return empty string.

        Preferred way to specify version is metadata "Version" field.
        '''
        v = self.get_metadata().get('Version', '')
        if not v and self.loaded:
            v = getattr(self.module, '__version__', '')
        return str(v)

    def get_citation_required(self):
        '''
        Return True if Citation-Required: Yes
        '''
        v = self.get_metadata().get('Citation-Required', 'No')
        return v.lower() == 'yes'

    def get_docstring(self):
        '''
        Get docstring either from loaded module, or try to parse first python
        statement from file, without executing any code.
        '''
        if self.loaded:
            return self.module.__doc__

        try:
            c = compile(''.join(open(self.filename)), 'x', 'single')
            s = c.co_consts[0]
            if isinstance(s, basestring):
                return s
        except:
            pass

    def load(self, pmgapp=None, force=0):
        '''
        Load and initialize plugin.

        If pmgapp == -1, do not initialize.
        '''
        import time

        assert not self.is_temporary

        starttime = time.time()
        if pmgapp is None:
            pmgapp = get_pmgapp()

        verbose = pref_get('verbose', False)

        try:
            # overload cmd.extend to register commands
            extend_orig = cmd.extend
            def extend_overload(a, b=None):
                self.commands.append(a if b else a.__name__)
                return extend_orig(a, b)
            cmd.extend = extend_overload

            # do not use self.loaded here
            if force and self.module is not None:
                reload(self.module)
            else:
                __import__(self.mod_name, level=0)

            if pmgapp != -1:
                self.legacyinit(pmgapp)

            cmd.extend = extend_orig

            self.loadtime = time.time() - starttime
            if verbose and pymol.invocation.options.show_splash:
                print ' Plugin "%s" loaded in %.2f seconds' % (self.name, self.loadtime)
        except:
            if verbose:
                import traceback
                traceback.print_exc()
            else:
                print sys.exc_info()[1]
            print "Unable to initialize plugin '%s' (%s)." % (self.name, self.mod_name)
            return False

        return True

    def legacyinit(self, pmgapp):
        '''
        Call the __init__ or __init_plugin__ function which takes the PMGApp
        instance as argument (usually adds menu items).

        This must be called after loading and after launching of the external
        GUI (PMGApp).

        Ignore the build-in <method-wrapper '__init__'> that takes a
        string as first argument. Only call __init__ if it's of type
        'function'.
        '''
        import types

        mod = self.module
        if mod is None:
            raise RuntimeError('not loaded')

        if hasattr(mod, '__init_plugin__'):
            mod.__init_plugin__(pmgapp)
        elif hasattr(mod, '__init__'):
            if isinstance(mod.__init__, types.FunctionType):
                mod.__init__(pmgapp)

    def uninstall(self, parent=None):
        '''
        Remove a plugin

        Removes the complete directory tree in case of a package.
        '''
        from .legacysupport import tkMessageBox

        if parent is None:
            parent = get_tk_focused()

        ok = tkMessageBox.askyesno('Confirm',
                'Do you really want to uninstall plugin "%s"' % (self.name), parent=parent)
        if not ok:
            return False

        showinfo = tkMessageBox.showinfo
        dirname, basename = os.path.split(self.filename)

        try:
            if basename == '__init__.py':
                import shutil
                shutil.rmtree(dirname)
            else:
                for suffix in ['', 'o', 'c']:
                    filename = self.filename + suffix
                    if os.path.exists(filename):
                        os.remove(filename)
        except OSError:
            showinfo('Error', 'Could not delete files for plugin "%s".' % (self.name), parent=parent)
            return False

        plugins.pop(self.name, None)
        autoload.pop(self.name, None)
        set_pref_changed()

        showinfo('Info', 'Plugin "%s" successfully removed. Please restart PyMOL.' % (self.name), parent=parent)
        return True

def findPlugins(paths):
    '''
    Find all python modules (extension .py and directories with __init__.py)
    inside a list of directories.

    Returns a dictionary with names to filenames mapping.
    '''
    import time
    start = time.time()

    verbose = pref_get('verbose', False)

    modules = dict()

    for path in paths:
        if not os.path.isdir(path):
            continue

        for filename in os.listdir(path):
            # ignore names that start with dot or underscore
            if filename[0] in ['.', '_']:
                continue

            if '.' in filename:
                name, _, ext = filename.partition('.')
                if ext == 'py':
                    if name not in modules:
                        modules[name] = os.path.join(path, filename)
                    elif verbose:
                        print ' warning: multiple plugins named', name
            else:
                name, filename = filename, os.path.join(path, filename, '__init__.py')
                if os.path.exists(filename):
                    if name not in modules:
                        modules[name] = filename
                    elif verbose:
                        print ' warning: multiple plugins named', name

    if verbose:
        print ' Scanning for modules took %.4f seconds' % (time.time() - start)
    return modules

def initialize(pmgapp=-1):
    '''
    Searches for plugins and registers them.

    pmgapp == -2: No Autoloading
    pmgapp == -1: Autoloading but no legacyinit
    else:         Autoloading and legacyinit
    '''

    # check for obsolete development version of pymolplugins
    if 'pymolplugins' in sys.modules:
        from .legacysupport import tkMessageBox
        tkMessageBox.showwarning('WARNING',
                '"pymolplugins" now integrated into PyMOL as "pymol.plugins"! '
                'Please remove the old pymolplugins module and delete ~/.pymolrc_plugins.py')

    if os.path.exists(PYMOLPLUGINSRC):
        from pymol import parsing
        parsing.run_file(PYMOLPLUGINSRC, {'__script__': PYMOLPLUGINSRC}, {})

    autoload = (pmgapp != -2)
    for parent in [startup]:
        modules = findPlugins(parent.__path__)

        for name, filename in modules.iteritems():
            mod_name = parent.__name__ + '.' + name
            info = PluginInfo(name, filename, mod_name)
            if autoload and info.autoload:
                info.load(pmgapp)

# pymol commands
cmd.extend('plugin_load', plugin_load)
cmd.extend('plugin_pref_save', pref_save)

# autocompletion
cmd.auto_arg[0]['plugin_load'] = [ lambda: cmd.Shortcut(plugins), 'plugin', ''  ]

# vi:expandtab:smarttab:sw=4
