# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 2006-2009 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 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Moovida with Fluendo's plugins.
#
# The GPL part of Moovida is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Moovida" in the root directory of this distribution package
# for details on that license.
#
# Author: Olivier Tilloy <olivier@fluendo.com>

"""
Plugin management controllers and decorators.
"""

from elisa.core.utils import defer, caching
from elisa.core.utils.sorting import async_ordered_placement
from elisa.core.utils.i18n import install_translation
from elisa.core import common
from elisa.core.plugin_registry import PluginStatusMessage
from elisa.core.input_event import EventValue

from elisa.plugins.base.models.plugin import PluginModel

from elisa.plugins.pigment.widgets.box import VBox
from elisa.plugins.pigment.widgets.theme import Theme
from elisa.plugins.pigment.pigment_controller import PigmentController
from elisa.plugins.pigment.graph.text import Text
from elisa.plugins.pigment.graph.image import Image

from elisa.plugins.poblesec.link import Link
from elisa.plugins.poblesec.section import SectionMenuController
from elisa.plugins.poblesec.base.list import GenericListViewMode
from elisa.plugins.poblesec.base.preview_list import \
    MenuItemPreviewListController, DoubleLineMenuItemPreviewListController
from elisa.plugins.poblesec.widgets.background import WidgetWithBackground
from elisa.plugins.poblesec.widgets.info_screen import TextInfoScreen
from elisa.plugins.poblesec.widgets.button import PanelButton

from elisa.plugins.poblesec.sections_menu import main_menu_decorate


from elisa.extern import enum

from twisted.internet import task
from twisted.web import error as web_error

from distutils.version import LooseVersion

import os.path, platform


_ = install_translation('poblesec')


def plugins_decorator(controller):
    controller_path = '/poblesec/plugins_menu'
    label = _('Plugins')
    icon = 'elisa.plugins.poblesec.plugins_section'
    dfr = main_menu_decorate(controller, controller_path, label, icon)
    return dfr


class PluginSectionController(SectionMenuController):
    pass


def find_new_plugins_decorator(controller):
    link = Link()
    link.controller_path = '/poblesec/plugins/find'
    link.label = _('Available Plugins')
    controller.model.append(link)
    return defer.succeed(None)


def plugin_library_decorator(controller):
    link = Link()
    link.controller_path = '/poblesec/plugins/library'
    link.label = _('Library')
    controller.model.append(link)
    return defer.succeed(None)


def plugin_updates_decorator(controller):
    link = Link()
    link.controller_path = '/poblesec/plugins/updates'
    link.label = _('Updates')
    controller.model.append(link)
    return defer.succeed(None)


class PluginListViewMode(GenericListViewMode):

    """
    Implementation of the common view modes API.
    """

    plugin_icon = 'elisa.plugins.poblesec.glyphs.small.plugins'

    def get_label(self, item):
        return defer.succeed(item.title)

    def get_sublabel(self, item):
        author = item.author_name
        if not author:
            author = _('Unknown Author')
        return defer.succeed(author)

    def get_default_image(self, item):
        return self.plugin_icon

    def get_image(self, item, theme):
        try:
            icon_uri = item.icons[1].references[0]
        except IndexError:
            # No icon available
            return None
        else:
            cache_path = caching.get_pictures_cache_path()
            return caching.get_and_cache_to_file(icon_uri, cache_path)


