# -*- 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.
#
# Authors: Philippe Normand <philippe@fluendo.com>
#          Olivier Tilloy <olivier@fluendo.com>

"""
Component instances management
"""

from elisa.core import log, common
from elisa.core.utils import defer
from elisa.core.component import get_component_path

from twisted.internet import task


class AlreadyRegistered(Exception):
    """
    Raised when trying to registered a component that is already registered.
    """
    pass


class CannotUnregister(Exception):
    """
    Raised when trying to unregister a component that is not registered.
    """
    pass


class Manager(log.Loggable):
    """
    A manager handles a list of components.

    Components can be registered and unregistered from the manager.

    Optionnally the manager can implement start/stop methods if it
    needs to handle any kind of loop (example: media sources scanning,
    input events polling, etc). start/stop methods are called by the
    parent object (application).

    @ivar components:   components currently registered in the manager
    @type components:   C{list} of L{elisa.core.component.Component}

    @cvar entry_point:   the entry point listing providers in plugins
    @type entry_point:   C{str}
    """

    entry_point = None

    def __init__(self):
        super(Manager, self).__init__()
        self._bus = None
        self.components = []

    def _clean_component(self, result, component):
        return component.clean()

    def clean(self):
        component_paths = [component.path for component in self.components]
        return self._unload_components(component_paths)

    def _load_components(self, component_names):
        """
        Asynchronously load and register a list of components.

        @param component_names: the list of the names of components to load
        @type  component_names: C{list} of C{str}
        """
        plugin_registry = common.application.plugin_registry

        def load_components_iter(loaded):
            def create_component_done(component, component_name):
                self.debug('Loaded provider %s' % component_name)
                dfr = self.register_component(component)
                dfr.addCallback(lambda result: (True, component))
                dfr.addErrback(lambda failure: (False, component))
                return dfr

            def create_component_failure(failure, component_name):
                log_path = common.application.log_failure(failure)
                self.warning('Creating %s failed. A full traceback can'
                             ' be found at %s' % (component_name, log_path))
                return (False, failure)

            for component_name in component_names:
                dfr = plugin_registry.create_component(component_name)
                dfr.addCallback(create_component_done, component_name)
                dfr.addErrback(create_component_failure, component_name)
                dfr.addCallback(loaded.append)
                yield dfr

        def load_components_iter_done(iter, loaded):
            really_loaded = filter(lambda x: x[0], loaded)
            self.debug('Loaded %d providers' % len(really_loaded))
            return loaded

        loaded = []
        load_dfr = task.coiterate(load_components_iter(loaded))
        load_dfr.addCallback(load_components_iter_done, loaded)
        return load_dfr

    def _unload_components(self, component_names):
        """
        Asynchronously unregister and unload a list of components.

        @param component_names: the list of the names of components to unload
        @type  component_names: C{list} of C{str}
        """
        def unload_components_iter(unloaded):
            for component_name in component_names:
                dfr = defer.succeed(component_name)
                for component in self.components:
                    component_path = get_component_path(component.__class__)
                    if component_path == component_name:
                        dfr = self.unregister_component(component)

                        def unregistered(result):
                            self.debug('Unloaded provider %s' % component_name)
                            unloaded.append(component_name)
                            return component.clean()

                        dfr.addCallback(unregistered)
                        dfr.addErrback(lambda failure: None)
                        break
                yield dfr

        def unload_components_iter_done(result, unloaded):
            self.debug('Unloaded %d providers' % len(unloaded))
            return unloaded

        unloaded = []
        unload_dfr = task.coiterate(unload_components_iter(unloaded))
        unload_dfr.addCallback(unload_components_iter_done, unloaded)
        return unload_dfr

    def register_component(self, component):
        """
        Register a new component.

        When trying to register a component twice, an AlreadyRegistered failure
        will be returned.

        @param component: the component to register
        @type component:  L{elisa.core.component.Component}

        @return: a deferred fired when the component is registered
        @rtype:  L{elisa.core.utils.defer.Deferred}
        """
        if component in self.components:
            self.debug('Component %s already registered' % component)
            return defer.fail(AlreadyRegistered(component))

        self.debug('Registering component %s' % component)
        self.components.append(component)
        return defer.succeed(component)

    def unregister_component(self, component):
        """
        Unregister a component.

        When trying to unregister a component that is not registered, a
        CannotUnregister failure will be returned.

        @param component: the component to unregister
        @type component:  L{elisa.core.component.Component}

        @return: a deferred fired when the component is unregistered
        @rtype:  L{elisa.core.utils.defer.Deferred}
        """
        try:
            self.components.remove(component)
        except ValueError:
            self.debug('Cannot unregister component %s' % component)
            return defer.fail(CannotUnregister(component))
        else:
            self.debug('Unregistering component %s' % component)
            return defer.succeed(component)

    def plugin_status_changed_cb(self, plugin, status):
        """
        Callback meant to be invoked (by the plugin registry) when the status
        of a plugin has changed.
        """
        # Build the list of providers provided by the plugin
        providers = []
        entry_map = plugin.get_entry_map(self.entry_point)
        for entry in entry_map.itervalues():
            component_name = '%s:%s' % \
                             (entry.module_name, '.'.join(entry.attrs)) 
            providers.append(component_name)
        if len(providers) == 0:
            return defer.succeed(None)

        if status:
            self.debug('Loading %d providers for plugin %s' % \
                       (len(providers), plugin.project_name))
            dfr = self._load_components(providers)
        else:
            self.debug('Unloading %d providers for plugin %s' % \
                       (len(providers), plugin.project_name))
            dfr = self._unload_components(providers)

        return dfr
