#!/usr/bin/env python
#
# Copyright 2015 Adam Victor Brandizzi
#
# This file is part of Confeitaria.
#
# Confeitaria is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Confeitaria is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Confeitaria.  If not, see <http://www.gnu.org/licenses/>.
import inspect
import urlparse
import collections
try:
    import cStringIO as StringIO
except:
    import StringIO

import confeitaria.request
import confeitaria.server.environment

from confeitaria.interfaces import \
    has_action_method, has_index_method, has_page_method, has_setter
from confeitaria.responses import NotFound, MethodNotAllowed


class RequestParser(object):
    """
    ``RequestParser`` is responsible for building a ``Request`` object from a
    URL path and query string and, optionally, the body of a POST request.

    When initialized, the request parser will receive a page - very likely one
    with subpages.

    The magic happens mostly when calling the ``parse_request()`` method. It
    recieves as its argument a dict - more specifically, a WSGI environment.

    As a result, it returns a ``Request`` object, which for its turn has the
    page pointed by the given path - if any - as well as relevant information
    about how to call it. If no page could be found, then a
    ``confeitaria.responses.NotFound`` response is thrown.

    The Object Publisher pattern
    ----------------------------
    ``RequestParser.parse_request()`` returns an stance of
    ``confeitaria.request.Request``. This object has many attributes, and one
    of the most important is ``page``. It is the page object that is pointed by
    the ``PATH_INFO`` value from the environment dict.

    ``RequestParser`` implements the so called *object publisher* pattern,
    where URLs are addresses for real Python objects.

    Suppose we have the following page classes::

    >>> class RootPage(object):
    ...     def index(self):
    ...         return 'root'
    >>> class SubPage(object):
    ...     def index(self):
    ...         return 'sub'
    >>> class AnotherPage(object):
    ...     def index(self):
    ...         return 'another'
    >>> class ArgPage(object):
    ...     def index(self, arg, kwarg='0'):
    ...         return 'arg: {0} kwarg: {1}'.format(arg, kwarg)
    >>> class KwArgPage(object):
    ...     def index(self, kwarg1='1', kwarg2='2'):
    ...         return 'kwarg1: {0} kwarg2: {1}'.format(kwarg1, kwarg2)
    >>> class VarArgPage(object):
    ...     def index(self, *args):
    ...         return 'args: ' + ' '.join(args)
    >>> class ActionPage(object):
    ...     def action(self, kwarg1='1', kwarg2='2'):
    ...         pass

    Then, we build the following object with them::

    >>> root = RootPage()
    >>> root.attr = object()
    >>> root.sub = SubPage()
    >>> root.sub.another = AnotherPage()
    >>> root.arg = ArgPage()
    >>> root.kwarg = KwArgPage()
    >>> root.vararg = VarArgPage()
    >>> root.action = ActionPage()

    And then create a request parser as the following::

    >>> parser = RequestParser(root)

    ...and now URL paths should be mapped to the pages of the object. The root
    path is mapped to the root page::

        >>> page = parser.parse_request({'PATH_INFO': '/'}).page
        >>> page.index()
        'root'

    If the path has one more compoment, ``RequestParser`` tries to get a page
    from the attribute (of the root page) with the same name of the path
    component::

        >>> page = parser.parse_request({'PATH_INFO': '/sub'}).page
        >>> page.index()
        'sub'

    If the path has yet another component, then the request parser tries to get
    an attribute from the previous subpage, and so on::

        >>> page = parser.parse_request({'PATH_INFO': '/sub/another'}).page
        >>> page.index()
        'another'

    The ``Request`` object
    ----------------------

    We have seen how the returned object has a ``page`` attribute, but the
    ``confeitaria.request.Request`` object should have at least six
    attributes. They are::

    ``page``
        The page object pointed by the given URL path, if any. It is either the
        page given to the ``RequestParser`` constructor or one of its
        subpages::

        >>> isinstance(parser.parse_request({'PATH_INFO': '/'}).page, RootPage)
        True
        >>> isinstance(
        ...     parser.parse_request({'PATH_INFO': '/sub'}).page, SubPage)
        True

    ``path_args``
        The components o the URL path do not necessarily point only to the
        page. If the page method has mandatory arguments, there should be extra
        components in the URL path after a page is found. These extra componets
        will fill the mandatory arguments from the page method from the found
        page. You can find these components are found at the``path_args``
        attribute from the request::

        >>> parser.parse_request({'PATH_INFO': '/arg/value'}).path_args
        ['value']

        If there is no extra compoment, it will be an empty list::

        >>> parser.parse_request({'PATH_INFO': '/sub'}).path_args
        []

    ``query_args``
        A dict containing _all_ values from the query string from the URL.

        >>> parser.parse_request({
        ...     'PATH_INFO': '/', 'QUERY_STRING': 'arg=value'
        ... }).query_args
        {'arg': 'value'}
        >>> parser.parse_request({
        ...     'PATH_INFO': '/sub', 'QUERY_STRING': 'arg=value'
        ... }).query_args
        {'arg': 'value'}
        >>> parser.parse_request({
        ...     'PATH_INFO': '/sub', 'QUERY_STRING': 'arg=value&kwarg1=ok'
        ... }).query_args
        {'kwarg1': 'ok', 'arg': 'value'}

        If the query string is empty, so is the attribute

        >>> parser.parse_request({'PATH_INFO': '/sub'}).query_args
        {}

    ``form_args``
        A dict containing _all_ values from the request body::

        >>> import StringIO
        >>> parser.parse_request({
        ...     'REQUEST_METHOD': 'POST', 'PATH_INFO': '/action',
        ...     'QUERY_STRING': 'arg1=value', 'CONTENT_LENGTH': len('arg2=ok'),
        ...     'wsgi.input': StringIO.StringIO('arg2=ok')
        ... }).form_args
        {'arg2': 'ok'}

        If none is given, it is an empty dict::

        >>> parser.parse_request({
        ...     'PATH_INFO': '/sub', 'QUERY_STRING': 'arg1=value'
        ... }).form_args
        {}

    ``args``
        A sublist of ``path_args`` to be unpacked as the positional arguments
        of the page method from ``page``::

        >>> parser.parse_request({'PATH_INFO': '/arg/value'}).args
        ['value']

        In practice, right now, in this implementation, it will be equal to
        ``path_args`` but some undefined  behaviors can change in the future
        changing this fact.

    ``kwargs``
        A dict to be unpacked as the keyword arguments of ``page`` page method.
        If no request body is given, its values will come from ``query_args``::

        >>> parser.parse_request({
        ...     'PATH_INFO': '/kwarg', 'QUERY_STRING': 'kwarg1=query'
        ... }).kwargs
        {'kwarg1': 'query'}

        If a request body is available, then its values will come from
        ``form_args``::

        >>> parser.parse_request({
        ...     'REQUEST_METHOD': 'POST', 'PATH_INFO': '/action',
        ...     'QUERY_STRING': 'kwarg1=query',
        ...     'CONTENT_LENGTH': len('kwarg1=form'),
        ...     'wsgi.input': StringIO.StringIO('kwarg1=form')
        ... }).kwargs
        {'kwarg1': 'form'}

        This attribute will only contain values that matches optional arguments
        from the page method to be called - other values from ``form_args`` and
        ``query_args`` are not present::

        >>> parser.parse_request({
        ...     'PATH_INFO': '/kwarg',
        ...     'QUERY_STRING': 'kwarg1=query&nothere=true'
        ... }).kwargs
        {'kwarg1': 'query'}
        >>> parser.parse_request({
        ...     'REQUEST_METHOD': 'POST', 'PATH_INFO': '/action',
        ...     'CONTENT_LENGTH': len('kwarg1=form&nothere=true'),
        ...     'wsgi.input': StringIO.StringIO('kwarg1=form&nothere=true')
        ... }).kwargs
        {'kwarg1': 'form'}

        Also, it does not contain values for positional arguments, which always
        should come from the URL path components::

        >>> r = parser.parse_request({
        ...     'PATH_INFO': '/arg/yes', 'QUERY_STRING': 'kwarg=query&arg=no'
        ... })
        >>> r.kwargs
        {'kwarg': 'query'}
        >>> r.args
        ['yes']

    Unpacking ``args`` and ``kwargs`` as the arguments to the page method of
    the provided page should always match::

        >>> r.page.index(*r.args, **r.kwargs)
        'arg: yes kwarg: query'

    Raisig ``NotFound``
    -------------------

    On the other hand, if some of the pages do not have an attribute with the
    same name as the next compoment, then an ``confeitaria.responses.NotFound``
    exception is raised to signalize that the page was not found::

        >>> parser.parse_request({'PATH_INFO': '/nopage'})
        Traceback (most recent call last):
          ...
        NotFound: /nopage not found
        >>> parser.parse_request({'PATH_INFO': '/sub/nopage'})
        Traceback (most recent call last):
          ...
        NotFound: /sub/nopage not found

    The same happens when an attribute is found but it is not a page::

        >>> parser.parse_request({'PATH_INFO': '/attr'})
        Traceback (most recent call last):
          ...
        NotFound: /attr not found

    Positional arguments
    --------------------

    There is, however, a situation where the path has compoments that does not
    map to attributes and yet ``parse_request()`` succeeds. When the last found
    page's ``index()`` or ``action()`` method expects arguments, and the path
    has the same number of compoments remaining as the  number of arguments of
    the page method.

    For example, the index method from ``ArgPage`` expects a positional
    argument. So, any URL hitting the ``ArgPage`` object should have an extra
    component, which will be an argument to the index method::

    Since the ``index()`` method expects parameters, we should pass one more
    component in the path::

        >>> request = parser.parse_request({'PATH_INFO': '/arg/value'})
        >>> request.page.index(*request.args)
        'arg: value kwarg: 0'

    Yet, we cannot pass more path compoments than the number of arguments in
    the method::

        >>> parser.parse_request({'PATH_INFO': '/arg/value/other'})
        Traceback (most recent call last):
          ...
        NotFound: /arg/value/other not found

    Neither can we pass _less_ arguments - it will also result in a
    ``confeitaria.responses.NotFound`` response::

        >>> parser.parse_request({'PATH_INFO': '/arg'})
        Traceback (most recent call last):
          ...
        NotFound: /arg not found

    On the other hand, the page method can have arbitrary path arguments if
    it has a varargs parameter::

        >>> request = parser.parse_request({'PATH_INFO': '/vararg/v1/v2'})
        >>> request.page.index(*request.args)
        'args: v1 v2'

    Optional parameters
    -------------------

    Index methods can also have optional arguments. They can come from either
    the query string or from the submitted form fields. If the request body is
    not given, then the optional arguments will come from the query string::


    >>> parser.parse_request({
    ...     'PATH_INFO': '/kwarg', 'QUERY_STRING': 'kwarg1=value'
    ... }).kwargs
    {'kwarg1': 'value'}

    Action methods, on the other hand, are not expected to use the query
    arguments as their optional parameters. Instead, it should use the parsed
    values form a POST request body.

    If ``parse_request()`` received the second argument, it is expected to be
    the body of such a POST request, as a string. The parsed values can be
    found at the ``form_args`` attribute from the request::

    >>> parser.parse_request({
    ...     'REQUEST_METHOD': 'POST', 'PATH_INFO': '/action',
    ...     'QUERY_STRING': 'kwarg1=query',
    ...     'CONTENT_LENGTH': len('kwarg1=form'),
    ...     'wsgi.input': StringIO.StringIO('kwarg1=form')
    ... }).kwargs
    {'kwarg1': 'form'}
    """

    def __init__(self, page):
        """
        ``RequestParser`` expects as its constructor argument a page
        (probably with subpages) to which to map URLs.
        """
        self.url_dict = path_dict(page, has_page_method, sep='/', path='')

        for url, page in self.url_dict.items():
            if has_setter(page, 'url'):
                page.set_url(url if url != '' else '/')

        self.urls = sorted(self.url_dict.keys(), reverse=True)

    def parse_request(self, environment):
        if isinstance(environment, dict):
            env = confeitaria.server.environment.Environment(environment)
        else:
            env = environment

        page_path, args_path = split_path(env.path_info, self.urls)
        path_args = split_args(args_path)

        page = self.url_dict[page_path]

        if env.request_method == 'POST' and has_action_method(page):
            page_method = page.action
            request_kwargs = env.form_args
        elif env.request_method == 'GET' and has_index_method(page):
            page_method = page.index
            request_kwargs = env.query_args
        else:
            raise MethodNotAllowed(
                message='{0} does not support {1} requests'.format(
                    env.path_info, env.request_method
                )
            )

        sig = signature(page_method, exclude_self=True)
        if not match_method_args(path_args, sig):
            raise NotFound(message='{0} not found'.format(env.url))

        kwargs = subdict(request_kwargs, sig.kwargs.keys())

        return confeitaria.request.Request(
            page, env.path_info, page_path, args_path, path_args,
            env.query_args, env.form_args, path_args, kwargs, env.url,
            env.request_method
        )


