# -*- 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>'


import pprint
import tempfile
import shutil
import os, os.path
import sys

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

from elisa.core.epm.exceptions import *

from elisa.core.log import Loggable
from elisa.extern.epr.egg_parser import EggParser

from elisa.core.component import parse_dependency

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

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


def easy_install_py(egg_name):
    # FIXME: this is sluggish
    if not os.path.isdir(ELISA_DEPS_DIR):
        os.makedirs(ELISA_DEPS_DIR)
    try:
        os.environ['PYTHONPATH'] += ':%s' % ELISA_DEPS_DIR
    except KeyError:
        os.environ['PYTHONPATH'] = ':%s' % ELISA_DEPS_DIR

    c = easy_install.easy_install(Distribution())
    c.initialize_options()
    c.install_dir = ELISA_DEPS_DIR
    c.args = (egg_name,)
    c.finalize_options()
    c.run()


class EggPlugin(Loggable):
    """
    An Elisa plugin.

    Can represent:

      1. a plugin coming from a remote repository
      2. a local .elisa (egg) file.
      3. an already installed plugin

    For now the property list is freely settable.
    """

    def __init__(self, repository=None, local_file=None, plugin_class=None, **kwargs):
        """
        Fill the plugin data.

        @param repository:   the repository where the plugin is coming from, if it doesn't
                             represent a single .elisa file
        @type repository:    L{epm.egg_repository.EggRepository}
        @param local_file:   a single, local .elisa file's path
        @type local_file:    string
        @param plugin_class: the class of an installed plugin
        @type plugin_class:  L{elisa.core.plugin.Plugin}
        @param kwargs:       properties of the plugin
        @type kwargs:        dictionary
        """
        Loggable.__init__(self)
        self.repository = repository
        self.local_file = local_file
        self.plugin_class = plugin_class

        self.download_path = None
        self.update_state_id = ''

        # FIXME: we should have at least some mandatory fields