class FindNewPluginsController(DoubleLineMenuItemPreviewListController):

    """
    A list controller that displays a list of plugins.

    The list of plugins is downloaded from a set of plugin repositories.
    """

    view_mode = PluginListViewMode
    empty_label = _('There are no new plugins available.')

    def initialize(self):
        dfr = super(FindNewPluginsController, self).initialize()
        self._plugin_dicts = {}
        self._update_success = True
        dfr.addCallback(self._populate_collection)
        dfr.addCallback(self._connect_bus)
        return dfr

    def clean(self):
        if self._update_success:
            bus = common.application.bus
            bus.unregister(self._plugin_status_changed_cb)
        return super(FindNewPluginsController, self).clean()

    def _populate_collection(self, result):
        def get_plugin_list(cache_file):
            plugins = \
                plugin_registry.get_downloadable_plugins(reload_cache=True)
            installed = list(plugin_registry.get_plugin_names())

            def iterate_plugins(plugins):
                def _cmp(plugin1, plugin2):
                    return cmp(plugin1.title.lower(), plugin2.title.lower())

                is_power_user = common.application.is_power_user()

                for plugin in plugins:
                    name = plugin['name']
                    if name in installed:
                        continue
                    if plugin['quality'] == 'unstable' and not is_power_user:
                        continue
                    model = PluginModel.from_dict(plugin)

                    # Filter out platform-specific plugins if the target
                    # platform does not match.
                    if not model.runs_on_current_platform():
                        continue

                    self._plugin_dicts[name] = plugin
                    yield async_ordered_placement(self.model, model, _cmp)

            dfr = task.coiterate(iterate_plugins(plugins))
            return dfr

        def failed_update(failure):
            failure.trap(web_error.Error)
            self._update_success = False

        plugin_registry = common.application.plugin_registry
        # Query the plugin registry for a list of new plugins
        dfr = plugin_registry.update_cache()
        dfr.addCallback(get_plugin_list)
        dfr.addErrback(failed_update)
        dfr.addCallback(lambda x: result)
        return dfr

    def _connect_bus(self, result):
        if self._update_success:
            bus = common.application.bus
            bus.register(self._plugin_status_changed_cb, PluginStatusMessage)
        return result

    def _plugin_status_changed_cb(self, message, sender):
        # Notification that the status of a plugin has changed.
        # It may mean that a new plugin has been installed, in which case we
        # want to remove it from the list.
        for plugin in self.model:
            if plugin.name == message.plugin_name:
                self.model.remove(plugin)
                if len(self.model) == 0:
                    empty_label = _('All the available plugins have ' \
                                    'been successfully installed.')
                    self.display_empty_alert(empty_label)
                break

    def set_frontend(self, frontend):
        super(FindNewPluginsController, self).set_frontend(frontend)
        if not self._update_success:
            empty_label = _('Cannot connect to the plugin repository.')
            self.display_empty_alert(empty_label)

    def item_activated(self, item):
        browser = self.frontend.retrieve_controllers('/poblesec/browser')[0]
        path = '/poblesec/plugins/information'
        plugin_dict = self._plugin_dicts[item.name]
        return browser.history.append_controller(path, item.title,
                                                 plugin=item, installed=False,
                                                 plugin_dict=plugin_dict)


class PluginLibraryController(DoubleLineMenuItemPreviewListController):

    """
    A list controller that displays a list of plugins.

    The list of plugins is created querying the application's plugin registry.

    @cvar blacklist: a blacklist of plugins we do not want to show in the
                     collection, either because they are part of the
                     infrastructure, or because they do not provide any visible
                     service to the user.
    @type blacklist: C{list} of C{str}
    """

    view_mode = PluginListViewMode

    blacklist = []
    _plugins = ['amazon', 'amp', 'avahi', 'base', 'bluetooth', 'coherence',
                'daap', 'dailymotion', 'database', 'discogs', 'dvd',
                'elisa-updater', 'favorites', 'filtered-shares', 'gnome',
                'gstreamer', 'hal', 'helper', 'http-client', 'httpd', 'ipod',
                'lastfm', 'lirc', 'osso', 'picasa', 'pigment', 'player',
                'poblesec', 'rhythmbox', 'rss', 'search', 'shelf', 'smbwin32',
                'testing', 'themoviedb', 'thetvdb', 'tutorial-amazon',
                'winremote', 'winscreensaver', 'wmd']
    blacklist.extend(map(lambda x: 'elisa-plugin-%s' % x, _plugins))

    def initialize(self):
        dfr = super(PluginLibraryController, self).initialize()
        dfr.addCallback(self._populate_collection)
        dfr.addCallback(self._connect_bus)
        return dfr

    def clean(self):
        bus = common.application.bus
        bus.unregister(self._plugin_status_changed_cb)
        return super(PluginLibraryController, self).clean()

    def _blacklisted(self, plugin_name):
        return not plugin_name.startswith('elisa-plugin-') \
            or plugin_name in self.blacklist

    def _populate_collection(self, result):
        # Asynchronous sorted population of the list of plugins.
        plugin_registry = common.application.plugin_registry
        plugins = plugin_registry.get_plugins()

        def iterate_plugins(plugins):
            def _cmp(plugin1, plugin2):
                return cmp(plugin1.title.lower(), plugin2.title.lower())

            for plugin_name, enabled in plugins:
                if self._blacklisted(plugin_name):
                    continue
                plugin = plugin_registry.get_plugin_by_name(plugin_name)
                plugin_registry.get_plugin_metadata(plugin)
                model = PluginModel.from_distribution(plugin)
                model.enabled = enabled
                yield async_ordered_placement(self.model, model, _cmp)

        dfr = task.coiterate(iterate_plugins(plugins))
        dfr.addCallback(lambda x: result)
        return dfr

    def _connect_bus(self, result):
        bus = common.application.bus
        bus.register(self._plugin_status_changed_cb, PluginStatusMessage)
        return result

    def _plugin_status_changed_cb(self, message, sender):
        # Notification that the status of a plugin has changed.
        for index, plugin in enumerate(self.model):
            if plugin.name == message.plugin_name:
                plugin.enabled = \
                    (message.action == PluginStatusMessage.ActionType.ENABLED)
                self.model[index] = plugin
                break

    def item_activated(self, item):
        browser = self.frontend.retrieve_controllers('/poblesec/browser')[0]
        path = '/poblesec/plugins/information'
        return browser.history.append_controller(path, item.title, plugin=item)


