# -*- test-case-name: vumi.middleware.tests.test_base -*-

from twisted.internet.defer import inlineCallbacks, returnValue

from vumi.utils import load_class_by_string
from vumi.errors import ConfigError, VumiError


class MiddlewareError(VumiError):
    pass


class BaseMiddleware(object):
    """Common middleware base class.

    This is a convenient definition of and set of common functionality
    for middleware classes. You need not subclass this and should not
    instantiate this directly.

    The :meth:`__init__` method should take exactly the following
    options so that your class can be instantiated from configuration
    in a standard way:

    :param string name: Name of the middleware.
    :param dict config: Dictionary of configuraiton items.
    :type worker: vumi.service.Worker
    :param worker:
         Reference to the transport or application being wrapped by
         this middleware.

    If you are subclassing this class, you should not override
    :meth:`__init__`. Custom setup should be done in
    :meth:`setup_middleware` instead.
    """

    def __init__(self, name, config, worker):
        self.name = name
        self.config = config
        self.worker = worker

    def setup_middleware(self):
        """Any custom setup may be done here.

        :rtype: Deferred or None
        :returns: May return a deferred that is called when setup is
                  complete.
        """
        pass

    def teardown_middleware(self):
        """"Any custom teardown may be done here

        :rtype: Deferred or None
        :returns: May return a Deferred that is called when teardown is
                    complete
        """
        pass

    def handle_inbound(self, message, connector_name):
        """Called when an inbound transport user message is published
        or consumed.

        The other methods -- :meth:`handle_outbound`,
        :meth:`handle_event`, :meth:`handle_failure` -- all function
        in the same way. Only the kind of message being processed
        differs.

        :param vumi.message.TransportUserMessage message:
            Inbound message to process.
        :param string connector_name:
            The name of the connector the message is being received on or sent
            to.
        :rtype: vumi.message.TransportUserMessage
        :returns: The processed message.
        """
        return message

    def handle_outbound(self, message, connector_name):
        """Called to process an outbound transport user message.
        See :meth:`handle_inbound`.
        """
        return message

    def handle_event(self, event, connector_name):
        """Called to process an event message (
        :class:`vumi.message.TransportEvent`).
        See :meth:`handle_inbound`.
        """
        return event

    def handle_failure(self, failure, connector_name):
        """Called to process a failure message (
        :class:`vumi.transports.failures.FailureMessage`).
        See :meth:`handle_inbound`.
        """
        return failure


class TransportMiddleware(BaseMiddleware):
    """Message processor middleware for Transports.
    """


class ApplicationMiddleware(BaseMiddleware):
    """Message processor middleware for Applications.
    """


class MiddlewareStack(object):
    """Ordered list of middlewares to pass a Message through.
    """

    def __init__(self, middlewares):
        self.middlewares = middlewares

    @inlineCallbacks
    def _handle(self, middlewares, handler_name, message, connector_name):
        method_name = 'handle_%s' % (handler_name,)
        for middleware in middlewares:
            handler = getattr(middleware, method_name)
            message = yield handler(message, connector_name)
            if message is None:
                raise MiddlewareError('Returned value of %s.%s should never ' \
                                'be None' % (middleware, method_name,))
        returnValue(message)

    def apply_consume(self, handler_name, message, connector_name):
        return self._handle(
            self.middlewares, handler_name, message, connector_name)

    def apply_publish(self, handler_name, message, connector_name):
        return self._handle(
            reversed(self.middlewares), handler_name, message, connector_name)

    @inlineCallbacks
    def teardown(self):
        for mw in reversed(self.middlewares):
            yield mw.teardown_middleware()


def create_middlewares_from_config(worker, config):
    """Return a list of middleware objects created from a worker
       configuration.
       """
    middlewares = []
    for item in config.get("middleware", []):
        keys = item.keys()
        if len(keys) != 1:
            raise ConfigError("Middleware items contain only a single"
                              " key-value pair. The key should be a name"
                              " for the middleware. The value should be"
                              " the full dotted name of the class"
                              " implementing the middleware.")
        middleware_name = keys[0]
        cls_name = item[middleware_name]
        middleware_config = config.get(middleware_name, {})
        cls = load_class_by_string(cls_name)
        middleware = cls(middleware_name, middleware_config, worker)
        middlewares.append(middleware)
    return middlewares


@inlineCallbacks
def setup_middlewares_from_config(worker, config):
    """Create a list of middleware objects, call .setup_middleware() on
       then and then return the list.
       """
    middlewares = create_middlewares_from_config(worker, config)
    for mw in middlewares:
        yield mw.setup_middleware()
    returnValue(middlewares)