#        properties = ['name', 'user', 'version', 'license', 'summary', 'description',
#                      'category_id', 'py_deps', 'ext_deps', 'plugin_deps', 'rel_path', 'size',
#                      'needs_elisa_restart', 'keywords']
#        for p in properties:


        for p in kwargs:
            self.__setattr__(p, kwargs.get(p, None))

        if self.local_file:
            # FIXME: this is indeed a blocking call, but we could delete it...
            assert(os.path.exists(self.local_file))

            # FIXME: when representing a local file, use EggParser to get the
            #        metadata, don't accept it from kwargs

            # HACK: build a proper list from a string representation like
            # "a, b"
            for key in ['plugin_deps', 'py_deps', 'ext_deps']:
                if key in kwargs and kwargs[key]:
                    value = kwargs.get(key)
                    value_list = map(lambda v: v.strip(), value.split(','))
                    self.__setattr__(key, value_list)
                else:
                    self.__setattr__(key, [])

        if self.plugin_class:
            self.name = self.plugin_class.name
            self.version = self.plugin_class.version
            self.plugin_deps = self.plugin_class.plugin_dependencies

    def get_plugin_deps(self):
        """Get the list of deps with version information."""
        deps = []
        for d in self.plugin_deps:
            dep = parse_dependency(d)
            deps.append({'name' : dep[0],
                         'sign' : dep[1],
                         'version' : dep[2]})

        return deps

    def pre_install(self):
        """
        Run the pre-installation script.

        @raise InstallationError: when the script doesn't return 0
        """
        pass

    def post_install(self):
        """
        Run the post-installation script.

        @raise InstallationError: when the script doesn't return 0
        """
        pass

    def pre_remove(self):
        """
        Run the pre-disinstallation script.

        @raise InstallationError: when the script doesn't return 0
        """
        pass

    def post_remove(self):
        """
        Run the post-disinstallation script.

        @raise InstallationError: when the script doesn't return 0
        """
        pass

    def is_installed_systemwide(self):
        """
        Tell whether the plugin is installed system wide.

        @return: the answer
        @rtype: bool
        """
        home = os.path.expanduser('~')
        if self.plugin_class:
            if not self.plugin_class.directory.startswith(home) \
               and not self.plugin_class.distribution_file.startswith(home):
                return True
        return False

    def _install_py_deps(self):
        for py_dep in self.py_deps:
            try:
                __import__(py_dep)
            except ImportError:
                try:
                    easy_install_py(py_dep)
                except Exception, exc:
                    self.warning(exc)

    def _blocking_custom_install(self, force=False):
        """Put the plugin in the right directory and run the needed scripts.

        @raise InstallationError: when something goes wrong.
        """

        from elisa.core.epm.egg_registry import EggRegistry
        registry = EggRegistry()

        if (self.name, self.version) in map(lambda p: (p.name, p.version),
                            registry.get_installed_plugins()):
            raise InstallationError("%s is already installed." % (self.name))

        self._install_py_deps()

        if not force:
            deps = registry.get_uninstalled_plugin_deps_for(plugin_name=self.name,
                                                            plugin_version=self.version)
            if deps:
                raise InstallationError("Unsatisfied dependencies for %s: %s" \
                                        % (self.name, map(lambda d: d['name'],
                                                          deps)))

        if not self.download_path:
            raise InstallationError("%s has to be downloaded before installing." \
                                    % (self.name))

        self.pre_install()

        #FIXME: think about this
        download_basename = os.path.basename(self.download_path)
        plugin_path = os.path.join(ELISA_PLUGINS_DIR, download_basename)
        shutil.copyfile(self.download_path, plugin_path)

        try:
            self.post_install()
        except InstallationError, e:
            #FIXME: think about this
            # os.remove(plugin_path)
            raise e

    def custom_install(self, force=False):
        return threads.deferToThread(self._blocking_install, force)

    def _blocking_uninstall(self, force=False):
        """Remove the plugin and run the needed scripts.

        @param force: uninstall even if there are plugins dipending on this one
        @type force: bool
        @raise UninstallationError: when cannot uninstall the plugin

        Block the installation if there are plugins depending on this (unless
        'force' is True) or if the plugin is installed system-wide.
        """

        from elisa.core.epm.egg_registry import EggRegistry
        registry = EggRegistry()

        if not self.plugin_class:
            raise UninstallationError("%s is not installed." \
                                      % (self.name))

        if self.is_installed_systemwide():
            raise UninstallationError("%s is installed system-wide." \
                                      % (self.name))

        deps = registry.get_installed_dependent_plugins(plugin_name=self.name)
        if deps and not force:
            raise UninstallationError("Installed dependecies: %s" \
                                      % map(lambda p: p.name, deps))

        #FIXME: what to do with 'force': it isn't just to uninstall the
        #       plugin, as other plugins would break. But have to find a way to
        #       walk the dendencies graph starting from the leaves

        self.pre_remove()

        home = os.path.expanduser('~')
        elisa_home = os.path.join(home, '.elisa', 'plugins')

        # FIXME: check me (I'm wrong) and free me!
        if self.plugin_class.distribution_file:
            # remove this assert: otherwise, testing in different directories
            # will not work
            ###assert(self.plugin_class.distribution_file.startswith(elisa_home))
            os.remove(self.plugin_class.distribution_file)

        elif self.plugin_class.directory:
            # just to be sure we don't go into greater troubles...
            raise UninstallationError("I won't uninstall local directories")

        # FIXME: do we really need this? If so, take care: we already removed
        #        the plugin, and we hadn't cached the postremove script
        self.post_remove()

        self.plugin_class = None

        return self

    def uninstall(self, force=False):
        return threads.deferToThread(self._blocking_uninstall, force)

    def download(self, directory=None):
        """Retrieve the plugin data.

        If the plugin is not a local .elisa file, ask the repository for the
        data: if download is ok, set the instance variable "download_path" to
        the absolute path of the downloaded file, to None otherwise.

        @param directory: the absolute path of the directory where to store the
                    downloaded file. Needs to exist and to be writeable
        @type directory: string
        @return: a deferred triggered when the download has finished
        @rtype: L{twisted.internet.defer.Defer}
        """

        plugin_path = None

        if not directory:
            directory = tempfile.mkdtemp()

        assert(os.path.exists(directory))

        if self.repository:
            dfr = self.repository.download(plugin=self)

            def download_done(res):
                data, egg_name = res

                download_path = os.path.join(directory, egg_name)
                egg = open(download_path, 'wb')
                egg.write(data)
                egg.close()
                self.download_path = download_path
                self.debug("Successfully downloaded '%s' to %s" \
                           % (egg_name, directory))

                return res

            def download_error(err):
                self.info("Couldn't get download url for '%s'" % (self.name))
                self.download_path = None

                return err

            dfr.addCallback(download_done)
            dfr.addErrback(download_error)

            return dfr

        elif self.local_file:
            self.download_path = self.local_file
            self.debug("'%s' is a local file, no need to download" \
                       % (self.local_file))
            return defer.succeed(True)

        elif self.plugin_class:
            return defer.fail(Exception("Cannot download an installed plugin"))

        else:
            return defer.fail(Exception("Unknow plugin type"))


    def __repr__(self):
        return pprint.pformat(self.__dict__)

