# -*- coding: utf-8 -*-
# Elisa - Home multimedia server
# Copyright (C) 2007 Fluendo Embedded S.L. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 2.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Elisa with Fluendo's plugins.
#
# The GPL part of Elisa is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Elisa" in the root directory of this distribution package
# for details on that license.

"""
Elisa Plugin Manager
"""

__maintainer__ = 'Guido Amoruso <guidonte@fluendo.com>'


from distutils.version import LooseVersion
import urlparse
import pprint
import cPickle
import copy
import tempfile
import shutil
import os
import sys

from setuptools.command import easy_install
from setuptools.command.easy_install import samefile
from distutils.dist import Distribution

from twisted.internet import defer
from twisted.internet import threads

from elisa.core.application import CONFIG_DIR, CONFIG_FILE
from elisa.core.plugin_registry import PluginRegistry
from elisa.core.config import Config
from elisa.extern.epr.egg_parser import EggParser
from elisa.core.log import Loggable

from elisa.core.epm.egg_plugin import EggPlugin
from elisa.core.epm.egg_repository import create_repository_from_source
from elisa.core.epm.exceptions import *

# HACK
ELISA_PLUGINS_DIR = os.path.join(os.path.expanduser('~'), '.elisa', 'plugins')
ELISA_CORE_DIR = os.path.join(os.path.expanduser('~'), '.elisa', 'core')

# FIXME: load from config
DEFAULT_REPOSITORIES = ["http://elisa-plugins.fluendo.com/main/xmlrpc"]
#DEFAULT_REPOSITORIES = ['http://localhost:8000/xmlrpc']


