from itertools import chain

from . import context, GET, Response
from .compat import ustr, string_types
from .routing import RouteArg
from .util.http import parse_header

__all__ = ['subrequest', 'subrequest_raw', 'subrequest_bytes']


class Markup(ustr):
    def __html__(self):
        return self


def subrequest(view, *args, **kwargs):
    """
    Return the response generated by a subrequest to ``view``

    This behaves differently depending on the value of ``view``:

    - **A string containing a ``/``**:
      a full request context is constructed to emulate a request
      to the given path.
      The subrequest will invoke all middleware layers and application hooks.

    - **A callable**: a new subrequest context will be created and
      ``view(*args, **kwargs)`` will be called. Middleware and application
      hooks will not be called, and any
      :class:`~fresco.routeargs.RouteArg`\s  defined will not be resolved.

    - **Any other value**: the view function is looked up using the same
      route resolution rules as :meth:`~fresco.core.FrescoApp.urlfor`.
      Middleware and hooks will be skipped, but
      :class:`~fresco.routeargs.RouteArg`\s will be resolved.

    If you pass in a view callable you can force RouteArgs to be resolved by
    specifying ``_resolve=True``.

    You can force a full request to be emulated by specifying ``_full=True``.

    Passing additional positional and keyword arguments has a different
    effect depending on the value of ``view``, ``_resolve`` and ``_full``:

    - **If view is a path, or if _full is True**,
      ``*args`` and ``**kwargs`` are passed to
      :meth:`~fresco.core.FrescoApp.requestcontext`,
      and may be used to customize the WSGI environment
      used for the subrequest.

    - **If view is a callable and _resolve is False**,
      ``*args`` and ``**kwargs`` are passed to the view callable.

    - **Otherwise**
      ``*args`` and ``**kwargs`` are passed to the route lookup mechanism
      and should provide the values defined in the route's path.
      ``kwargs`` values also override any :class:`~fresco.routeargs.RouteArg`\s
      defined by the route. These will only be generated from the request if
      a corresponding value is not present in ``kwargs``.

    :param view: The target path, view callable, or viewspec.

    :param _mode: One of ``raw``, ``str`` or ``bytes``.
                 If ``str``, the content of the subrequest will be
                 returned as a string.
                 If ``bytes``, the content will be returned as a
                 byte string.
                 If ``raw``, the raw response will be returned. In
                 this case the caller is responsible for iterating the
                 content and calling any functions in ``response.onclose``.

    :param _resolve: if ``True``, callable views will be resolved via the route
                     traversal mechanism, allowing routed arguments to be
                     set from the current request.

    :param _full: if ``True``, a full new request context will be set up
                  ensuring application hooks and middleware are called
                  for the subrequest. In this case any additional arguments
                  are passed to :meth:`~fresco.core.FrescoApp.requestcontext`
                  and used to populate the new request environ.

    :param args: Additional positional arguments

    :param kwargs: Additional keyword arguments
    """

    full = kwargs.pop('_full', False)
    resolve = kwargs.pop('_resolve', False)
    mode = kwargs.pop('_mode', 'str')

    if full or isinstance(view, string_types) and '/' in view:
        app = context.app
        if full:
            path = app.urlfor(view, *args, **kwargs)
        else:
            path = view
        with app.requestcontext(path, *args, **kwargs) as c:
            c.is_subrequest = True
            response = app.view()

    else:
        context.push(**dict(context.currentcontext(), is_subrequest=True))
        try:
            if not callable(view) or resolve:
                view, routed_args, routed_kwargs = \
                    resolve_viewspec(view, *args, **kwargs)

                # Use args/kwargs extracted during route traversal,
                args, kwargs = routed_args, routed_kwargs

            response = view(*args, **kwargs)
        finally:
            context.pop()

    if mode == 'str':
        return response_to_str(response)
    elif mode == 'bytes':
        return response_to_bytes(response)
    elif mode == 'raw':
        return response
    else:
        raise ValueError("Mode must be one of 'str', 'bytes', or 'raw'")


