'''
Middleware are functions or callable objects similar to
:ref:`WSGI application handlers <wsgi-handlers>`
with the only difference that they can return ``None``. Middleware can be used
in conjunction with a :class:`WsgiHandler` or any other handler which iterate
through a list of middleware in a similar way (for example django wsgi
handler).

Here we introduce the :class:`Router` and :class:`MediaRouter` to handle
requests on given urls. Pulsar is shipped with
:ref:`additional wsgi middleware <wsgi-additional-middleware>` for manipulating
the environment before a client response is returned.


This module implements several WSGI middleware which does not
serve request but instead perform initialization and sanity checks.
It also introduces the **Response middlewares** implemented as a subclass of
:class:`ResponseMiddleware`. Response middlewares are used by the
:class:`pulsar.apps.wsgi.wrappers.WsgiResponse` to modify/add headers and
manipulate content.

.. _wsgi-additional-middleware:

Additional WSGI Middlewares
============================

Several :ref:`wsgi middleware <wsgi-middleware>` useful in several
applications.

clean path
~~~~~~~~~~~~~~~~~~
.. autofunction:: clean_path_middleware

cookie
~~~~~~~~~~~~~~~~~~
.. autofunction:: cookies_middleware

authorization
~~~~~~~~~~~~~~~~~~
.. autofunction:: authorization_middleware


.. _wsgi-response-middleware:

Response Middlewares
=============================

Response middleware are callable objects which can be used in conjunction
with pulsar :ref:`application handlers <wsgi-handlers>`. They must return
a :ref:`WsgiResponse <wsgi-response>` which can be the same as
the one passed to the callable or a brand new one.

Interface
~~~~~~~~~~~~~~~~~~

.. autoclass:: ResponseMiddleware
   :members:
   :member-order: bysource

GZip Middleware
~~~~~~~~~~~~~~~~~~~~~~
.. autoclass:: GZipMiddleware
   :members:
   :member-order: bysource

'''
import re
from gzip import GzipFile

import pulsar
from pulsar.utils.httpurl import BytesIO, parse_cookie

from .auth import parse_authorization_header

re_accepts_gzip = re.compile(r'\bgzip\b')


__all__ = ['clean_path_middleware',
           'AccessControl',
           'GZipMiddleware',
           'cookies_middleware',
           'authorization_middleware',
           'is_streamed']


def is_streamed(content):
    try:
        len(content)
    except TypeError:
        return True
    return False


def clean_path_middleware(environ, start_response):
    '''Clean url from double slashes and redirect if needed.'''
    path = environ['PATH_INFO']
    if path and '//' in path:
        url = re.sub("/+", '/', path)
        if not url.startswith('/'):
            url = '/%s' % url
        qs = environ['QUERY_STRING']
        if qs:
            url = '%s?%s' % (url, qs)
        raise pulsar.HttpRedirect(url)


def cookies_middleware(environ, start_response):
    '''Parse the ``HTTP_COOKIE`` key in ``environ``.

    Set the new ``http.cookie`` key in ``environ`` with a dictionary
    of cookies obtained via the :func:`pulsar.utils.httpurl.parse_cookie`
    function.
    '''
    c = environ.get('http.cookie')
    if not isinstance(c, dict):
        c = environ.get('HTTP_COOKIE', '')
        if not c:
            c = {}
        else:
            if not isinstance(c, str):
                c = c.encode('utf-8')
            c = parse_cookie(c)
        environ['http.cookie'] = c


def authorization_middleware(environ, start_response):
    '''Parse the ``HTTP_AUTHORIZATION`` key in the ``environ``.

    If available, set the ``http.authorization`` key in ``environ`` with
    the result obtained from
    :func:`pulsar.apps.wsgi.auth.parse_authorization_header` function.
    '''
    key = 'http.authorization'
    c = environ.get(key)
    if c is None:
        code = 'HTTP_AUTHORIZATION'
        if code in environ:
            environ[key] = parse_authorization_header(environ[code])


def wait_for_body_middleware(environ, start_response):
    '''Use this middleware to wait for the full body.

    This middleware wait for the full body to be received before letting
    other middleware to be processed.

    Useful when using synchronous web-frameworks.
    '''
    return environ['stream'].on_message_complete.add_callback(lambda s: None)


#####################################################    RESPONSE MIDDLEWARE
class ResponseMiddleware(object):
    '''Bas class for :class:`pulsar.apps.wsgi.wrappers.WsgiResponse`
middlewares. The focus of this class is the :meth:`execute` method where
the middleware logic is implemented.'''
    def version(self, environ):
        return environ.get('wsgi.version')

    def available(self, environ, response):
        '''Check if this :class:`ResponseMiddleware` can be applied to
the *response* object.

:param environ: a WSGI environ dictionary.
:param response: a :class:`pulsar.apps.wsgi.wrappers.WsgiResponse`
:return: ``True`` or ``False``.'''
        return True

    def __call__(self, environ, response):
        if not self.available(environ, response):
            return response
        resp = self.execute(environ, response)
        return resp if resp is not None else response

    def execute(self, environ, response):
        '''Manipulate *response*, called only if the :meth:`available`
method returns ``True``.'''
        pass


class AccessControl(ResponseMiddleware):
    '''A response middleware which add the ``Access-Control-Allow-Origin``
response header.'''
    def __init__(self, origin='*', methods=None):
        self.origin = origin
        self.methods = methods

    def available(self, environ, response):
        return response.status_code == 200

    def execute(self, environ, response):
        response.headers['Access-Control-Allow-Origin'] = self.origin
        if self.methods:
            response.headers['Access-Control-Allow-Methods'] = self.methods


class GZipMiddleware(ResponseMiddleware):
    """A :class:`ResponseMiddleware` for compressing content if the request
allows gzip compression. It sets the Vary header accordingly.

The compression implementation is from
http://jython.xhaus.com/http-compression-in-python-and-jython
    """
    def __init__(self, min_length=200):
        self.min_length = min_length

    def available(self, environ, response):
        # It's not worth compressing non-OK or really short responses
        try:
            if response.status_code == 200 and not response.is_streamed:
                if response.length() < self.min_length:
                    return False
                headers = response.headers
                # Avoid gzipping if we've already got a content-encoding.
                if 'Content-Encoding' in headers:
                    return False
                # MSIE have issues with gzipped response of various
                # content types.
                if "msie" in environ.get('HTTP_USER_AGENT', '').lower():
                    ctype = headers.get('Content-Type', '').lower()
                    if not ctype.startswith("text/") or "javascript" in ctype:
                        return False
                ae = environ.get('HTTP_ACCEPT_ENCODING', '')
                if not re_accepts_gzip.search(ae):
                    return False
                return True
        except Exception:
            raise

    def execute(self, environ, response):
        headers = response.headers
        headers.add_header('Vary', 'Accept-Encoding')
        content = b''.join(response.content)
        response.content = (self.compress_string(content),)
        response.headers['Content-Encoding'] = 'gzip'

    def compress_string(self, s):
        zbuf = BytesIO()
        zfile = GzipFile(mode='wb', compresslevel=6, fileobj=zbuf)
        zfile.write(s)
        zfile.close()
        return zbuf.getvalue()