class EggRegistry(Loggable):
    """
    Elisa egg registry.

    Knows about static repositories and discovers dynamic repositories,
    update the local cache and answers to queries about plugins: installed ones,
    uninstalled, upgradeables, dependencies.
    """
    
    def __init__(self, cache_file=None, static_sources=[], plugin_dirs=None):
        """
        Initialize repositories and set the local cache file.

        Loads the statically configured repositories and discovers the dynamic
        ones actually present at this time. Set the user's local cache file
        location, creating it if doesn't exist, using a sensible default in the
        Elisa user directory.

        @param cache_file: the absolute path to the user's local cache file.
        @type chache_file: a string
        @param static_sources: an alternative sources' list
        @type static_sources: list of strings
        @param plugin_dirs: the directory where to search for installed plugins
        @type plugin_dirs: list of strings
        """
        Loggable.__init__(self)
        if not cache_file:
            cache_file = os.path.join(ELISA_PLUGINS_DIR, 'cache.db')

        self.cache_file = cache_file
        self.plugin_dirs = plugin_dirs

        self.cached_plugin_list = []

        # create the cache file if doesn't exist
        if not os.path.exists(self.cache_file):
            self.debug('Cache file "%s" does not exist. Creating it.' \
                       % (self.cache_file))
            open(self.cache_file, 'wb').close()

        self.static_repositories = []
        for source in static_sources:
            repo = create_repository_from_source(source=source)
            self.static_repositories.append(repo)
        if not self.static_repositories:
            self._load_static_repositories()

        self.dynamic_repositories = []
        self._discover_dynamic_repositories()

        config_file = os.path.join(CONFIG_DIR, CONFIG_FILE)
        self.plugin_registry = PluginRegistry(Config(config_file))
        self.plugin_registry.load_plugins(self.plugin_dirs)

    def _load_static_repositories(self):
        # FIXME: get static repositories' sources from config file.
        for source in DEFAULT_REPOSITORIES:
            repository = create_repository_from_source(source=source)
            self.static_repositories.append (repository)

    def _discover_dynamic_repositories(self):
        """Discover actually existent dynamic repositories."""
        pass

    def get_repositories(self):
        """Get the list of known static and dynamic repositories.

        @return: the list of repositories
        @rtype:  list of L{epm.egg_repository.EggRepository}
        """
        return self.static_repositories + self.dynamic_repositories

    def _blocking_list_plugins(self, source=None):
        """Load plugins from the cache and from dynamic repositories.

        @param source: the source of the repository, to filter plugins' origin
        @type source:  string
        @return:       the discovered plugins
        @rtype:        list of L{epm.egg_plugin.EggPlugin}s
        """

        plugins = []
        cache = open(self.cache_file, 'rb')

        try:
            plugins = cPickle.load(cache)
        except Exception, e:
            self.warning('Cannot load plugins from cache: ' + str(e))

        cache.close()

        dynamic_repositories = self.dynamic_repositories
        if source:
            dynamic_repositories = filter (lambda r: r.source == source,
                                           dynamic_repositories)

        for repository in dynamic_repositories:
            plugins += repository.get_plugins()

        if source and not dynamic_repositories:
            plugins = filter (lambda p: p.repository and (p.repository.source == source),
                              plugins)

        self.cached_plugin_list = plugins

        return plugins

    def list_plugins(self, source=None):
        return threads.deferToThread(self._blocking_list_plugins, (source))

    def update_cache(self):
        """Cache locally information about plugins from static repositories.

        Try to contact all known static repositories: if a problem occurs with
        one of those, just retrieve that information from the local cache, in
        order not to clean the cache just for a missing Internet connection (for
        example.)

        @return: a deferred trigged when all work is done
        @rtype: L{twisted.internet.defer.Defer}
        """

        handle, tmpname = tempfile.mkstemp()
        cache = open(tmpname, 'wb')
        plugins = []
        dfr_plugins = []

        def add_plugins(plugin_set, plugins, repository):
            self.debug("Got %s plugins set from %r", len(plugin_set),
                       repository)
            plugins += plugin_set
            return plugin_set

        # FIXME: check if this is correct. I think that if I return a deferred
        # from and errback, the calling chain continue through the callback
        # chain: is this true?
        def add_plugins_error(err, plugins, repository):
            self.debug("Got an error: %r", err)
            self.debug("Loading plugins for '%s' from local cache" \
                       % (repository.source))
            #plugins += self._blocking_list_plugins(source=repository.source)
            dfr = self.list_plugins(source=repository.source)
            dfr.addCallback(add_plugins, plugins, repository)
            return dfr

        for repository in self.static_repositories:
            dfr = repository.get_plugins()

            dfr.addCallback(add_plugins, plugins, repository)
            dfr.addErrback(add_plugins_error, plugins, repository)

            dfr_plugins.append(dfr)

        def write_cache(res):
            self.debug("Writing %r plugins to local cache", len(plugins))
            cPickle.dump(plugins, cache)
            cache.close()
            shutil.copyfile(tmpname, self.cache_file)

            return res

        dfr_list = defer.DeferredList(dfr_plugins)
        dfr_list.addCallback(write_cache)

        return dfr_list

    #FIXME: maybe rename this "get_repository_plugin", as the registry only
    #       search for repository plugins, not for installed ones.
    def get_plugin(self, name, version=None, source=None):
        """
        Get the plugin by name, version and repository source.

        The information about the plugin is retrieved through
        "self.cached_plugin_list", if possible. Otherwise we launch
        "self._blocking_list_plugins()", even if the first time can block...

        If the version is not specified the plugin with gratest version number
        should be reported; if more than one with the same version number is
        found, the first in the list is returned.

        @param name:    the name of the plugin
        @type name:     string
        @param version: the version of the plugin. None to get the latest one
        @type version:  string
        @param source:  the source URL of the repository
        @type source:   string
        @return:        the plugin, if found in the repositories. None,
                        otherwise
        @rtype:         L{epm.egg_plugin.EggPlugin}
        """

        #plugins = filter (lambda p: p.name == name, self.list_plugins())
        plugin_list = self.cached_plugin_list or self._blocking_list_plugins()
        plugins = filter (lambda p: p.name == name, self.cached_plugin_list)

        if version:
            plugins = filter (lambda p: p.version == version, plugins)
        if source:
            plugins = filter (lambda p: p.repository.source == source, plugins)

        if len(plugins) == 0:
            return None

        # FIXME: checkme!
        def cmp_by_looseversion(p, q):
            p_version = LooseVersion(p.version or '0')
            q_version = LooseVersion(q.version or '0')
            if p_version > q_version:
                return -1
            return 1

        plugins.sort(cmp=cmp_by_looseversion)

        return plugins[0]

    def get_plugin_deps_for(self, plugin_name, plugin_version=None):
        """Retrieve the dependencies for a plugin.

        Search the dependencies graph, assuming that there are no circular
        dependencies.

        @param plugin_name: the name of the plugin
        @type plugin_name: string
        @param plugin_version: the version of the plugin
        @type plugin_version: string
        @return: the list of dependencies
        @rtype: a list of dicts
        """

        plugin = self.get_plugin(name=plugin_name, version=plugin_version)
        if not plugin:
            return []

        to_check = plugin.get_plugin_deps()
        deps = copy.copy(to_check)

        for dep in to_check:
            deps += self.get_plugin_deps_for(plugin_name=dep['name'],
                                             plugin_version=dep['version'])

        return deps

    def get_uninstalled_plugin_deps_for(self,
                                        plugin_name,
                                        plugin_version=None,
                                        installed_plugins=[]):
        """Retrieve the uninstalled dependencies for a plugin.

        Also report the dependencies needing an upgrade.

        Search the dependencies graph, pruning trees when a dependence is known
        to be already installed, because we assume that is has already its
        dependecies satisfied. Assume that there are no circular dependencies.

        @param plugin_name: the name of the plugin
        @type plugin_name: string
        @param plugin_version: the version of the plugin
        @type plugin_name: string
        @param installed_plugins: a cached list of installed plugins
        @type installed_plugins: list of strings
        @return: the list of uninstalled dependecies
        @rtype: a list of dicts
        """

        if not installed_plugins:
            installed_plugins = self.get_installed_plugins()

        for i in installed_plugins:
            if plugin_name == i.name:
                if not plugin_version:
                    return []
                if LooseVersion(plugin_version) == LooseVersion(i.version):
                    return []

        plugin = self.get_plugin(name=plugin_name, version=plugin_version)
        if not plugin:
            return []

        installed_names = map(lambda p: p.name, installed_plugins)
        def check_dep(dep):
            if not dep['name'] in installed_names:
                return True
            dep_version = dep['version'] or '0'
            plugin_version = plugin.version or '0'
            if LooseVersion(dep_version) >= LooseVersion(plugin_version):
                return True
            return False

        to_check = filter(check_dep, plugin.get_plugin_deps())
        missing = copy.copy(to_check)

        for dep in to_check:
            missing += self.get_uninstalled_plugin_deps_for(plugin_name=dep['name'],
                                                            plugin_version=dep['version'],
                                                            installed_plugins=installed_plugins)

        # a bit hackish... remove duplicates from the list.
        missing = map(lambda i: dict(i),
                      list(set(map(lambda m: tuple(m.items()),
                      missing))))

        return missing

    def get_installed_plugins(self):
        """Retrieve the list of installed plugins.

        Can filter by plugin name, returning at most one plugin.

        @param plugin_name: the optional plugin name to search for
        @type plugin_name: string
        @return: the list of installed plugins
        @rtype: list of L{epm.egg_plugin.EggPlugin}s
        """

        # TODO: check whether self.plugin_registry is correctly update when
        # new plugins are insalled
        plugins = []
        ##self.plugin_registry.load_plugins()
        for name, klass in self.plugin_registry.plugin_classes.iteritems():
            plugins.append(EggPlugin(plugin_class=klass))

        return plugins

    def get_upgradeable_plugins(self, update_states=[]):
        """Get the list of upgradeable plugins.

        If a plugin is no more in the repository, sure it is not upgradeable.

        @param update_states: the update states you want to filter on
        @type update_states: list of string
        @return: the list of upgradeable plugins
        @rtype:  a list of L{epm.egg_plugin.EggPlugin}
        """

        upgradeables = []
        for p in self.get_installed_plugins():
            # get plugin from prepository
            plugin = self.get_plugin(name=p.name)
            if plugin \
               and (LooseVersion(plugin.version) > LooseVersion(p.version)):
                #upgradeables.append(plugin)
                p.new_version = plugin.version
                p.update_state_id = plugin.update_state_id
                upgradeables.append(p)

        if update_states:
            upgradeables = filter(lambda p: p.update_state_id in update_states,
                                  upgradeables)

        return upgradeables

    # FIXME: maybe rename get_installed_dependent_plugins_for
    def get_installed_dependent_plugins(self, plugin_name, installed_plugins=[]):
        """Retrieve the plugins that depend on the specified one.

        Assume that there are not circular dependencies.

        @param plugin_name:       the name of the plugin
        @type plugin_name:        string
        @param installed_plugins: a cached list of installed plugins
        @type installed_plugins:  list of strings
        @return:                  the list of plugins that depends on the
                                  specified one
        @rtype:                   list of strings
        """

        if not installed_plugins:
            installed_plugins = self.get_installed_plugins()

        dependents = filter(lambda p: plugin_name in \
                                      map(lambda d: d['name'],
                                          p.get_plugin_deps()),
                            installed_plugins)

        to_check = copy.copy(dependents)
        for d in to_check:
            dependents += self.get_installed_dependent_plugins(d, installed_plugins)

        return dependents

    def _blocking_easy_install_plugin(self, plugin_name, plugin_version, directory):
        """Use easy_install internal API to install.

        @param plugin_name: the name of the egg distribution to install. It
                            should be something like "elisa-plugin-*"
        @type plugin_name: string
        @param plugin_version: the version to install, or None to get the
                               latest found
        @type plugin_version: string
        @param directory: the absolute path to the installation directory
        @type directory: string
        """

        # FIXME: kludge to install Elisa core into .elisa/core directory
        if plugin_name == 'elisa-plugin-core' or 'elisa_plugin_core' in plugin_name:
            if not os.path.exists(ELISA_CORE_DIR):
                os.mkdir(ELISA_CORE_DIR)
            init_file = os.path.join(ELISA_CORE_DIR, '__init__.py')
            if not os.path.exists(init_file):
                open(init_file, "wb").close()

            directory = ELISA_CORE_DIR

        try:
            os.environ['PYTHONPATH'] = '%s:%s' % (directory, os.environ['PYTHONPATH'])
        except KeyError:
            os.environ['PYTHONPATH'] = directory

        c = easy_install.easy_install(Distribution())
        c.initialize_options()

        c.upgrade = True
        c.install_dir = directory

        # FIXME: remove hardcoded crap
        c.find_links = map(lambda repo: repo.source.replace('xmlrpc', 'plugins'),
                           self.get_repositories())
        self.debug("find_links: %s" % c.find_links)

        # FIXME: maybe remove me? But we have to provide all the stated
        # dependences, if we don't rely on Pypi.
        # get a comma-separated list of hostnames
        c.allow_hosts = ",".join(map (lambda repo: urlparse.urlparse(repo.source)[1],
                                 self.get_repositories()))
        self.debug("allow_hosts: %s" % c.allow_hosts)

        c.zip_ok = True

        # FIXME: parse here the plugin_name stuff (URI)

        c.args = (plugin_name,)
        if plugin_version:
            c.args = (plugin_name + '==' + plugin_version,)
        c.finalize_options()

        c.run()

        if not c.outputs:
            raise InstallationError("Could not easy_install %s" % plugin_name)

        # HACK to get the plugin names
        installed = [os.path.basename(f).split("-")[0] for f in c.outputs]
        installed = [name[len('elisa_plugin_'):] for name in installed]

        # Load the newly installed plugin from the plugin registry to run
        # the install() method on it. This should take care to modify the
        # Elisa configuration, if necessary, and other staff
        self.plugin_registry.load_plugins(self.plugin_dirs)

        plugins = []
        for name in installed:
            try:
                plugins.append(self.plugin_registry.get_plugin_with_name(name))
            except Exception, e:
                raise InstallationError("Plugin registry cannot load %s" % name)

        for plugin in plugins:
            plugin.install()

        # TODO: think about recording the installation history somewhere.
        self.debug("easy_installed plugins: %s" % (c.outputs))

        return c.outputs

    def easy_install_plugin(self,
                            plugin_name,
                            plugin_version=None,
                            directory=ELISA_PLUGINS_DIR,
                            upgrade=False):
        if not upgrade:
            # FIXME: crappy hack
            # Disactivated, by now, because we should also check the version,
            # before aborting the install of a local file
