import warnings
import jinja2
from jinja2.ext import Extension
from jinja2 import nodes
from webassets import Bundle
from webassets.loaders import GlobLoader, LoaderError
from webassets.exceptions import ImminentDeprecationWarning


__all__ = ('assets', 'Jinja2Loader',)


class AssetsExtension(Extension):
    """
    As opposed to the Django tag, this tag is slightly more capable due
    to the expressive powers inherited from Jinja. For example:

        {% assets "src1.js", "src2.js", get_src3(),
                  filter=("jsmin", "gzip"), output=get_output() %}
        {% endassets %}
    """

    tags = set(['assets'])

    BundleClass = Bundle   # Helpful for mocking during tests.

    def __init__(self, environment):
        super(AssetsExtension, self).__init__(environment)

        # Add the defaults to the environment
        environment.extend(
            assets_environment=None,
        )

    def parse(self, parser):
        lineno = next(parser.stream).lineno

        files = []
        output = nodes.Const(None)
        filters = nodes.Const(None)
        dbg = nodes.Const(None)
        depends = nodes.Const(None)

        # Parse the arguments
        first = True
        while parser.stream.current.type != 'block_end':
            if not first:
                parser.stream.expect('comma')
            first = False

            # Lookahead to see if this is an assignment (an option)
            if parser.stream.current.test('name') and parser.stream.look().test('assign'):
                name = next(parser.stream).value
                parser.stream.skip()
                value = parser.parse_expression()
                if name == 'filters':
                    filters = value
                elif name == 'filter':
                    filters = value
                    warnings.warn('The "filter" option of the {%% assets %%} '
                                  'template tag has been renamed to '
                                  '"filters" for consistency reasons '
                                  '(line %s).' % lineno,
                                    ImminentDeprecationWarning)
                elif name == 'output':
                    output = value
                elif name == 'debug':
                    dbg = value
                elif name == 'depends':
                    depends = value
                else:
                    parser.fail('Invalid keyword argument: %s' % name)
            # Otherwise assume a source file is given, which may be any
            # expression, except note that strings are handled separately above.
            else:
                expression = parser.parse_expression()
                if isinstance(expression, (nodes.List, nodes.Tuple)):
                    files.extend(expression.iter_child_nodes())
                else:
                    files.append(expression)

        # Parse the contents of this tag
        body = parser.parse_statements(['name:endassets'], drop_needle=True)

        # We want to make some values available to the body of our tag.
        # Specifically, the file url(s) (ASSET_URL), and any extra dict set in
        # the bundle (EXTRA).
        #
        # A short interlope: I would have preferred to make the values of the
        # extra dict available directly. Unfortunately, the way Jinja2 does
        # things makes this problematic. I'll explain.
        #
        # Jinja2 generates Python code from it's AST which it then executes.
        # So the way extensions implement making custom variables available to
        # a block of code is by generating a ``CallBlock``, which essentially
        # wraps our child nodes in a Python function. The arguments of this
        # function are the values that are available to our tag contents.
        #
        # But we need to generate this ``CallBlock`` now, during parsing, and
        # right now we don't know the actual ``Bundle.extra`` values yet. We
        # only resolve the bundle during rendering!
        #
        # This would easily be solved if Jinja2 where to allow extensions to
        # scope it's context, which is a dict of values that templates can
        # access, just like in Django (you might see on occasion
        # ``context.resolve('foo')`` calls in Jinja2's generated code).
        # However, it seems the context is essentially only for the initial
        # set of data passed to render(). There are some statements by Armin
        # that this might change at some point, but I've run into this problem
        # before, and I'm not holding my breath.
        #
        # I **really** did try to get around this, including crazy things like
        # inserting custom Python code by patching the tag child nodes::
        #
        #        rv = object.__new__(nodes.InternalName)
        #        # l_EXTRA is the argument we defined for the CallBlock/Macro
        #        # Letting Jinja define l_kwargs is also possible
        #        nodes.Node.__init__(rv, '; context.vars.update(l_EXTRA)',
        #                            lineno=lineno)
        #        # Scope required to ensure our code on top
        #        body = [rv, nodes.Scope(body)]
        #
        # This custom code would run at the top of the function in which the
        # CallBlock node would wrap the code generated from our tag's child
        # nodes. Note that it actually does works, but doesn't clear the values
        # at the end of the scope).
        #
        # If it is possible to do this, it certainly isn't reasonable/
        #
        # There is of course another option altogether: Simple resolve the tag
        # definition to a bundle right here and now, thus get access to the
        # extra dict, make all values arguments to the CallBlock (Limited to
        # 255 arguments to a Python function!). And while that would work fine
        # in 99% of cases, it wouldn't be correct. The compiled template could
        # be cached and used with different bundles/environments, and this
        # would require the bundle to resolve at parse time, and hardcode it's
        # extra values.
        #
        # Interlope end.
        #
        # Summary: We have to be satisfied with a single EXTRA variable.
        args = [nodes.Name('ASSET_URL', 'param'),
                nodes.Name('ASSET_SRI', 'param'),
                nodes.Name('EXTRA', 'param')]

        # Return a ``CallBlock``, which means Jinja2 will call a Python method
        # of ours when the tag needs to be rendered. That method can then
        # render the template body.
        call = self.call_method(
            # Note: Changing the args here requires updating ``Jinja2Loader``
            '_render_assets', args=[filters, output, dbg, depends, nodes.List(files)])
        call_block = nodes.CallBlock(call, args, [], body)
        call_block.set_lineno(lineno)
        return call_block

    @classmethod
    def resolve_contents(cls, contents, env):
        """Resolve bundle names."""
        result = []
        for f in contents:
            try:
                result.append(env[f])
            except KeyError:
                result.append(f)
        return result

    def _render_assets(self, filter, output, dbg, depends, files, caller=None):
        env = self.environment.assets_environment
        if env is None:
            raise RuntimeError('No assets environment configured in '+
                               'Jinja2 environment')

        # Construct a bundle with the given options
        bundle_kwargs = {
            'output': output,
            'filters': filter,
            'debug': dbg,
            'depends': depends
        }
        bundle = self.BundleClass(
            *self.resolve_contents(files, env), **bundle_kwargs)

        # Retrieve urls (this may or may not cause a build)
        with bundle.bind(env):
            urls = bundle.urls(calculate_sri=True)

        # For each url, execute the content of this template tag (represented
        # by the macro ```caller`` given to use by Jinja2).
        result = u""
        for entry in urls:
            if isinstance(entry, dict):
                result += caller(entry['uri'], entry.get('sri', None), bundle.extra)
            else:
                result += caller(entry, None, bundle.extra)
        return result


