# -*- 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: Florian Boucault <florian@fluendo.com>

"""
Queue of cancellable deferreds.
"""

from elisa.core.log import Loggable
from elisa.core.utils import defer

class Request(object):
    """
    Encapsulate a request to call a function. It is used in L{CancellableQueue}
    to queue the function calls.

    @ivar call:     function call encapsulated
    @type call:     callable
    @ivar args:     positional arguments to be passed to the function
    @type args:     tuple of objects
    @ivar kwargs:   keyword arguments to be passed to the function
    @type kwargs:   dict of the form {str: object}
    @ivar dfr:      deferred used as an interface with the request: fired when
                    the request has been processed
    @type dfr:      L{elisa.core.utils.cancellable_defer.CancellableDeferred}
    @ivar call_dfr: deferred returned by the function call
    @type call_dfr: L{elisa.core.utils.cancellable_defer.CancellableDeferred}
    """
    def __init__(self, call, *args, **kwargs):
        """
        @param call:   function call encapsulated
        @type call:    callable
        @param args:   positional arguments to be passed to the function
        @type args:    tuple of objects
        @param kwargs: keyword arguments to be passed to the function
        @type kwargs:  dict of the form {str: object}
        """
        self.call = call
        self.args = args
        self.kwargs = kwargs

        # build a deferred that will be the interface with the request
        self.dfr = defer.Deferred(canceller=self._cancel)
        # deferred returned by the actual call
        self.call_dfr = None

    def _cancel(self, dfr):
        if self.call_dfr != None:
            self.call_dfr.cancel()
            self.call_dfr = None

    def __str__(self):
        args_str = ""
        for value in self.args:
            args_str += "%s, " % value
        for name, value in self.kwargs.iteritems():
            args_str += "%s=%s, " % (name, value)
        return "call to %s with arguments: (%s)" % (self.call, args_str)

class CancellableQueue(Loggable):
    """
    Queue of cancellable function calls.

    One may need to enqueue function calls so that they are called one at a
    time sequentially. L{CancellableQueue} allows that while giving the
    possibility to cancel these function calls before they are processed
    or while they are made if they return
    L{elisa.core.utils.cancellable_defer.CancellableDeferred}.
    """

    def __init__(self):
        super(CancellableQueue, self).__init__()

        # queue containing all the requests that were enqueued and not
        # processed so far, that is the requests for which the function has
        # not been called yet
        self._requests_queue = []

        # request that is currently being processed, that is the request for
        # which the function has been called and the deferred returned has not
        # been fired yet
        self._current_request = None

    def enqueue(self, call, *args, **kwargs):
        """
        Enqueue a function call that will be called whenever the previously
        enqueued calls have been processed.
        Return a cancellable deferred that will allow interaction with the
        enqueued function call.

        @param call:   function call to enqueue
        @type call:    callable
        @param args:   positional arguments to be passed to the function
        @type args:    tuple of objects
        @param kwargs: keyword arguments to be passed to the function
        @type kwargs:  dict of the form {str: object}

        @rtype: L{elisa.core.utils.cancellable_defer.CancellableDeferred}
        """
        request = Request(call, *args, **kwargs)
        self._enqueue_request(request)
        return request.dfr

    def empty(self):
        """
        Remove all function call that were enqueued previously. The function
        call currently being processed is cancelled if it returned a cancellable
        deferred.
        """
        self.info("Empty loading queue that was containing %s requests" \
                    % len(self._requests_queue))

        # clear the preloading queue
        self._requests_queue[:] = []

        # cancel the current request
        if self._current_request != None:
            self._current_request.dfr.cancel()
            self._current_request = None

    def _enqueue_request(self, request):
        self.info("Enqueue %s" % request)
        self._requests_queue.insert(0, request)

        # if not busy, start processing that very request
        if self._current_request == None:
            self._process_next_request()

    def _process_next_request(self, dummy=None):
        try:
            # find the first request that was not yet cancelled
            cancelled = True
            while cancelled:
                request = self._requests_queue.pop()
                cancelled = request.dfr.called

            self.info("Process next request in the queue: %s" % request)
            self._current_request = request
            dfr = self._process_request(request)
            dfr.addCallbacks(self._current_request_done,
                             self._current_request_failed)
            dfr.addBoth(self._process_next_request)
        except IndexError:
            self.info("No request left to be processed in the queue")
            # there is no request left in the queue
            self._current_request = None

    def _process_request(self, request):
        dfr = defer.maybeDeferred(request.call, *request.args, **request.kwargs)
        request.call_dfr = dfr
        return dfr

    def _current_request_done(self, result):
        self.info("Current request processing done (%s)" % self._current_request)
        self._current_request.dfr.callback(result)
        self._current_request = None
        return result

    def _current_request_failed(self, failure):
        if not isinstance(failure.value, defer.CancelledError):
            self.warning("Current request processing failed: %s" % failure)
        self._current_request.dfr.errback(failure)
        self._current_request = None
        return failure