##            if plugin_name.endswith(".egg"):
##                plugin_name = os.path.basename(plugin_name)
##                plugin_name = plugin_name.split("-")[0].replace("_", "-")

            if plugin_name in map(lambda p: ('elisa-plugin-' + p.name),
                                  self.get_installed_plugins()):
                return defer.fail(AlreadyInstalledError(plugin_name[len('elisa-plugin-'):]))

        dfr = threads.deferToThread(self._blocking_easy_install_plugin,
                                    plugin_name,
                                    plugin_version,
                                    directory)

        def on_install_ok(res):
            if not ('elisa-plugin-' + plugin_name) not in res[0]:
                return defer.fail(Exception("Couldn't install %s" % plugin_name))

            # FIXME: this is blocking code, shit!
            egg_parser = EggParser()
            metadata = egg_parser.parse_egg_file(plugin_name)

            installed_plugins = [egg_parser.parse_egg_file(p) for p in res]
            plugin_names = [egg_parser.parse_egg_file(p)['name'][len('elisa-plugin-'):]
                            for p in res]
            # FIXME: this is a workaround to the way "name" is built in the
            # eggs. Like this, we cannot have dashes in the name, which
            # probably is correct; look also at the hack in _blocking_easy_install.
            plugin_names = [name.replace('-', '_') for name in plugin_names]

            # Load the newly installed plugin from the plugin registry to run
            # the install() method on it. This should take care to modify the
            # Elisa configuration, if necessary, and other staff
            self.plugin_registry.load_plugins(self.plugin_dirs)

            plugins = []
            for name in plugin_names:
                try:
                    plugins.append(self.plugin_registry.get_plugin_with_name(name))
                except Exception, e:
                    err = InstallationError("Plugin registry cannot load %s" % name)
                    return defer.fail(err)