assets = AssetsExtension  # nicer import name


class Jinja2Loader(GlobLoader):
    """Parse all the Jinja2 templates in the given directory, try to
    find bundles in active use.

    Try all the given environments to parse the template, until we
    succeed.
    """

    def __init__(self, assets_env, directories, jinja2_envs, charset='utf8', jinja_ext='*.html'):
        self.asset_env = assets_env
        self.directories = directories
        self.jinja2_envs = jinja2_envs
        self.charset = charset
        self.jinja_ext = jinja_ext

    def load_bundles(self):
        bundles = []
        for template_dir in self.directories:
            for filename in self.glob_files((template_dir, self.jinja_ext)):
                bundles.extend(self.with_file(filename, self._parse) or [])
        return bundles

    def _parse(self, filename, contents):
        for i, env in enumerate(self.jinja2_envs):
            try:
                t = env.parse(contents.decode(self.charset))
            except jinja2.exceptions.TemplateSyntaxError as e:
                #print ('jinja parser (env %d) failed: %s'% (i, e))
                pass
            else:
                result = []
                def _recurse_node(node_to_search):
                    for node in node_to_search.iter_child_nodes():
                        if isinstance(node, jinja2.nodes.Call):
                            if isinstance(node.node, jinja2.nodes.ExtensionAttribute)\
                               and node.node.identifier == AssetsExtension.identifier:
                                filter, output, dbg, depends, files = node.args
                                bundle = Bundle(
                                    *AssetsExtension.resolve_contents(files.as_const(), self.asset_env),
                                    **{
                                        'output': output.as_const(),
                                        'depends': depends.as_const(),
                                        'filters': filter.as_const()})
                                result.append(bundle)
                        else:
                            _recurse_node(node)
                for node in t.iter_child_nodes():
                    _recurse_node(node)
                return result
        else:
            raise LoaderError('Jinja parser failed on %s, tried %d environments' % (
                filename, len(self.jinja2_envs)))
        return False
