"""Adapter for using Jinja2 with Django."""

from __future__ import unicode_literals

import functools
import logging
import re

from django.apps import apps
from django.conf import settings
from django.template.base import Origin, TemplateDoesNotExist
from django.template.loader import BaseLoader
from django.utils.importlib import import_module

try:
    import importlib.util
    if hasattr(importlib.util, 'find_spec'):  # Py3>=3.4
        def has_helpers(path):
            return importlib.util.find_spec('helpers', path) is not None
    else:  # Py3<3.4
        def has_helpers(path):
            # For Python 3.3, just try to import the module. Unfortunately,
            # this changes the contract slightly for Python 3.3: if there is an
            # module but this raises a legitimate ImportError, jingo will act
            # as if the module doesn't exist. The intent is that we raise
            # legitimate ImportErrors but ignore missing modules.
            try:
                import_module('helpers', path)
                return True
            except ImportError:
                return False
except ImportError:
    import imp

    def has_helpers(path):
        try:
            imp.find_module('helpers', path)
            return True
        except ImportError:
            return False

import jinja2

try:
    from django.template.engine import Engine

    has_engine = True

    def get_standard_processors():
        return Engine.get_default().template_context_processors

except ImportError:
    from django.template.context import get_standard_processors
    has_engine = False

VERSION = (0, 8, 0)
__version__ = '.'.join(map(str, VERSION))

EXCLUDE_APPS = (
    'admin',
    'admindocs',
    'registration',
    'context_processors',
)

log = logging.getLogger('jingo')

_helpers_loaded = False


class Template(jinja2.Template):

    def render(self, context={}):
        """Render's a template, context can be a Django Context or a
        dictionary.
        """
        # flatten the Django Context into a single dictionary.
        context_dict = {}
        if hasattr(context, 'dicts'):
            for d in context.dicts:
                context_dict.update(d)
        else:
            context_dict = context

            # Django Debug Toolbar needs a RequestContext-like object in order
            # to inspect context.
            class FakeRequestContext:
                dicts = [context]
            context = FakeRequestContext()

        # Used by debug_toolbar.
        if settings.TEMPLATE_DEBUG:
            from django.test import signals
            self.origin = Origin(self.filename)
            signals.template_rendered.send(sender=self, template=self,
                                           context=context)

        return super(Template, self).render(context_dict)


class Environment(jinja2.Environment):
    template_class = Template

    def get_template(self, name, parent=None, globals=None):
        """Make sure our helpers get loaded before any templates."""
        load_helpers()
        return super(Environment, self).get_template(name, parent, globals)

    def from_string(self, source, globals=None, template_class=None):
        load_helpers()
        return super(Environment, self).from_string(source, globals,
                                                    template_class)


_env = None


def get_env():
    """Configure and return a jinja2 Environment."""
    global _env
    if _env:
        return _env
    # Mimic Django's setup by loading templates from directories in
    # TEMPLATE_DIRS and packages in INSTALLED_APPS.
    loaders = [jinja2.FileSystemLoader(d) for d in settings.TEMPLATE_DIRS]
    loaders += [jinja2.PackageLoader(c.name) for c in apps.get_app_configs()]

    opts = {
        'trim_blocks': True,
        'extensions': ['jinja2.ext.i18n'],
        'autoescape': True,
        'auto_reload': settings.DEBUG,
        'loader': jinja2.ChoiceLoader(loaders),
    }

    if hasattr(settings, 'JINJA_CONFIG'):
        if hasattr(settings.JINJA_CONFIG, '__call__'):
            config = settings.JINJA_CONFIG()
        else:
            config = settings.JINJA_CONFIG
        opts.update(config)

    e = Environment(**opts)
    # Install null translations since gettext isn't always loaded up during
    # testing.
    if ('jinja2.ext.i18n' in e.extensions or
            'jinja2.ext.InternationalizationExtension' in e.extensions):
        e.install_null_translations()
    _env = e
    return e


def render_to_string(request, template, context=None):
    """
    Render a template into a string.
    """
    def get_context():
        c = {} if context is None else context.copy()
        for processor in get_standard_processors():
            c.update(processor(request))
        return c

    # If it's not a Template, it must be a path to be loaded.
    if not isinstance(template, jinja2.environment.Template):
        template = get_env().get_template(template)

    return template.render(get_context())


def load_helpers():
    """Try to import ``helpers.py`` from each app in INSTALLED_APPS."""
    # We want to wait as long as possible to load helpers so there aren't any
    # weird circular imports with jingo.
    global _helpers_loaded
    if _helpers_loaded:
        return
    _helpers_loaded = True

    from jingo import helpers  # noqa

    for config in apps.get_app_configs():
        if not has_helpers(config.name):
            continue

        import_module('%s.helpers' % config.name)


class Register(object):
    """Decorators to add filters and functions to the template Environment."""
    def filter(self, f=None, override=True):
        """Adds the decorated function to Jinja's filter library."""
        def decorator(f):
            @functools.wraps(f)
            def wrapper(*args, **kw):
                return f(*args, **kw)
            return self.filter(wrapper, override)

        if not f:
            return decorator
        if override or f.__name__ not in get_env().filters:
            get_env().filters[f.__name__] = f
        return f

    def function(self, f=None, override=True):
        """Adds the decorated function to Jinja's global namespace."""
        def decorator(f):
            @functools.wraps(f)
            def wrapper(*args, **kw):
                return f(*args, **kw)
            return self.function(wrapper, override)

        if not f:
            return decorator
        if override or f.__name__ not in get_env().globals:
            get_env().globals[f.__name__] = f
        return f

    def inclusion_tag(self, template):
        """Adds a function to Jinja, but like Django's @inclusion_tag."""
        def decorator(f):
            @functools.wraps(f)
            def wrapper(*args, **kw):
                context = f(*args, **kw)
                t = get_env().get_template(template).render(context)
                return jinja2.Markup(t)
            return self.function(wrapper)
        return decorator

register = Register()


class Loader(BaseLoader):
    is_usable = True

    def __init__(self):
        if has_engine:
            super(Loader, self).__init__(Engine.get_default())
        else:
            super(Loader, self).__init__()
        include_pattern = getattr(settings, 'JINGO_INCLUDE_PATTERN', None)
        if include_pattern:
            self.include_re = re.compile(include_pattern)
        else:
            self.include_re = None

    def _valid_template(self, template_name):
        if self.include_re:
            if not self.include_re.search(template_name):
                return False

        if hasattr(template_name, 'split'):
            app = template_name.split('/')[0]
            if app in getattr(settings, 'JINGO_EXCLUDE_APPS', EXCLUDE_APPS):
                return False

        return True

    def load_template(self, template_name, template_dirs=None):
        if not self._valid_template(template_name):
            raise TemplateDoesNotExist(template_name)

        try:
            template = get_env().get_template(template_name)
            return template, template.filename
        except jinja2.TemplateNotFound:
            raise TemplateDoesNotExist(template_name)

    def load_template_source(self, template_name, template_dirs=None):
        if not self._valid_template(template_name):
            raise TemplateDoesNotExist(template_name)

        try:
            template = get_env().get_template(template_name)
        except jinja2.TemplateNotFound:
            raise TemplateDoesNotExist(template_name)

        with open(template.filename, 'rb') as fp:
            return (fp.read().decode(settings.FILE_CHARSET), template.filename)