#            for plugin in plugins:
#                plugin.install()

            self.debug("Installed plugin names and instances: %s, %s" \
                       % (plugin_names, plugins))

            # Build the remove list
            to_remove = []
            for remove_list in [p['replaces'].split(',') for p in installed_plugins]:
                for name in filter(lambda i: i.strip() != "", remove_list):
                    to_remove.append(name.strip())

            def remove_done(res, removed, to_remove):
                self.info("Removed %s" % (removed))
                if to_remove:
                    dfr = self.remove(to_remove[0])
                    dfr.addCallback(remove_done, to_remove[0], to_remove[1:])
                    dfr.addErrback(remove_error, to_remove[0], to_remove[1:])
                else:
                    dfr = defer.succeed(res.name)

            def remove_error(res, removed, to_remove):
                self.debug("Errors removing %s" % (removed))
                if to_remove:
                    dfr = self.remove(to_remove[0])
                    dfr.addCallback(remove_done, to_remove[0], to_remove[1:])
                    dfr.addErrback(remove_error, to_remove[0], to_remove[1:])

            if to_remove:
                dfr = self.remove(to_remove[0])
                dfr.addCallback(remove_done, to_remove[0], to_remove[1:])
                dfr.addErrback(remove_error, to_remove[0], to_remove[1:])
            else:
                dfr = defer.succeed(plugin_name)

            return dfr

        def on_install_error(res):
            return defer.fail(Exception("Error while easy_installing: " + \
                                        res.getErrorMessage()))

        dfr.addCallback(on_install_ok)
        dfr.addErrback(on_install_error)

        return dfr

    # FIXME: should be possible to specify the version to install
    def _custom_install(self, plugin_name, plugin_version=None):
        """Install a plugin resolving dependecies.

        The dependencies are resolved from the repository plugins the registry
        is aware of.

        @param plugin_name: the plugin name (or file path) to install
        @type plugin_name: string
        @param plugin_version: the plugin version to install
        @type plugin_version: string
        @return: a deferred
        @rtype: L{twisted.internet.defer.DeferredList}
        """

        if plugin_name.endswith('.elisa') or plugin_name.endswith('.egg'):
            if not os.path.exists(plugin_name):
                err = Exception("The file '%s' does not exists" % (plugin_name))
                return defer.fail(err)

            egg_parser = EggParser()
            metadata = egg_parser.parse_egg_file(plugin_name)
            plugin = EggPlugin(local_file=plugin_name, **metadata)

        else:
            plugin = self.get_plugin(name=plugin_name, version=plugin_version)
            if not plugin:
                err = Exception("Cannot find '%s' at version '%s' in the repositories" \
                                % (plugin_name, plugin_version))
                return defer.fail(err)

        if not plugin:
            return defer.fail(Exception("NothingToInsall"))

        # this should never happen, indeed! self.get_plugin() only search for
        # repository plugins
        if plugin.plugin_class:
            return defer.fail(Exception("'%s' is already installed" \
                                        % (plugin.name)))

        # download and install missing plugins, starting from the leaves, that is
        # the ones that don't have further dependencies
        # FIXME: check the dependecy versions
        uninstalled = self.get_uninstalled_plugin_deps_for(plugin_name=plugin.name,
                                                           plugin_version=plugin.version)
        unknown = []
        uninstalled_plugins = []
        for u in uninstalled:
            p = self.get_plugin(name=u['name'], version=u['version'])
            if not p:
                self.debug("Cannot find any information about %s, at version %s" \
                           % (u['name'], u['version']))
                unknown.append((u['name'], u['version']))
            uninstalled_plugins.append(p)

        if unknown:
            unknown = list(set(unknown))
            return defer.fail(Exception("Aborting installation, " \
                                        "there are unknown plugins: %s" \
                                        % unknown))

        uninstalled_plugins += [plugin]
        self.warning("Plugins to install: %s" \
                     % (map(lambda p: (p.name, p.version), uninstalled_plugins)))

        def download_ok(res, plugin, remaining):
            self.info("Download ok for %s" % (plugin.name))
            dfr = plugin.install()
            dfr.addCallback(install_ok, plugin, remaining)
            return dfr

        def install_ok(res, plugin, remaining):
            self.info("Install ok for %s, going on with %s" \
                      % (plugin.name, map(lambda r: r.name, remaining)))
            if remaining:
                dfr = remaining[0].download()
                dfr.addCallback(download_ok, remaining[0], remaining[1:])
                dfr.addErrback(download_error, u)
            else:
                #dfr = defer.succeed(plugin)
                dfr = defer.succeed([])

            return dfr

        def download_error(res, plugin):
            self.info("Download error for %s" % (plugin.name))
            return res

        dfr = uninstalled_plugins[0].download()
        dfr.addCallback(download_ok, uninstalled_plugins[0], uninstalled_plugins[1:])
        dfr.addErrback(download_error, uninstalled_plugins[0])

        def done(results):
            errors = False
            for success, value in results:
                if not success:
                    errors = True

            if not errors:
                self.debug("Install successful")
            else:
                self.warning("Something went wrong while installing")

            return results

        return dfr

    def _blocking_upgrade(self, plugin_name=None, directory=ELISA_PLUGINS_DIR, update_states=[]):
        """Upgrade one or all the installed plugins.

        @param plugin_name: the name of the installed plugin. Or None.
        @type plugin_name: string
        @return: a deferred
        @rtype: L{twisted.internet.defer.Deferred}
        """

        upgradeables = self.get_upgradeable_plugins(update_states=update_states)
        upgraded = []

        if not upgradeables:
            raise NothingToUpgradeError("Nothing to upgrade")

        if plugin_name:
            # FIXME: perhaps we should also consider a plugin egg (.elisa)
            if plugin_name not in map(lambda p: p.name, upgradeables):
                raise Exception("The plugin '%s' is not upgradeable " \
                                "or is not installed" % (plugin_name))

            # FIXME: remove hardcoded stuff
            try:
                upgraded += self._blocking_easy_install_plugin('elisa-plugin-' + plugin_name,
                                                               None,
                                                               directory)
            except Exception, e:
                self.warning(str(e))

        else:
            self.debug("Upgrading all: %s" \
                       % (map(lambda u: (u.name, u.version, u.new_version),
                              upgradeables)))

            for u in upgradeables:
                plugin = self.get_plugin(name=u.name,
                                         version=u.new_version)
                if not plugin:
                    raise Exception("Cannot find '%s' for upgrade" % (u.name))

                try:
                    upgraded += self._blocking_easy_install_plugin('elisa-plugin-' + plugin.name,
                                                                   None,
                                                                   directory)
                except Exception, e:
                    self.warning(str(e))

        return upgraded

    def upgrade(self, plugin_name=None, directory=ELISA_PLUGINS_DIR, update_states=[]):
        return threads.deferToThread(self._blocking_upgrade,
                                     plugin_name, directory, update_states)

    # FIXME: use correctly the asyncronous API and activate me!
    def _async_upgrade(self, plugin_name=None, directory=ELISA_PLUGINS_DIR, update_states=[]):
        """Upgrade one or all the installed plugins.

        @param plugin_name: the name of the installed plugin. Or None.
        @type plugin_name: string
        @return: a deferred
        @rtype: L{twisted.internet.defer.Deferred}
        """

        upgradeables = self.get_upgradeable_plugins(update_states=update_states)

        if not upgradeables:
            err = NothingToUpgradeError("Nothing to upgrade")
            return defer.fail(err)

        if plugin_name:
            # FIXME: perhaps we should also consider a plugin egg (.elisa)
            if plugin_name not in map(lambda p: p.name, upgradeables):
                err = Exception("The plugin '%s' is not upgradeable " \
                                "or is not installed" % (plugin_name))
                return defer.fail(err)

            #dfr = self.install(plugin_name)
            # FIXME: remove hardcoded stuff
            dfr = self.easy_install_plugin('elisa-plugin-' + plugin_name,
                                           upgrade=True,
                                           directory=directory)

        else:
            self.debug("Upgrading all: %s" \
                       % (map(lambda u: (u.name, u.version, u.new_version),
                              upgradeables)))

            def upgrade_done(results):
                failed = []
                succeed = []

                for result, value in results:
                    if result:
                        succeed.append(value)
                    else:
                        failed.append(value)

                if failed:
                    self.debug("Errors while upgrading: %s" \
                               % map(lambda f: (f.getErrorMessage()), failed))
