# Copyright (c) 2007-2009 Oliver Cope. All rights reserved.
# See LICENCE.TXT for terms of redistribution and use.

import cgi
import os
import posixpath
import re
import threading
from urllib import quote, quote_plus

from urlparse import urlunparse
try:
    from functools import partial
except ImportError:
    from pesto.utils import partial

import pesto.response
from pesto.utils import MultiDict
from pesto.httputils import FileUpload
from pesto.wsgiutils import make_query
from pesto.cookie import parse_cookie_header, Cookie
from pesto import DEFAULT_CHARSET

__all__ = ['Request', 'currentrequest',]

KB = 1024
MB = 1024 * KB

# This object will contain a reference to the current request
try:
    __local__ = threading.local()
except AttributeError:
    import logging
    logging.warn("threading.local not available!")
    class mocklocal(object):
        request = None
    __local__ = mocklocal()

def currentrequest():
    """
    Return the current Request object, or ``None`` if no request object is
    available.
    """
    try:
        return __local__.request
    except AttributeError:
        return None

class Request(object):
    """
    Models an HTTP request.
    """

    # Maximum size for application/x-www-form-urlencoded post data, or maximum
    # field size in multipart/form-data encoded data (not including file
    # uploads)
    MAX_SIZE = 16 * KB

    # Maximum size for multipart/form-data encoded post data
    MAX_MULTIPART_SIZE = 2 * MB

    _session = None
    _form = None
    _files = None
    _query = None
    _cookies = None

    charset = DEFAULT_CHARSET

    def __new__(
        cls,
        environ,
        parse_content_type = re.compile(r'\s*(?:.*);\s*charset=([\w\d\-]+)\s*$')
    ):
        u"""
        Ensure the same instance is returned when called multiple times on the
        same environ object.

        >>> from pesto.testing import TestApp
        >>> env1 = TestApp.make_environ()
        >>> env2 = TestApp.make_environ()
        >>> Request(env1) is Request(env1)
        True
        >>> Request(env2) is Request(env2)
        True
        >>> Request(env1) is Request(env2)
        False
        """
        try:
            return environ['pesto.request']
        except KeyError:
            request = object.__new__(cls)
            __local__.request = request
            request.environ = environ
            request.environ['pesto.request'] = request
            if 'CONTENT_TYPE' in environ:
                match = parse_content_type.match(environ['CONTENT_TYPE'])
                if match:
                    request.charset = match.group(1)
            return request

    def form(self):
        """
        Return the contents of any submitted form data

        If the form has been submitted via POST, GET parameters are also
        available via ``Request.query``.
        """
        if self._form is None:
            if self.request_method in ('PUT', 'POST'):
                self._form = MultiDict(
                    parse_post(
                        self.environ,
                        self.environ['wsgi.input'],
                        self.charset,
                        self.MAX_SIZE,
                        self.MAX_MULTIPART_SIZE,
                    )
                )
            else:
                self._form = MultiDict(
                    parse_querystring(
                        self.environ['QUERY_STRING'],
                        self.charset
                    )
                )
        return self._form
    form = property(form)

    def files(self):
        """
        Return ``FileUpload`` objects for all uploaded files
        """
        form = self.form
        if self._files is None:
            self._files = MultiDict(
                (k, v)
                for k, v in self.form.iterallitems()
                if isinstance(v, FileUpload)
            )
        return self._files
    files = property(files)

    def query(self):
        """
        Return a ``MultiDict`` of any querystring submitted data.

        This is available regardless of whether the original request was a
        ``GET`` request.

        Synopsis::

            >>> from pesto.testing import TestApp
            >>> request = Request(TestApp.make_environ(QUERY_STRING="animal=moose"))
            >>> request.query.get('animal')
            u'moose'

        Note that this property is unaffected by the presence of POST data::

            >>> from pesto.testing import TestApp
            >>> from StringIO import StringIO
            >>> postdata = 'animal=hippo'
            >>> request = Request(TestApp.make_environ(
            ...     QUERY_STRING="animal=moose",
            ...     REQUEST_METHOD="POST",
            ...     CONTENT_TYPE = "application/x-www-form-urlencoded; charset=UTF-8",
            ...     CONTENT_LENGTH=len(postdata),
            ...     wsgi_input=postdata
            ... ))
            >>> request.form.get('animal')
            u'hippo'
            >>> request.query.get('animal')
            u'moose'
        """
        if self._query is None:
            self._query = MultiDict(
                parse_querystring(self.environ.get('QUERY_STRING'))
            )

        return self._query
    query = property(query)

    def __getitem__(self, key):
        """
        Return the value of key from submitted form values.
        """
        marker = []
        v = self.get(key, marker)
        if v is marker:
            raise KeyError(key)
        return v

    def get(self, key, default=None):
        """
        Looks up a key in submitted form values
        """
        return self.form.get(key, default)

    def getlist(self, key):
        """
        Return a form value as a list.
        """
        return self.form.getlist(key)

    def __contains__(self, key):
        return self.form.has_key(key)

    def cookies(self):
        """
        Return a ``pesto.utils.MultiDict`` of cookies read from the request headers::

            >>> from pesto.testing import TestApp
            >>> request = Request(TestApp.make_environ(
            ...     HTTP_COOKIE='''$Version="1";
            ...     Customer="WILE_E_COYOTE";
            ...     Part="Rocket_0001";
            ...     Part="Catapult_0032"
            ... '''))
            >>> [c.value for c in request.cookies.getlist('Customer')]
            ['WILE_E_COYOTE']
            >>> [c.value for c in request.cookies.getlist('Part')]
            ['Rocket_0001', 'Catapult_0032']


        See rfc2109, section 4.4
        """
        if self._cookies is None:
            self._cookies = MultiDict(
                (cookie.name, cookie)
                for cookie in parse_cookie_header(self.get_header("Cookie"))
            )
        return self._cookies
    cookies = property(
        cookies, None, None,
        cookies.__doc__
    )

    def get_header(self, name, default=None):
        """
        Return an arbitrary HTTP header from the request.

        name
            HTTP header name, eg 'User-Agent' or 'If-Modified-Since'.

        default
            default value to return if the header is not set.

        Technical note:

        Headers in the original HTTP request are always formatted like this::

            If-Modified-Since: Thu, 04 Jan 2007 21:41:08 GMT

        However, in the WSGI environ dictionary they appear as follows::

            {
                ...
                'HTTP_IF_MODIFIED_SINCE': 'Thu, 04 Jan 2007 21:41:08 GMT'
                ...
            }

        Despite this, this method expects the *former* formatting (with
        hyphens), and is not case sensitive.

        """
        return self.environ.get(
            'HTTP_' + name.upper().replace('-', '_'),
            default
        )

    def request_path(self):
        """
        Return the path component of the requested URI
        """
        scheme, netloc, path, params, query, frag = self.parsed_uri
        return path
    request_path = property(request_path, doc=request_path.__doc__)

    @property
    def request_uri(self):
        """
        Return the absolute URI, including query parameters.
        """
        return urlunparse(self.parsed_uri)

    @property
    def application_uri(self):
        """
        Return the base URI of the WSGI application (ie the URI up to
        SCRIPT_NAME, but not including PATH_INFO or query information).

        Synopsis::

            >>> from pesto.testing import make_environ
            >>> request = Request(make_environ(SCRIPT_NAME='/animals', PATH_INFO='/alligator.html'))
            >>> request.application_uri
            'http://localhost/animals'
        """
        uri = self.parsed_uri
        scheme, netloc, path, params, query, frag = self.parsed_uri
        return urlunparse((scheme, netloc, self.script_name, '', '', ''))

    def parsed_uri(self):
        """
        Returns the current URI as a tuple of the form::

            (
             addressing scheme, network location, path,
             parameters, query, fragment identifier
            )

        Synopsis::

            >>> from pesto.testing import make_environ
            >>> request = Request(make_environ(
            ...     wsgi_url_scheme = 'https',
            ...     HTTP_HOST = 'example.com',
            ...     SCRIPT_NAME = '/animals',
            ...     PATH_INFO = '/view',
            ...     SERVER_PORT = '443',
            ...     QUERY_STRING = 'name=alligator'
            ... ))
            >>> request.parsed_uri
            ('https', 'example.com', '/animals/view', '', 'name=alligator', '')

        Note that the port number is stripped if the addressing scheme is
        'http' and the port is 80, or the scheme is https and the port is 443::

            >>> request = Request(make_environ(
            ...     wsgi_url_scheme = 'http',
            ...     HTTP_HOST = 'example.com:80',
            ...     SCRIPT_NAME = '/animals',
            ...     PATH_INFO = '/view',
            ...     QUERY_STRING = 'name=alligator'
            ... ))
            >>> request.parsed_uri
            ('http', 'example.com', '/animals/view', '', 'name=alligator', '')
        """
        env = self.environ.get
        script_name = env("SCRIPT_NAME", "")
        path_info = env("PATH_INFO", "")
        query_string = env("QUERY_STRING", "")
        scheme = env('wsgi.url_scheme', 'http')

        try:
            host = self.environ['HTTP_HOST']
            if ':' in host:
                host, port = host.split(':', 1)
            else:
                port = self.environ['SERVER_PORT']
        except KeyError:
            host = self.environ['SERVER_NAME']
            port = self.environ['SERVER_PORT']

        if (scheme == 'http' and port == '80') \
            or (scheme == 'https' and port == '443'):
            netloc = host
        else:
            netloc = host + ':' + port

        return (
            scheme,
            netloc,
            script_name + path_info,
            '', # Params
            query_string,
            '', # Fragment
        )
    parsed_uri = property(parsed_uri, doc=parsed_uri.__doc__)

     # getters for environ properties
    def _get_env(self, name, default=None):
        """
        Return a value from the WSGI environment
        """
        return self.environ.get(name, default)

    env_prop = lambda name, doc, default=None, _get_env=_get_env: property(
        partial(_get_env, name=name, default=None), doc=doc
    )

    content_type  = env_prop('CONTENT_TYPE', "HTTP Content-Type header")
    document_root = env_prop('DOCUMENT_ROOT', "Server document root")
    path_info     = env_prop('PATH_INFO', "WSGI PATH_INFO value", '')
    query_string  = env_prop('QUERY_STRING', "WSGI QUERY_STRING value")
    script_name   = env_prop('SCRIPT_NAME', "WSGI SCRIPT_NAME value")
    server_name   = env_prop('SERVER_NAME', "WSGI SERVER_NAME value")
    remote_addr   = env_prop('REMOTE_ADDR', "WSGI REMOTE_ADDR value")

    def referrer(self):
        """
        Return the HTTP referrer header, or None if this is not available.
        """
        return self.get_header('Referer')
    referrer = property(referrer, doc=referrer.__doc__)

    def user_agent(self):
        """
        Return the HTTP user agent header, or None if this is not available.
        """
        return self.get_header('User-Agent')
    user_agent = property(user_agent, doc=user_agent.__doc__)

    def request_method(self):
        """
        Return the HTTP method used for the request, eg ``GET`` or ``POST``.
        """
        return self.environ.get("REQUEST_METHOD").upper()
    request_method = property(request_method, doc=request_method.__doc__)


    def session(self):
        """
        Return the session associated with this request.

        Requires a session object to have been inserted into the WSGI
        environment by a middleware application (see
        ``pesto.session.base.sessioning_middleware`` for an example).
        """
        return self.environ["pesto.session"]

    session = property(
        session, None, None,
        doc = session.__doc__
    )


    def make_uri(
        self, scheme=None, netloc=None,
        path=None, parameters=None,
        query=None, fragment=None,
        urlquote = True
    ):
        r"""
        Make a new URI based on the current URI, replacing any of the six
        elements:

            scheme, netloc, path, parameters, query, fragment

        Synopsis:

        Calling request.make_uri with no arguments will return the current URI::

            >>> from pesto.testing import make_environ
            >>> request = Request(make_environ(HTTP_HOST='example.com', SCRIPT_NAME='', PATH_INFO='/foo'))
            >>> request.make_uri()
            'http://example.com/foo'

        Using keyword arguments it is possible to override any part of the URI::

            >>> request.make_uri(scheme='ftp')
            'ftp://example.com/foo'

            >>> request.make_uri(path='/bar')
            'http://example.com/bar'

            >>> request.make_uri(query={'page' : '2'})
            'http://example.com/foo?page=2'

        The path and query values are URL escaped before being returned::

            >>> request.make_uri(path=u'/caff\u00e8 latte')
            'http://example.com/caff%C3%A8%20latte'

        If a relative path is passed, the returned URI is joined to the old in
        the same way as a web browser would interpret a relative HREF in a
        document at the current location::

            >>> request = Request(make_environ(HTTP_HOST='example.com', SCRIPT_NAME='', PATH_INFO='/banana/milkshake'))
            >>> request.make_uri(path='pie')
            'http://example.com/banana/pie'

            >>> request.make_uri(path='../strawberry')
            'http://example.com/strawberry'

            >>> request.make_uri(path='../../../plum')
            'http://example.com/plum'

        Note that a URI with a trailing slash will have different behaviour
        from one without a trailing slash::

            >>> request = Request(make_environ(HTTP_HOST='example.com', SCRIPT_NAME='', PATH_INFO='/banana/milkshake/'))
            >>> request.make_uri(path='mmmm...')
            'http://example.com/banana/milkshake/mmmm...'

            >>> request = Request(make_environ(HTTP_HOST='example.com', SCRIPT_NAME='', PATH_INFO='/banana/milkshake'))
            >>> request.make_uri(path='mmmm...')
            'http://example.com/banana/mmmm...'

        """
        uri = []

        parsed_uri = self.parsed_uri

        if path is not None:
            if isinstance(path, unicode):
                path = path.encode(DEFAULT_CHARSET)
            if path[0] != '/':
                path = posixpath.join(posixpath.dirname(parsed_uri[2]), path)
                path = posixpath.normpath(path)
        else:
            path = parsed_uri[2]

        path = quote(path)

        if query is not None and isinstance(query, (MultiDict, dict)):
            query = make_query(query)

        for specified, parsed in zip((scheme, netloc, path, parameters, query, fragment), parsed_uri):
            if specified is not None:
                uri.append(specified)
            else:
                uri.append(parsed)

        return urlunparse(uri)

    def text(self):
        """
        Return a useful text representation of the request
        """
        import pprint
        return "<%s\n\trequest_uri=%s\n\trequest_path=%s\n\t%s\n\t%s>" % (
                self.__class__.__name__,
                self.request_uri,
                self.request_path,
                pprint.pformat(self.environ),
                pprint.pformat(self.form.items()),
        )

# Imports at end to avoid circular dependencies
from pesto.httputils import parse_querystring, parse_post