def split_args(path):
    """
    Given a path, returns its components::

    >>> split_args('a/b/c')
    ['a', 'b', 'c']

    Any empty value will be discarded::

    >>> split_args('///a////b////c////')
    ['a', 'b', 'c']
    """
    return [a for a in path.split('/') if a if a]


def split_path(path, urls):
    """
    Receives a string representing an HTML request path, and a collection of
    URL paths. If any of the URL paths are a prefix to the given path, then
    it returns the prefix and the rest of the path::

    >>> split_path('/a/b/c', {'/a', '/d'})
    ('/a', 'b/c')
    >>> split_path('/a/b/c', {'/a/b', '/d'})
    ('/a/b', 'c')

    If no prefix is found, it will consider it to be ``'/'``, even if it is not
    contained in the collection::

    >>> split_path('/a/b/c', {})
    ('/', 'a/b/c')

    The first part will not have any slashes at the end; the second part will
    not have any slashes either at the beginning or at the end::

    >>> split_path('/a/b/c/', {'/a/', '/d/'})
    ('/a', 'b/c')
    """
    page_path = first_prefix(path, urls, default='/')
    if page_path != '/':
        page_path = page_path.rstrip('/')

    args_path = path.replace(page_path, '', 1)
    args_path = args_path.strip('/')

    return page_path, args_path