class PluginAction(Link):

    """
    A contextual action associated to a given plugin or list of plugins.

    @ivar action: a callback invoked to trigger the action
    @type action: C{callable}
    @ivar args:   a list of arguments to pass to the callback
    @type args:   C{list}
    """

    def __init__(self, action=None, args=[]):
        super(PluginAction, self).__init__()
        self.action = action
        if action is None:
            self.action = self._no_action
        self.args = args

    def _no_action(self, item, *args):
        raise NotImplementedError('No action.')


class PluginUpdatesViewMode(PluginListViewMode):

    def get_label(self, item):
        if isinstance(item, PluginAction):
            return defer.succeed(item.label)
        return defer.succeed(item.title)

    def get_sublabel(self, item):
        if isinstance(item, PluginAction):
            return defer.succeed(item.sublabel)
        sublabel = _('Select To Update To Version %(version)s') % {'version': item.version}
        return defer.succeed(sublabel)

    def get_default_image(self, item):
        if isinstance(item, PluginAction):
            return item.icon
        return super(PluginUpdatesViewMode, self).get_default_image(item)

    def get_image(self, item, theme):
        if isinstance(item, PluginAction):
            return None
        return super(PluginUpdatesViewMode, self).get_image(item, theme)

    def get_preview_image(self, item, theme):
        if isinstance(item, PluginAction):
            return None
        su = super(PluginUpdatesViewMode, self)
        return su.get_preview_image(item, theme)


