from django.template import Library as DjangoLibrary, InvalidTemplateLibrary
from jinja2.ext import Extension as Jinja2Extension
import types
from coffin.interop import (
    DJANGO, JINJA2,
    guess_filter_type, jinja2_filter_to_django, django_filter_to_jinja2)


__all__ = ['Library']


class Library(DjangoLibrary):
    """Version of the Django ``Library`` class that can handle both
    Django template engine tags and filters, as well as Jinja2
    extensions and filters.

    Tries to present a common registration interface to the extension
    author, but provides both template engines with only those
    components they can support.

    Since custom Django tags and Jinja2 extensions are two completely
    different beasts, they are handled completely separately. You can
    register custom Django tags as usual, for example:

        register.tag('current_time', do_current_time)

    Or register a Jinja2 extension like this:

        register.tag(CurrentTimeNode)

    Filters, on the other hand, work similarily in both engines, and
    for the most one can't tell whether a filter function was written
    for Django or Jinja2. A compatibility layer is used to make to
    make the filters you register usuable with both engines:

        register.filter('cut', cut)

    However, some of the more powerful filters just won't work in
    Django, for example if more than one argument is required, or if
    context- or environmentfilters are used. If ``cut`` in the above
    example where such an extended filter, it would only be registered
    with Jinja.

    See also the module documentation for ``coffin.interop`` for
    information on some of the limitations of this conversion.

    TODO: Jinja versions of the ``simple_tag`` and ``inclusion_tag``
    helpers would be nice, though since custom tags are not needed as
    often in Jinja, this is not urgent.
    """

    def __init__(self):
        super(Library, self).__init__()
        self.jinja2_filters = {}
        self.jinja2_extensions = []
        self.jinja2_environment_attrs = {}
        self.jinja2_globals = {}
        self.jinja2_tests = {}

    @classmethod
    def from_django(cls, django_library):
        """Create a Coffin library object from a Django library.

        Specifically, this ensures that filters already registered
        with the Django library are also made available to Jinja,
        where applicable.
        """
        from copy import copy
        result = cls()
        result.tags = copy(django_library.tags)
        for name, func in django_library.filters.iteritems():
            result._register_filter(name, func, type='django')
        return result

    def test(self, name=None, func=None):
        def inner(f):
            name = getattr(f, "_decorated_function", f).__name__
            self.jinja2_tests[name] = f
            return f
        if name == None and func == None:
            # @register.test()
            return inner
        elif func == None:
            if (callable(name)):
                # register.test()
                return inner(name)
            else:
                # @register.test('somename') or @register.test(name='somename')
                def dec(func):
                    return self.test(name, func)
                return dec
        elif name != None and func != None:
            # register.filter('somename', somefunc)
            self.jinja2_tests[name] = func
            return func
        else:
            raise InvalidTemplateLibrary("Unsupported arguments to "
                "Library.test: (%r, %r)", (name, func))

    def object(self, name=None, func=None):
        def inner(f):
            name = getattr(f, "_decorated_function", f).__name__
            self.jinja2_globals[name] = f
            return f
        if name == None and func == None:
            # @register.object()
            return inner
        elif func == None:
            if (callable(name)):
                # register.object()
                return inner(name)
            else:
                # @register.object('somename') or @register.object(name='somename')
                def dec(func):
                    return self.object(name, func)
                return dec
        elif name != None and func != None:
            # register.object('somename', somefunc)
            self.jinja2_globals[name] = func
            return func
        else:
            raise InvalidTemplateLibrary("Unsupported arguments to "
                "Library.object: (%r, %r)", (name, func))

    def tag(self, name_or_node=None, compile_function=None, environment={}):
        """Register a Django template tag (1) or Jinja 2 extension (2).

        For (1), supports the same invocation syntax as the original
        Django version, including use as a decorator.

        For (2), since Jinja 2 extensions are classes (which can't be
        decorated), and have the tag name effectively built in, only the
        following syntax is supported:

            register.tag(MyJinjaExtensionNode)

        If your extension needs to be configured by setting environment
        attributes, you can can pass key-value pairs via ``environment``.
        """
        if isinstance(name_or_node, type) and issubclass(name_or_node, Jinja2Extension):
            if compile_function:
                raise InvalidTemplateLibrary('"compile_function" argument not supported for Jinja2 extensions')
            self.jinja2_extensions.append(name_or_node)
            self.jinja2_environment_attrs.update(environment)
            return name_or_node
        else:
            if environment:
                raise InvalidTemplateLibrary('"environment" argument not supported for Django tags')
            return super(Library, self).tag(name_or_node, compile_function)

    def tag_function(self, func_or_node):
        if not isinstance(func_or_node, types.FunctionType) and \
                issubclass(func_or_node, Jinja2Extension):
            self.jinja2_extensions.append(func_or_node)
            return func_or_node
        else:
            return super(Library, self).tag_function(func_or_node)

    def filter(self, name=None, filter_func=None, type=None, jinja2_only=None):
        """Register a filter with both the Django and Jinja2 template
        engines, if possible - or only Jinja2, if ``jinja2_only`` is
        specified. ``jinja2_only`` does not affect conversion of the
        filter if neccessary.

        Implements a compatibility layer to handle the different
        auto-escaping approaches transparently. Extended Jinja2 filter
        features like environment- and contextfilters are however not
        supported in Django. Such filters will only be registered with
        Jinja.

        If you know which template language the filter was written for,
        you may want to specify type="django" or type="jinja2", to disable
        the interop layer which in some cases might not be able to operate
        entirely opaque. For example, Jinja 2 filters may not receive a
        "Undefined" value if the interop layer is applied.

        Supports the same invocation syntax as the original Django
        version, including use as a decorator.

        If the function is supposed to return the registered filter
        (by example of the superclass implementation), but has
        registered multiple filters, a tuple of all filters is
        returned.
        """
        def filter_function(f):
            return self._register_filter(
                getattr(f, "_decorated_function", f).__name__,
                f, type=type, jinja2_only=jinja2_only)
        if name == None and filter_func == None:
            # @register.filter()
            return filter_function
        elif filter_func == None:
            if (callable(name)):
                # @register.filter
                return filter_function(name)
            else:
                # @register.filter('somename') or @register.filter(name='somename')
                def dec(func):
                    return self.filter(name, func, type=type,
                                       jinja2_only=jinja2_only)
                return dec
        elif name != None and filter_func != None:
            # register.filter('somename', somefunc)
            return self._register_filter(name, filter_func, type=type,
                jinja2_only=jinja2_only)
        else:
            raise InvalidTemplateLibrary("Unsupported arguments to "
                "Library.filter: (%r, %r)", (name, filter_func))

    def jinja2_filter(self, *args, **kwargs):
        """Shortcut for filter(type='jinja2').
        """
        kw = {'type': JINJA2}
        kw.update(kwargs)
        return self.filter(*args, **kw)

    def _register_filter(self, name, func, type=None, jinja2_only=None):
        assert type in (None, JINJA2, DJANGO,)

        # The user might not specify the language the filter was written
        # for, but sometimes we can auto detect it.
        filter_type, can_be_ported = guess_filter_type(func)
        assert not (filter_type and type) or filter_type == type, \
               "guessed filter type (%s) not matching claimed type (%s)" % (
                   filter_type, type,
               )
        if not filter_type and type:
            filter_type = type

        if filter_type == JINJA2:
            self.jinja2_filters[name] = func
            if can_be_ported and not jinja2_only:
                self.filters[name] = jinja2_filter_to_django(func)
            return func
        elif filter_type == DJANGO:
            self.filters[name] = func
            if not can_be_ported and jinja2_only:
                raise ValueError('This filter cannot be ported to Jinja2.')
            if can_be_ported:
                self.jinja2_filters[name] = django_filter_to_jinja2(func)
            return func
        else:
            django_func = jinja2_filter_to_django(func)
            jinja2_func = django_filter_to_jinja2(func)
            if jinja2_only:
                self.jinja2_filters[name] = jinja2_func
                return jinja2_func
            else:
                # register the filter with both engines
                self.filters[name] = django_func
                self.jinja2_filters[name] = jinja2_func
                return (django_func, jinja2_func)