def match_method_args(args, sig):
    """
    ``match_method_args()`` checks whether we have enough arguments in the
    ``args`` list to match the signature in ``sig``.

    If the list of arguments has exactly the same number of mandatory arguments
    found in the signature, it returns ``True``::

    >>> def f(a, b): pass
    >>> match_method_args([1, 2], signature(f))
    True

    If we have less arguments than needed, it returns false::

    >>> match_method_args([1], signature(f))
    False

    If we have more arguments than needed, it returns ``False`` again::

    >>> match_method_args([1, 2, 3], signature(f))
    False

    However, if the signature accepts varargs, then the args list can have more
    values than the number of mandatory arguments::

    >>> def g(a, b, *args): pass
    >>> match_method_args([1, 2, 3, 4, 5, 6], signature(g))
    True

    Of course, it will return ``False`` when, given a signature with varargs,
    the arguments count is below the number of mandatory arguments::

    >>> match_method_args([1], signature(g))
    False
    """
    available = len(args)
    required = len(sig.args)

    if available < required:
        return False
    elif available > required and not sig.varargs:
        return False
    else:
        return True


def first_prefix(string, prefixes, default=None):
    """
    Given a list of strings ``l`` and a string ``s``, ``first_prefix()``
    finds the first item from ``l`` that is a prefix for ``s`` (i.e. the
    item ``i`` from ``l`` that satisfies ``s.startswith(i)``::

    >>> first_prefix('abc', ['b', 'a', 'abc'])
    'a'

    By default, if ``first_prefix()`` finds no prefix from the list, it returns
    ``None``::

    >>> first_prefix('jkl', ['b', 'a', 'abc']) is None
    True

    However, if the the ``default`` argument is given, this is the value to be
    returned when o prefix is found::

    >>> first_prefix('jkl', ['b', 'a', 'abc'], default='J')
    'J'
    """
    for p in prefixes:
        if string.startswith(p):
            return p

    return default