class PluginUpdatesController(DoubleLineMenuItemPreviewListController):

    """
    A list controller that displays a list of plugins.

    The plugins listed are those for which a newer version is available for
    download from a set of plugin repositories.
    """

    view_mode = PluginUpdatesViewMode
    empty_label = _('There are no updates available.')

    def initialize(self):
        dfr = super(PluginUpdatesController, self).initialize()
        self._plugin_dicts = {}
        self._update_success = True
        dfr.addCallback(self._populate_collection)
        return dfr

    def _populate_collection(self, result):
        def get_plugin_list(cache_file):
            plugins = \
                plugin_registry.get_downloadable_plugins(reload_cache=True)
            installed = list(plugin_registry.get_plugin_names())

            def iterate_plugins(plugins):
                def _cmp(plugin1, plugin2):
                    return cmp(plugin1.title.lower(), plugin2.title.lower())

                for plugin in plugins:
                    name = plugin['name']
                    if name not in installed:
                        continue
                    current = plugin_registry.get_plugin_by_name(name)
                    current_version = LooseVersion(current.version)
                    new_version = LooseVersion(plugin['version'])
                    if new_version <= current_version:
                        continue
                    self._plugin_dicts[name] = plugin
                    model = PluginModel.from_dict(plugin)
                    yield async_ordered_placement(self.model, model, _cmp)

            dfr = task.coiterate(iterate_plugins(plugins))
            return dfr

        def failed_update(failure):
            failure.trap(web_error.Error)
            self._update_success = False

        def add_update_all_action(plugins_result):
            if len(self.model) >= 2:
                update_all = PluginAction()
                update_all.label = _('Update All')
                update_all.sublabel = _('Apply All The Available Updates')
                update_all.icon = 'elisa.plugins.poblesec.glyphs.small.plugins'
                self.model.insert(0, update_all)
            return plugins_result

        plugin_registry = common.application.plugin_registry
        # Query the plugin registry for a list of new plugins
        dfr = plugin_registry.update_cache()
        dfr.addCallback(get_plugin_list)
        dfr.addErrback(failed_update)
        dfr.addCallback(add_update_all_action)
        dfr.addCallback(lambda x: result)
        return dfr

    def set_frontend(self, frontend):
        super(PluginUpdatesController, self).set_frontend(frontend)
        if not self._update_success:
            empty_label = _('Cannot connect to the plugin repository.')
            self.display_empty_alert(empty_label)

    def _show_restart_popup_cb(self, result, item):
        main = self.frontend.retrieve_controllers('/poblesec')[0]
        return main.show_restart_popup(result, [self._plugin_dicts[item.name],])

    def _update_plugin(self, item, single_update=True):
        def remove_from_list(result, item):
            self.model.remove(item)
            updates = [item for item in self.model \
                       if isinstance(item, PluginModel)]
            if len(updates) == 0:
                empty_label = _('All the available updates have ' \
                                'been successfully installed.')
                self.display_empty_alert(empty_label)
            return result

        plugin_registry = common.application.plugin_registry
        dfr = plugin_registry.update_plugin(self._plugin_dicts[item.name])
        dfr.addCallback(remove_from_list, item)
        if single_update:
            dfr.addCallback(self._show_restart_popup_cb, item)
        return dfr

    def _update_all_plugins(self):
        def iterate_updates(updates):
            for item in updates:
                dfr = self._update_plugin(item, single_update=False)
                yield dfr

        updates = [item for item in self.model \
                   if isinstance(item, PluginModel)]
        dfr = task.coiterate(iterate_updates(updates))
        dfr.addCallback(self._show_restart_popup_cb)
        return dfr

    def item_activated(self, item):
        if isinstance(item, PluginAction):
            return self._update_all_plugins()

        return self._update_plugin(item)


class PluginInfoScreen(TextInfoScreen):
    """
    DOCME
    """

    def pack_captions(self, caption):
        caption.name = Text()
        caption.foreground.pack_start(caption.name, expand=True)
        caption.name.visible = True

        caption.author = Text()
        caption.foreground.pack_start(caption.author, expand=True)
        caption.author.visible = True

        caption.version = Text()
        caption.foreground.pack_start(caption.version, expand=True)
        caption.version.visible = True

    def pack_buttons(self, footer):
        footer.button = PanelButton()
        footer.button.visible = True
        footer.pack_start(footer.button, expand=True)

