# -*- 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: Olivier Tilloy <olivier@fluendo.com>
#          Benjamin Kampmann <benjamin@fluendo.com>

"""
Metadata handling through a simple unified API.
"""

from elisa.core.manager import Manager, AlreadyRegistered

from elisa.core.utils import defer
from elisa.core.utils.cancellable_defer import CancellableDeferred, \
        cancellable_deferred_iterator
from twisted.internet import task


class IncompleteMetadataResponse(Exception):
    """
    The exception raised when all the metadata capabilities in the list have
    been tried and failed to satisfy the check callback.
    """
    pass


class MetadataManager(Manager):
    """
    The MetadataManager can be queried for a given metadata request to fill a
    model.

    It will go sequentially through all the registered metadata capabilities
    that are able to handle the request until the expected metadata is filled.
    """

    entry_point = 'elisa.core.components.metadata_provider'

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

        The internal list of capabilities is always kept sorted by decreasing
        rank.

        @param capability: an instance of a metadata capability to register
        @type capability:  L{elisa.core.components.metadata_capability.MetadataCapability}

        @return: a deferred fired when the capability is registered
        @rtype:  L{elisa.core.utils.defer.Deferred}
        """
        # This method is overridden
        if capability in self.components:
            self.debug('Capability %s already registered' % str(capability))
            return defer.fail(AlreadyRegistered(capability))

        self.debug('Registering capability %s' % str(capability))
        for index in xrange(len(self.components)):
            if capability.rank > self.components[index].rank:
                # Insert the capability at the right position in the list
                self.components.insert(index, capability)
                return defer.succeed(capability)

        # Append the capability to the end of the list, lowest rank
        self.components.append(capability)
        return defer.succeed(capability)

    def get_metadata(self, model, check_callback):
        """
        Try to retrieve the requested metadata and populate the model with it.

        All the capabilities able to handle the request are tried sequentially
        in decreasing order of capability rank until all the requested metadata
        is retrieved or all the capabilities have been tried.
        If all the capabilities have been tried and the requested metadata
        remains incomplete, the IncompleteMetadataResponse exception will be
        raised.

        @param model:          the model that should be populated with the
                               metadata
        @type model:           a subclass of
                               L{elisa.core.components.model.Model}
        @param check_callback: A function called after each
                               capability.get_metadata was called and has
                               answered. The only parameter is the model
                               itself. The callback should check whether we can
                               stop processing this model as all desired values
                               are set (by returning True) or if the manager
                               should keep on processing the model.
        @type check_callback:  callable

        @return:               a deferred fired when the metadata retrieval is
                               complete
        @rtype:                L{elisa.core.utils.defer.Deferred}

        @raise IncompleteMetadataResponse: when the requested metadata remains
                                           incomplete
        """
        @cancellable_deferred_iterator
        def iter_components(model):
            for capability in self.components:
                if check_callback(model):
                    return

                self.debug('Trying capability %s' % capability)
                error_message = 'Capability %s failed, continuing' % capability

                try:
                    if not capability.able_to_handle(model):
                        continue
                except:
                    # If for some reason the able_to_handle method is broken
                    # and raises an exception, this should not stop the whole
                    # process.
                    self.debug(error_message)
                    continue

                def errorHandler(failure):
                    # When one of the metadata capabilities fails for some
                    # reason, this should not stop the whole process.
                    self.debug(error_message)
                    failure.trap(Exception)

                try:
                    dfr = capability.get_metadata(model)
                except:
                    # If for some reason the get_metadata method is broken and
                    # raises an exception, this should not stop the whole
                    # process.
                    self.debug(error_message)
                    continue

                dfr.addErrback(errorHandler)
                yield dfr

        def iteration_done(result):
            if not check_callback(model):
                self.debug('Model still incomplete')
                raise IncompleteMetadataResponse(model)

            self.debug('All capabilities tried')

        iterator = iter_components(model)
        res_dfr = task.coiterate(iter(iterator))
        res_dfr.addCallback(iteration_done)

        def cancel(dfr):
            iterator.cancel()

        cancellable_dfr = CancellableDeferred(canceller=cancel)
        res_dfr.chainDeferred(cancellable_dfr)

        return cancellable_dfr