def subdict(d, keys):
    """
    ``subdict()`` receives a dict and a list of strings and return a dict
    containing all values mapped by the list of strings if present in the
    original dict::

    >>> subdict({'a': 1, 'b': 2}, ['a'])
    {'a': 1}

    ``subdict()`  just ignores and key not found in the dict::

    >>> subdict({'a': 1, 'b': 2}, ['a', 'c'])
    {'a': 1}
    """
    return {k: d[k] for k in keys if k in d}


def path_dict(obj, condition, sep='.', path=None):
    """
    ``path_dict()`` receives an object as its argument and returns a dict
    whose keys are the "path" to each attribute of the object (recursively)
    and the values are the attributes. Only objects satisfying a given
    condition will be added to the dict. For the tree below::

    >>> class Obj(object):
    ...     pass
    >>> o = Obj()
    >>> o.sub = Obj()
    >>> o.sub.another = Obj()
    >>> o.sub.another.value = 'a'
    >>> pd = path_dict(o, lambda o: isinstance(o, Obj))

    ...we get this::

    >>> pd[''] == o
    True
    >>> pd['sub'] == o.sub
    True
    >>> pd['sub.another'] == o.sub.another
    True
    >>> 'sub.another.value' in pd
    False

    Or using the example that most interest us - a page tree::

    >>> class TestPage(object):
    ...     def index(self):
    ...         return ''
    >>> page = TestPage()
    >>> page.sub = TestPage()
    >>> page.sub.another = TestPage()
    >>> page.sub.another.value = Obj()

    >>> from confeitaria.interfaces import has_page_method
    >>> pd = path_dict(page, condition=has_page_method, sep='/')
    >>> pd[''] == page
    True
    >>> pd['sub'] == page.sub
    True
    >>> pd['sub/another'] == page.sub.another
    True
    >>> 'sub/another/value' in pd
    False
    """
    result = {}

    if not condition(obj):
        return result
    else:
        result[path if path is not None else ''] = obj

    for an in dir(obj):
        attr = getattr(obj, an)
        if condition(attr):
            attr_path = sep.join((path, an)) if path is not None else an
            attr_dict = path_dict(attr, condition, sep=sep, path=attr_path)
            result.update(attr_dict)

    return result