def subrequest_bytes(*args, **kwargs):
    """
    Perform a subrequest and return the response content as a byte string.
    See :func:`~fresco.subrequests.subrequest` for details
    """
    return subrequest(_mode='bytes', *args, **kwargs)


def subrequest_raw(*args, **kwargs):
    """
    Perform a subrequest and return the raw response object.
    See :func:`~fresco.subrequests.subrequest` for details
    """
    return subrequest(_mode='raw', *args, **kwargs)


def response_to_str(response):
    if not isinstance(response, Response):
        return response
    ct, params = parse_header(response.get_header('Content-Type'))
    encoding = params.get('charset')
    try:
        # Check for the common case that response.content is already a
        # string
        if isinstance(response.content, ustr):
            content = response.content

        # ...or a list of strings?
        elif (isinstance(response.content, list) and
                all(isinstance(i, ustr) for i in response.content)):
            content = ''.join(response.content)

        # Anything else: decode the content iterator
        else:
            content = b''.join(response.content_iterator)
            if not encoding:
                raise ValueError("No content encoding specified")
            content = content.decode(encoding)
    finally:
        if response.onclose:
            for f in response.onclose:
                f()
    if ct.startswith('text/html') or 'xml' in ct.split(';')[0]:
        return Markup(content)
    return content


def response_to_bytes(response):
    try:
        return b''.join(response.content_iterator)
    finally:
        if response.onclose:
            for f in response.onclose:
                f()


def resolve_viewspec(viewspec, *args, **kwargs):
    """
    :param viewspec: a view name, a reference in the form
    and used to
                        ``'package.module.viewfunction'``, or the view
                        callable itself.
    :param args: positional arguments for the route path traversal.
    :param kwargs: keyword arguments for the route path traversal.
    :return: A tuple of ``(<view callable>, args, kwargs)``.
             ```args`` and ``kwargs`` are the arguments to apply
             to ``view`` as extracted during the route traversal.
    """
    app = kwargs.pop('_routecollection', context.app)
    request = kwargs.pop('request', None) or context.request
    method = kwargs.pop('method', GET)
    args = list(args)

    if isinstance(viewspec, string_types) and ':' in viewspec:
        viewspec, remainder = viewspec.split(':', 1)
        delegated_route = app.routefor(viewspec)

        for k in delegated_route.routed_args_default:
            if k not in kwargs:
                v = delegated_route.routed_args_default[k]
                if callable(v):
                    v = v(request)
                kwargs[k] = v

        delegate_args, delegate_kwargs = _get_args_for_route(
            delegated_route, request, args, kwargs)

        rc = delegated_route.routecollectionfactory(*delegate_args,
                                                    **delegate_kwargs)
        return resolve_viewspec(remainder,
                                _routecollection=rc,
                                request=request,
                                method=method,
                                *args, **kwargs)

    route = app.routefor(viewspec)
    view = route.getview(method)
    args, kwargs = _get_args_for_route(route, request, args, kwargs)
    return view, args, kwargs


def _get_args_for_route(route, request, args, kwargs):
    """
    Return the args/kwargs required to pass to the view callable for ``route``.

    Mutates both args and kwargs
    """
    route_kwargs = {}
    route_args = list(chain(
        ((a(request) if isinstance(a, RouteArg) else a)
            for a in route.view_args),
        (args.pop(0) for a in route.pattern.segments
            if a.converter and not a.name)))

    required_kwargs = chain(route.view_kwargs.keys(),
                            (s.name
                             for s in route.pattern.segments
                             if s.converter and s.name))

    _marker = []
    for k in required_kwargs:
        v = kwargs.pop(k, _marker)
        if v is not _marker:
            route_kwargs[k] = v
            continue

        v = route.routed_args_default.get(k, _marker)
        if v is not _marker:
            if callable(v):
                v = v(request)
            route_kwargs[k] = v
            continue

        v = route.view_kwargs.get(k, _marker)
        if v is not _marker:
            v = v(request) if isinstance(v, RouteArg) else v
            route_kwargs[k] = v

    return route_args, route_kwargs
