#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Router classes and methods
"""
 
import re
import sketch
 
#: Regex for URL definitions.
_ROUTE_REGEX = re.compile(r'''
    \<            # The exact character "<"
    (\w*)         # The optional variable name (restricted to a-z, 0-9, _)
    (?::([^>]*))? # The optional :regex part
    \>            # The exact character ">"
    ''', re.VERBOSE)
 
class BaseRoute(object):
    """Interface for URL routes. Custom routes must implement some or all
    methods and attributes from this class.
    """
    #: Route name, used to build URLs.
    name = None
    #: True if this route is only used for URL generation and never matches.
    build_only = False
 
    def match(self, request):
        """Matches this route against the current request.
 
        :param request:
            A ``webapp.Request`` instance.
        :returns:
            A tuple ``(handler, args, kwargs)`` if the route matches, or None.
        """
        raise NotImplementedError()
 
    def build(self, request, args, kwargs):
        """Builds and returns a URL for this route.
 
        :param request:
            The current ``Request`` object.
        :param args:
            Tuple of positional arguments to build the URL.
        :param kwargs:
            Dictionary of keyword arguments to build the URL.
        :returns:
            An absolute or relative URL.
        """
        raise NotImplementedError()
 
    def get_routes(self):
        """Generator to get all routes from a route.
 
        :yields:
            This route or all nested routes that it contains.
        """
        yield self
 
    def get_match_routes(self):
        """Generator to get all routes that can be matched from a route.
 
        :yields:
            This route or all nested routes that can be matched.
        """
        if not self.build_only:
            yield self
        elif not self.name:
            raise ValueError("Route %r is build_only but doesn't have a "
                "name" % self)
 
    def get_build_routes(self):
        """Generator to get all routes that can be built from a route.
 
        :yields:
            This route or all nested routes that can be built.
        """
        if self.name is not None:
            yield self
 
 
class SimpleRoute(BaseRoute):
    """A route that is compatible with webapp's routing. URL building is not
    implemented as webapp has rudimentar support for it, and this is the most
    unknown webapp feature anyway.
    """
    def __init__(self, template, handler):
        """Initializes a URL route.
 
        :param template:
            A regex to be matched.
        :param handler:
            A :class:`RequestHandler` class or dotted name for a class to be
            lazily imported, e.g., ``my.module.MyHandler``.
        """
        self.template = template
        self.handler = handler
        # Lazy property.
        self.regex = None
 
    def _regex(self):
        if not self.template.startswith('^'):
            self.template = '^' + self.template
 
        if not self.template.endswith('$'):
            self.template += '$'
 
        self.regex = re.compile(self.template)
        return self.regex
 
    def match(self, request):
        """Matches this route against the current request.
 
        .. seealso:: :meth:`BaseRoute.match`.
        """
        regex = self.regex or self._regex()
        match = regex.match(request.path)
        if match:
            return self.handler, match.groups(), {}
 
    def __repr__(self):
        return '<SimpleRoute(%r, %r)>' % (self.template, self.handler)
 
    __str__ = __repr__
 
 
class Route(BaseRoute):
    """A URL route definition. A route template contains parts enclosed by
    ``<>`` and is used to match requested URLs. Here are some examples::
 
        route = Route(r'/article/<id:[\d]+>', ArticleHandler)
        route = Route(r'/wiki/<page_name:\w+>', WikiPageHandler)
        route = Route(r'/blog/<year:\d{4}>/<month:\d{2}>/<day:\d{2}>/<slug:\w+>', BlogItemHandler)
 
    Based on `Another Do-It-Yourself Framework`_, by Ian Bicking. We added
    URL building, non-keyword variables and other improvements.
    """
    def __init__(self, template, handler=None, name=None, defaults=None,
        build_only=False):
        """Initializes a URL route.
 
        :param template:
            A route template to be matched, containing parts enclosed by ``<>``
            that can have only a name, only a regular expression or both:
 
              =============================  ==================================
              Format                         Example
              =============================  ==================================
              ``<name>``                     ``r'/<year>/<month>'``
              ``<:regular expression>``      ``r'/<:\d{4}>/<:\d{2}>'``
              ``<name:regular expression>``  ``r'/<year:\d{4}>/<month:\d{2}>'``
              =============================  ==================================
 
            If the name is set, the value of the matched regular expression
            is passed as keyword argument to the :class:`RequestHandler`.
            Otherwise it is passed as positional argument.
 
            The same template can mix parts with name, regular expression or
            both.
        :param handler:
            A :class:`RequestHandler` class or dotted name for a class to be
            lazily imported, e.g., ``my.module.MyHandler``.
        :param name:
            The name of this route, used to build URLs based on it.
        :param defaults:
            Default or extra keywords to be returned by this route. Values
            also present in the route variables are used to build the URL
            when they are missing.
        :param build_only:
            If True, this route never matches and is used only to build URLs.
        """
        self.template = template
        self.handler = handler
        self.name = name
        self.defaults = defaults or {}
        self.build_only = build_only
        # Lazy properties.
        self.regex = None
        self.variables = None
        self.reverse_template = None
 
    def _parse_template(self):
        self.variables = {}
        last = count = 0
        regex = reverse_template = ''
        for match in _ROUTE_REGEX.finditer(self.template):
            part = self.template[last:match.start()]
            name = match.group(1)
            expr = match.group(2) or '[^/]+'
            last = match.end()
 
            if not name:
                name = '__%d__' % count
                count += 1
 
            reverse_template += '%s%%(%s)s' % (part, name)
            regex += '%s(?P<%s>%s)' % (re.escape(part), name, expr)
            self.variables[name] = re.compile('^%s$' % expr)
 
        regex = '^%s%s$' % (regex, re.escape(self.template[last:]))
        self.regex = re.compile(regex)
        self.reverse_template = reverse_template + self.template[last:]
        self.has_positional_variables = count > 0
 
    def _regex(self):
        self._parse_template()
        return self.regex
 
    def _variables(self):
        self._parse_template()
        return self.variables
 
    def _reverse_template(self):
        self._parse_template()
        return self.reverse_template
 
    def match(self, request):
        """Matches this route against the current request.
 
        .. seealso:: :meth:`BaseRoute.match`.
        """
        regex = self.regex or self._regex()
        match = regex.match(request.path)
        if match:
            kwargs = self.defaults.copy()
            kwargs.update(match.groupdict())
            if kwargs and self.has_positional_variables:
                args = tuple(value[1] for value in sorted((int(key[2:-2]), \
                    kwargs.pop(key)) for key in \
                    kwargs.keys() if key.startswith('__')))
            else:
                args = ()
 
            return self.handler, args, kwargs
 
    def build(self, request, args, kwargs):
        """Builds a URL for this route.
 
        .. seealso:: :meth:`Router.build`.
        """
        full = kwargs.pop('_full', False)
        scheme = kwargs.pop('_scheme', None)
        netloc = kwargs.pop('_netloc', None)
        anchor = kwargs.pop('_anchor', None)
 
        if full or scheme or netloc:
            if not netloc:
                netloc = request.host
 
            if not scheme:
                scheme = 'http'
 
        path, query = self._build(args, kwargs)
        return urlunsplit(scheme, netloc, path, query, anchor)
 
    def _build(self, args, kwargs):
        """Builds the path for this route.
 
        :returns:
            A tuple ``(path, kwargs)`` with the built URL path and extra
            keywords to be used as URL query arguments.
        """
        variables = self.variables or self._variables()
        if self.has_positional_variables:
            for index, value in enumerate(args):
                key = '__%d__' % index
                if key in variables:
                    kwargs[key] = value
 
        values = {}
        for name, regex in variables.iteritems():
            value = kwargs.pop(name, self.defaults.get(name))
            if not value:
                raise KeyError('Missing argument "%s" to build URL.' % \
                    name.strip('_'))
 
            if not isinstance(value, basestring):
                value = str(value)
 
            if not regex.match(value):
                raise ValueError('URL buiding error: Value "%s" is not '
                    'supported for argument "%s".' % (value, name.strip('_')))
 
            values[name] = value
 
        return (self.reverse_template % values, kwargs)
 
    def __repr__(self):
        return '<Route(%r, %r, name=%r, defaults=%r, build_only=%r)>' % \
            (self.template, self.handler, self.name, self.defaults,
            self.build_only)
 
    __str__ = __repr__
 
 
class Router(object):
    """A simple URL router used to match the current URL, dispatch the handler
    and build URLs for other resources.
    """
    #: Class used when the route is a tuple. Default is compatible with webapp.
    route_class = SimpleRoute
 
    def __init__(self, app, routes=None):
        """Initializes the router.
 
        :param app:
            The :class:`WSGIApplication` instance.
        :param routes:
            A list of :class:`Route` instances to initialize the router.
        """
        self.app = app
        # Handler classes imported lazily.
        self._handlers = {}
        # All routes that can be matched.
        self.match_routes = []
        # All routes that can be built.
        self.build_routes = {}
        if routes:
            for route in routes:
                self.add(route)
 
    def add(self, route):
        """Adds a route to this router.
 
        :param route:
            A :class:`Route` instance.
        """
        if isinstance(route, tuple):
            # Simple route, compatible with webapp.
            route = self.route_class(*route)
 
        for r in route.get_match_routes():
            self.match_routes.append(r)
 
        for r in route.get_build_routes():
            self.build_routes[r.name] = r
 
    def match(self, request):
        """Matches all routes against the current request. The first one that
        matches is returned.
 
        :param request:
            A ``webapp.Request`` instance.
        :returns:
            A tuple ``(route, args, kwargs)`` if a route matched, or None.
        """
        for route in self.match_routes:
            match = route.match(request)
            if match:
                request.route = route
                request.route_args, request.route_kwargs = match[1], match[2]
                return match
 
    def dispatch(self, app, request, response, match, method=None):
        """Dispatches a request. This calls the :class:`RequestHandler` from
        the matched :class:`Route`.
 
        :param app:
            A :class:`WSGIApplication` instance.
        :param request:
            A ``webapp.Request`` instance.
        :param response:
            A :class:`Response` instance.
        :param match:
            A tuple ``(handler, args, kwargs)``, resulted from the matched
            route.
        :param method:
            Handler method to be called. In cases like exception handling, a
            method can be forced instead of using the request method.
        """
        handler_class, args, kwargs = match
        method = method or request.method.lower().replace('-', '_')
 
        # TODO : implement middleware
        # session_name = "sess"
        # if app.config.has_key('session_name'):
            # session_name = app.config['session_name']
 
 
 
        # TODO : fix plugins
        # if self.plugins_registered:
        #   if hasattr(handler, 'plugin_register'):
        #       for pl in self._Plugins:
        #           handler.plugin_register(pl, self._Plugins[pl])
        #   else:
        #       logging.error("Handler %s does not support plugin resitration" % handler.__name__)
        #
        # groups = match.groups()
        # handler.request.uri_groups = groups
 
        # if hasattr(handler_class, 'pre_hook'):
            # handler_class.pre_hook()
        # z = t
 
        if isinstance(handler_class, basestring):
            if handler_class not in self._handlers:
                self._handlers[handler_class] = import_string(handler_class)
 
            handler_class = self._handlers[handler_class]
 
        new_style_handler = True
        try:
            handler = handler_class(app, request, response)
 
            handler.session = sketch.Session(handler, 'sess')
            handler.config = app.config
        except TypeError, e:
            # Support webapp's initialize().
            new_style_handler = False
            handler = handler_class()
            handler.initialize(request, response)
 
            handler.session = sketch.Session(handler, 'sess')
            handler.config = app.config
 
        try:
            if new_style_handler:
                handler(method, *args, **kwargs)
            else:
                # Support webapp handlers which don't implement __call__().
                getattr(handler, method)(*args)
        except Exception, e:
            if method == 'handle_exception':
                # We are already handling an exception.
                raise
 
            # If the handler implements exception handling, let it handle it.
            handler.handle_exception(e, app.debug)
 
    def build(self, name, request, args, kwargs):
        """Builds and returns a URL for a named :class:`Route`.
 
        :param name:
            The route name.
        :param request:
            The current ``Request`` object.
        :param args:
            Tuple of positional arguments to build the URL.
        :param kwargs:
            Dictionary of keyword arguments to build the URL.
        :returns:
            An absolute or relative URL.
 
        .. seealso:: :meth:`RequestHandler.url_for`.
        """
        route = self.build_routes.get(name)
        if not route:
            raise KeyError('Route "%s" is not defined.' % name)
 
        return route.build(request, args, kwargs)
 
    def __repr__(self):
        routes = self.match_routes + [v for k, v in \
            self.build_routes.iteritems() if v not in self.match_routes]
 
        return '<Router(%r)>' % routes
 
    __str__ = __repr__