class PluginInformationController(PigmentController):
    """
    Display detailed information about a given plugin.

    @ivar plugin:      a plugin model
    @type plugin:      L{PluginModel}
    @ivar installed:   whether the plugin is already installed
    @type installed:   C{bool}
    @ivar plugin_dict: a dictionary representing the plugin as understood by
                       the plugin repository
    @type plugin_dict: C{dict}
    """

    Actions = enum.Enum('INSTALL', 'USE', 'BACK')

    def initialize(self, plugin, installed=True, plugin_dict=None):
        dfr = super(PluginInformationController, self).initialize()
        self.plugin = plugin
        self.installed = installed
        self.plugin_dict = plugin_dict
        dfr.addCallback(self._create_widget)
        return dfr

    def _create_widget(self, result):
        self.information = PluginInfoScreen()
        self.widget.add(self.information)
        self.widget.set_focus_proxy(self.information.right_panel.footer)

        self.information.left_panel.caption.name.label = self.plugin.title
        self.information.left_panel.caption.author.label = self.plugin.author_name
        self.information.left_panel.caption.version.label = \
                                _('Version %(version)s') % {'version': str(self.plugin.version)}
        self.information.right_panel.title.foreground.label = _('INFORMATION')
        self.information.right_panel.contents.summary.foreground.label = self.plugin.description

        try:
            img_uri = self.plugin.icons[0].references[0]
        except IndexError:
            theme = Theme.get_default()
            resource = 'elisa.plugins.poblesec.glyphs.large.plugins'
            img_file = theme.get_resource(resource)
            self.information.left_panel.artwork.foreground.set_from_file(img_file)
        else:
            cache_path = caching.get_pictures_cache_path()
            dfr = caching.get_and_cache_to_file(img_uri, cache_path)
            dfr.addCallback(self.information.left_panel.artwork.foreground.set_from_file)

        self.information.visible = True

        if self.installed:
            self._ready_to_use(None)
        else:
            self._set_action(self.Actions.INSTALL)

        self.information.right_panel.footer.button.connect('activated', self._do_action)

        return result

    def clean(self):
        self.information.clean()
        self.information = None
        return super(PluginInformationController, self).clean()

    def _set_action(self, action):
        self._action = action 
        if action == self.Actions.INSTALL:
            self.information.right_panel.footer.button.text.label = _('Download Plugin')
        elif action == self.Actions.USE:
            self.information.right_panel.footer.button.text.label = _('Use This Plugin')
        elif action == self.Actions.BACK:
            self.information.right_panel.footer.button.text.label = _('Back')


    def _download_failed(self, failure):
        self.warning('Downloading %s %s failed.' % \
                     (self.plugin.name, self.plugin.version))
        return failure

    def _ready_to_use(self, result):
        try:
            plugin_registry = common.application.plugin_registry
            plugin = plugin_registry.get_plugin_by_name(self.plugin.name)
            hook = plugin.load_entry_point('elisa.core.plugin_registry', 'use')
        except ImportError:
            self._set_action(self.Actions.BACK)
            return result
        else:
            self._set_action(self.Actions.USE)
            return result

    def _do_action(self, button=None):
        plugin_registry = common.application.plugin_registry
        if self._action == self.Actions.INSTALL:
            dfr = plugin_registry.download_plugin(self.plugin_dict)
            dfr.addCallback(plugin_registry.install_plugin, self.plugin.name)
            dfr.addErrback(self._download_failed)
            dfr.addBoth(self._ready_to_use)
            return dfr
        elif self._action == self.Actions.USE:
            plugin = plugin_registry.get_plugin_by_name(self.plugin.name)
            # The 'use me' hook is a function that takes as a single parameter
            # the frontend and returns a deferred.
            hook = plugin.load_entry_point('elisa.core.plugin_registry', 'use')
            return hook(self.frontend)
        elif self._action == self.Actions.BACK:
            controller = '/poblesec/browser'
            browser = self.frontend.retrieve_controllers(controller)[0]
            browser.history.go_back()

    def handle_input(self, manager, input_event):
        if input_event.value in (EventValue.KEY_OK, EventValue.KEY_RETURN):
            self._do_action()
            return True
        su = super(PluginInformationController, self)
        return su.handle_input(manager, input_event)



from elisa.plugins.poblesec.actions import OpenControllerAction

class OpenPluginAction(OpenControllerAction):

    def execute(self, item):
        return self.open_controller(item.controller_path, item.label,
                                    **item.controller_args)

class LinkCompatiblePluginListViewMode(PluginListViewMode):
    """
    Compatibility layer to support plugins not using
    InternetMediaController.append_controller to add themselves to the
    menu.
    """

    def get_label(self, item):
        try:
            return PluginListViewMode.get_label(self, item)
        except AttributeError:
            # item is probably a Link
            return defer.succeed(item.label)

    def get_image(self, item, theme):
        try:
            return PluginListViewMode.get_image(self, item, theme)
        except AttributeError:
            # item is probably a Link
            pass

    def get_default_image(self, item):
        try:
            # assume item is a link first
            return item.icon
        except AttributeError:
            # item is a a plugin
            return PluginListViewMode.get_default_image(self, item)

class InternetMediaController(MenuItemPreviewListController):

    view_mode = LinkCompatiblePluginListViewMode

    def create_actions(self):
        return OpenPluginAction(self, ''), []

    def append_plugin(self, plugin_name, label, controller_path, \
                      controller_args={}):
        """
        DOCME
        """
        plugin_registry = common.application.plugin_registry
        dist = plugin_registry.get_plugin_by_name('elisa-plugin-%s' % plugin_name)
        plugin_model = PluginModel.from_distribution(dist)
        plugin_model.controller_path = controller_path
        plugin_model.controller_args = controller_args
        plugin_model.label = label
        plugin_model.title = label
        self.model.append(plugin_model)