#                    self.debug("Theese plugin weren't upgraded: %s" \
#                               % map(lambda p: (p.name, p.version), failed))

                if succeed:
                    self.debug("Theese plugin were upgraded : %s" \
                               % map(lambda p: (p.name, p.version), succeed))

                return results

            dfr_upgrade_list = []
            for u in upgradeables:
                plugin = self.get_plugin(name=u.name,
                                         version=u.new_version)
                if not plugin:
                    err = Exception("Cannot find '%s' for upgrade" % (u.name))
                    dfr_upgrade_list.append(defer.fail(err))

                #dfr_upgrade = self.install(plugin.name)
                # FIXME: remove hardcoded stuff
                dfr_upgrade = self.easy_install_plugin('elisa-plugin-' + plugin.name,
                                                       upgrade=True,
                                                       directory=directory)

                dfr_upgrade.addCallback(lambda res: plugin)
                dfr_upgrade_list.append(dfr_upgrade)

            dfr = defer.DeferredList(dfr_upgrade_list)
            dfr.addBoth(upgrade_done)

        return dfr

    def remove(self, plugin_name):
        """Remove an installed plugin by name.

        @param plugin_name: the name of the installed plugin
        @type plugin_name: string
        @return: a deferred
        @rtype: L{twisted.internet.defer.Deferred}
        """

        installed = filter (lambda p: p.name == plugin_name,
                            self.get_installed_plugins())

        if not installed:
            self.debug("'%s' is not installed" % (plugin_name))
            err = Exception("'%s' is not installed" % (plugin_name))
            return defer.fail(err)

        plugin = installed[0]
        dfr = plugin.uninstall()

        def remove_ok(res):
            self.debug("Uninstall successful")
            return res

        def remove_error(err):
            self.debug("Errors while uninstalling: %s" \
                       % (str(err.getErrorMessage())))
            return err

        dfr.addCallback(remove_ok)
        dfr.addErrback(remove_error)

        return dfr

    def get_info(self, plugin_name, plugin_version=None):
        """Get information about a plugin.

        @param plugin_name: the name (or file path) of the plugin
        @type plugin_name: string
        @param plugin_version: the version of the plugin
        @type plugin_version: string
        @return: the retrieved information
        @rtype: L{twisted.internet.defer.Deferred}
        """

        if plugin_name.endswith('.elisa') or plugin_name.endswith('.egg'):
            if not os.path.exists(plugin_name):
                err = Exception("'%s' does not exists" % (plugin_name))
                return defer.fail(err)

            egg_parser = EggParser()
            metadata = egg_parser.parse_egg_file(plugin_name)
            plugin = EggPlugin(local_file=plugin_name, **metadata)

            return defer.succeed(plugin)

        else:
            # FIXME: should collect information about all the versions of
            # uinstalled plugins
            installable = self.get_plugin(name=plugin_name)

            installed = filter(lambda p: p.name == plugin_name,
                               self.get_installed_plugins())

            deps = self.get_plugin_deps_for(plugin_name=plugin_name,
                                            plugin_version=plugin_version)
            self.debug("Dependencies: %s" % deps)
            uninstalled = self.get_uninstalled_plugin_deps_for(plugin_name=plugin_name,
                                                               plugin_version=plugin_version)
            self.debug("Uninstalled dependencies: %s" % uninstalled)

            info = ''

            if installed:
                self.debug("'%s' is installed." % (plugin_name))
                info += "Installed version:\n %s\n" % (installed)

            if installable:
                self.debug("Repository version: %s" % (installable))
                info += "Repository version:\n %s" % (installable)

            return defer.succeed(info)

