import re
from zope.interface import implementer

from pyramid.interfaces import IRoutesMapper, IRoute

from pyramid.compat import (
    PY2,
    native_,
    text_,
    text_type,
    string_types,
    binary_type,
    is_nonstr_iter,
    decode_path_info,
)

from pyramid.exceptions import URLDecodeError

from pyramid.traversal import quote_path_segment, split_path_info, PATH_SAFE

_marker = object()


@implementer(IRoute)
class Route(object):
    def __init__(
        self, name, pattern, factory=None, predicates=(), pregenerator=None
    ):
        self.pattern = pattern
        self.path = pattern  # indefinite b/w compat, not in interface
        self.match, self.generate = _compile_route(pattern)
        self.name = name
        self.factory = factory
        self.predicates = predicates
        self.pregenerator = pregenerator


@implementer(IRoutesMapper)
class RoutesMapper(object):
    def __init__(self):
        self.routelist = []
        self.static_routes = []

        self.routes = {}

    def has_routes(self):
        return bool(self.routelist)

    def get_routes(self, include_static=False):
        if include_static is True:
            return self.routelist + self.static_routes

        return self.routelist

    def get_route(self, name):
        return self.routes.get(name)

    def connect(
        self,
        name,
        pattern,
        factory=None,
        predicates=(),
        pregenerator=None,
        static=False,
    ):
        if name in self.routes:
            oldroute = self.routes[name]
            if oldroute in self.routelist:
                self.routelist.remove(oldroute)

        route = Route(name, pattern, factory, predicates, pregenerator)
        if not static:
            self.routelist.append(route)
        else:
            self.static_routes.append(route)

        self.routes[name] = route
        return route

    def generate(self, name, kw):
        return self.routes[name].generate(kw)

    def __call__(self, request):
        environ = request.environ
        try:
            # empty if mounted under a path in mod_wsgi, for example
            path = decode_path_info(environ['PATH_INFO'] or '/')
        except KeyError:
            path = '/'
        except UnicodeDecodeError as e:
            raise URLDecodeError(
                e.encoding, e.object, e.start, e.end, e.reason
            )

        for route in self.routelist:
            match = route.match(path)
            if match is not None:
                preds = route.predicates
                info = {'match': match, 'route': route}
                if preds and not all((p(info, request) for p in preds)):
                    continue
                return info

        return {'route': None, 'match': None}


# stolen from bobo and modified
old_route_re = re.compile(r'(\:[_a-zA-Z]\w*)')
star_at_end = re.compile(r'\*(\w*)$')

# The tortuous nature of the regex named ``route_re`` below is due to the
# fact that we need to support at least one level of "inner" squigglies
# inside the expr of a {name:expr} pattern.  This regex used to be just
# (\{[a-zA-Z][^\}]*\}) but that choked when supplied with e.g. {foo:\d{4}}.
route_re = re.compile(r'(\{[_a-zA-Z][^{}]*(?:\{[^{}]*\}[^{}]*)*\})')


def update_pattern(matchobj):
    name = matchobj.group(0)
    return '{%s}' % name[1:]


def _compile_route(route):
    # This function really wants to consume Unicode patterns natively, but if
    # someone passes us a bytestring, we allow it by converting it to Unicode
    # using the ASCII decoding.  We decode it using ASCII because we don't
    # want to accept bytestrings with high-order characters in them here as
    # we have no idea what the encoding represents.
    if route.__class__ is not text_type:
        try:
            route = text_(route, 'ascii')
        except UnicodeDecodeError:
            raise ValueError(
                'The pattern value passed to add_route must be '
                'either a Unicode string or a plain string without '
                'any non-ASCII characters (you provided %r).' % route
            )

    if old_route_re.search(route) and not route_re.search(route):
        route = old_route_re.sub(update_pattern, route)

    if not route.startswith('/'):
        route = '/' + route

    remainder = None
    if star_at_end.search(route):
        route, remainder = route.rsplit('*', 1)

    pat = route_re.split(route)

    # every element in "pat" will be Unicode (regardless of whether the
    # route_re regex pattern is itself Unicode or str)
    pat.reverse()
    rpat = []
    gen = []
    prefix = pat.pop()  # invar: always at least one element (route='/'+route)

    # We want to generate URL-encoded URLs, so we url-quote the prefix, being
    # careful not to quote any embedded slashes.  We have to replace '%' with
    # '%%' afterwards, as the strings that go into "gen" are used as string
    # replacement targets.
    gen.append(
        quote_path_segment(prefix, safe='/').replace('%', '%%')
    )  # native
    rpat.append(re.escape(prefix))  # unicode

    while pat:
        name = pat.pop()  # unicode
        name = name[1:-1]
        if ':' in name:
            # reg may contain colons as well,
            # so we must strictly split name into two parts
            name, reg = name.split(':', 1)
        else:
            reg = '[^/]+'
        gen.append('%%(%s)s' % native_(name))  # native
        name = '(?P<%s>%s)' % (name, reg)  # unicode
        rpat.append(name)
        s = pat.pop()  # unicode
        if s:
            rpat.append(re.escape(s))  # unicode
            # We want to generate URL-encoded URLs, so we url-quote this
            # literal in the pattern, being careful not to quote the embedded
            # slashes.  We have to replace '%' with '%%' afterwards, as the
            # strings that go into "gen" are used as string replacement
            # targets.  What is appended to gen is a native string.
            gen.append(quote_path_segment(s, safe='/').replace('%', '%%'))

    if remainder:
        rpat.append('(?P<%s>.*?)' % remainder)  # unicode
        gen.append('%%(%s)s' % native_(remainder))  # native

    pattern = ''.join(rpat) + '$'  # unicode

    match = re.compile(pattern).match

    def matcher(path):
        # This function really wants to consume Unicode patterns natively,
        # but if someone passes us a bytestring, we allow it by converting it
        # to Unicode using the ASCII decoding.  We decode it using ASCII
        # because we don't want to accept bytestrings with high-order
        # characters in them here as we have no idea what the encoding
        # represents.
        if path.__class__ is not text_type:
            path = text_(path, 'ascii')
        m = match(path)
        if m is None:
            return None
        d = {}
        for k, v in m.groupdict().items():
            # k and v will be Unicode 2.6.4 and lower doesnt accept unicode
            # kwargs as **kw, so we explicitly cast the keys to native
            # strings in case someone wants to pass the result as **kw
            nk = native_(k, 'ascii')
            if k == remainder:
                d[nk] = split_path_info(v)
            else:
                d[nk] = v
        return d

    gen = ''.join(gen)

    def q(v):
        return quote_path_segment(v, safe=PATH_SAFE)

    def generator(dict):
        newdict = {}
        for k, v in dict.items():
            if PY2:
                if v.__class__ is text_type:
                    # url_quote below needs bytes, not unicode on Py2
                    v = v.encode('utf-8')
            else:
                if v.__class__ is binary_type:
                    # url_quote below needs a native string, not bytes on Py3
                    v = v.decode('utf-8')

            if k == remainder:
                # a stararg argument
                if is_nonstr_iter(v):
                    v = '/'.join([q(x) for x in v])  # native
                else:
                    if v.__class__ not in string_types:
                        v = str(v)
                    v = q(v)
            else:
                if v.__class__ not in string_types:
                    v = str(v)
                # v may be bytes (py2) or native string (py3)
                v = q(v)

            # at this point, the value will be a native string
            newdict[k] = v

        result = gen % newdict  # native string result
        return result

    return matcher, generator