Signature = collections.namedtuple(
    'Signature', ('args', 'kwargs', 'varargs', 'keywords')
)


def signature(f, exclude_self=False):
    """
    ``signature()`` should return a named tuple representing the argspec of
    the function in a more palatable way. it will have four attributes:

    ``args``
        a list with the name of positional arguments::

            >>> def f(a, b, c=3, d=3.14, e=4, *args, **kwargs): pass
            >>> sig = signature(f)
            >>> sig.args
            ['a', 'b']

        If the function has no arg, ``args`` is an empty list::

            >>> def noarg(): pass
            >>> signature(noarg).args
            []

    ``kwargs``
        a dictionary whose keys are the names of optional arguments mapping
        their default values::

            >>> sig.kwargs == {'c': 3, 'd': 3.14, 'e': 4}
            True

        If there is no optional argument, ``kwargs`` is a empty dict::

            >>> def noopt(): pass
            >>> signature(noopt).kwargs
            {}

    ``varargs``
        the name of the attribute containing the extra positional arguments
        (the "asterisk argument")::

            >>> sig.varargs
            'args'

        It is ``None`` if the function does not expect varargs::

            >>> signature(noarg).varargs is None
            True

    ``keywords``:

        the name of the argument containing the extra keyword values (i.e. the
        "double asterisk arg")::

            >>> sig.keywords
            'kwargs'

        It is ``None`` if the function does not expect extra keyword
        arguments::

            >>> signature(noarg).keywords is None
            True

    If called with ``exclude_self`` is true, the first argument of a bound
    method will be dropped. For example, if we have the class below::

        >>> class O(object):
        ...     def f(s, a, b):
        ...         pass

    ...the default behavior of ``signature()`` will be::

        >>> signature(O().f)
        Signature(args=['s', 'a', 'b'], kwargs={}, varargs=None, keywords=None)

    ...but it will ot list the first argument if ``exclude_self`` is true::

        >>> signature(O().f, exclude_self=True)
        Signature(args=['a', 'b'], kwargs={}, varargs=None, keywords=None)

    This does not happen with unbound methods and functions, however::

        >>> signature(O.f, exclude_self=True)
        Signature(args=['s', 'a', 'b'], kwargs={}, varargs=None, keywords=None)
        >>> def g(s, a, b): pass
        >>> signature(g, exclude_self=True)
        Signature(args=['s', 'a', 'b'], kwargs={}, varargs=None, keywords=None)

    This function should also work with callables.
    """
    try:
        argspec = inspect.getargspec(f)
    except TypeError:
        if hasattr(f, '__call__'):
            return signature(f.__call__, exclude_self=exclude_self)
        else:
            raise TypeError('{0} is neither a Python function nor callable')

    defaults = argspec.defaults if argspec.defaults is not None else []
    args_count = len(argspec.args) - len(defaults)

    args = argspec.args[:args_count]
    kwargs = dict(zip(argspec.args[args_count:], defaults))

    if exclude_self and getattr(f, 'im_self', False):
        args.pop(0)

    return Signature(args, kwargs, argspec.varargs, argspec.keywords)
