# runtime.py
# Copyright (C) 2006, 2007, 2008, 2009, 2010 Michael Bayer mike_mp@zzzcomputing.com
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php

"""provides runtime services for templates, including Context, Namespace, and various helper functions."""

from mako import exceptions, util
import __builtin__, inspect, sys

class Context(object):
    """provides runtime namespace, output buffer, and various callstacks for templates."""
    def __init__(self, buffer, **data):
        self._buffer_stack = [buffer]
        self._orig = data  # original data, minus the builtins
        self._data = __builtin__.__dict__.copy() # the context data which includes builtins
        self._data.update(data)
        self._kwargs = data.copy()
        self._with_template = None
        self._outputting_as_unicode = None
        self.namespaces = {}
        
        # "capture" function which proxies to the generic "capture" function
        self._data['capture'] = lambda x, *args, **kwargs: capture(self, x, *args, **kwargs)
        
        # "caller" stack used by def calls with content
        self.caller_stack = self._data['caller'] = CallerStack()
        
    @property
    def lookup(self):
        return self._with_template.lookup
        
    @property
    def kwargs(self):
        return self._kwargs.copy()
    
    def push_caller(self, caller):
        self.caller_stack.append(caller)
        
    def pop_caller(self):
        del self.caller_stack[-1]
        
    def keys(self):
        return self._data.keys()
        
    def __getitem__(self, key):
        return self._data[key]

    def _push_writer(self):
        """push a capturing buffer onto this Context and return the new Writer function."""
        
        buf = util.FastEncodingBuffer()
        self._buffer_stack.append(buf)
        return buf.write

    def _pop_buffer_and_writer(self):
        """pop the most recent capturing buffer from this Context 
        and return the current writer after the pop.
        
        """

        buf = self._buffer_stack.pop()
        return buf, self._buffer_stack[-1].write
        
    def _push_buffer(self):
        """push a capturing buffer onto this Context."""
        
        self._push_writer()
        
    def _pop_buffer(self):
        """pop the most recent capturing buffer from this Context."""
        
        return self._buffer_stack.pop()
        
    def get(self, key, default=None):
        return self._data.get(key, default)
        
    def write(self, string):
        """write a string to this Context's underlying output buffer."""
        
        self._buffer_stack[-1].write(string)
        
    def writer(self):
        """return the current writer function"""

        return self._buffer_stack[-1].write

    def _copy(self):
        c = Context.__new__(Context)
        c._buffer_stack = self._buffer_stack
        c._data = self._data.copy()
        c._orig = self._orig
        c._kwargs = self._kwargs
        c._with_template = self._with_template
        c._outputting_as_unicode = self._outputting_as_unicode
        c.namespaces = self.namespaces
        c.caller_stack = self.caller_stack
        return c
    def locals_(self, d):
        """create a new Context with a copy of this Context's current state, updated with the given dictionary."""
        if len(d) == 0:
            return self
        c = self._copy()
        c._data.update(d)
        return c
    def _clean_inheritance_tokens(self):
        """create a new copy of this Context with tokens related to inheritance state removed."""
        c = self._copy()
        x = c._data
        x.pop('self', None)
        x.pop('parent', None)
        x.pop('next', None)
        return c

class CallerStack(list):
    def __init__(self):
        self.nextcaller = None
    def __nonzero__(self):
        return self._get_caller() and True or False
    def _get_caller(self):
        return self[-1]
    def __getattr__(self, key):
        return getattr(self._get_caller(), key)
    def _push_frame(self):
        self.append(self.nextcaller or None)
        self.nextcaller = None
    def _pop_frame(self):
        self.nextcaller = self.pop()
        
        
class Undefined(object):
    """represents an undefined value in a template."""
    def __str__(self):
        raise NameError("Undefined")
    def __nonzero__(self):
        return False

UNDEFINED = Undefined()

class _NSAttr(object):
    def __init__(self, parent):
        self.__parent = parent
    def __getattr__(self, key):
        ns = self.__parent
        while ns:
            if hasattr(ns.module, key):
                return getattr(ns.module, key)
            else:
                ns = ns.inherits
        raise AttributeError(key)    
    
class Namespace(object):
    """provides access to collections of rendering methods, which 
      can be local, from other templates, or from imported modules"""
    
    def __init__(self, name, context, module=None, 
                            template=None, templateuri=None, 
                            callables=None, inherits=None, 
                            populate_self=True, calling_uri=None):
        self.name = name
        if module is not None:
            mod = __import__(module)
            for token in module.split('.')[1:]:
                mod = getattr(mod, token)
            self._module = mod
        else:
            self._module = None
        if templateuri is not None:
            self.template = _lookup_template(context, templateuri, calling_uri)
            self._templateuri = self.template.module._template_uri
        else:
            self.template = template
            if self.template is not None:
                self._templateuri = self.template.module._template_uri
        self.context = context
        self.inherits = inherits
        if callables is not None:
            self.callables = dict([(c.func_name, c) for c in callables])
        else:
            self.callables = None
        if populate_self and self.template is not None:
            (lclcallable, lclcontext) = _populate_self_namespace(context, self.template, self_ns=self)
    
    @property
    def module(self):
        return self._module or self.template.module
    
    @property
    def filename(self):
        if self._module:
            return self._module.__file__
        else:
            return self.template.filename
    
    @property
    def uri(self):
        return self.template.uri

    @property
    def attr(self):
        if not hasattr(self, '_attr'):
            self._attr = _NSAttr(self)
        return self._attr

    def get_namespace(self, uri):
        """return a namespace corresponding to the given template uri.
        
        if a relative uri, it is adjusted to that of the template of this namespace"""
        key = (self, uri)
        if self.context.namespaces.has_key(key):
            return self.context.namespaces[key]
        else:
            ns = Namespace(uri, self.context._copy(), templateuri=uri, calling_uri=self._templateuri) 
            self.context.namespaces[key] = ns
            return ns
    
    def get_template(self, uri):
        return _lookup_template(self.context, uri, self._templateuri)
        
    def get_cached(self, key, **kwargs):
        if self.template:
            if not self.template.cache_enabled:
                createfunc = kwargs.get('createfunc', None)
                if createfunc:
                    return createfunc()
                else:
                    return None
                
            if self.template.cache_dir:
                kwargs.setdefault('data_dir', self.template.cache_dir)
            if self.template.cache_type:
                kwargs.setdefault('type', self.template.cache_type)
            if self.template.cache_url:
                kwargs.setdefault('url', self.template.cache_url)
        return self.cache.get(key, **kwargs)
    
    @property
    def cache(self):
        return self.template.cache
    
    def include_file(self, uri, **kwargs):
        """include a file at the given uri"""
        _include_file(self.context, uri, self._templateuri, **kwargs)
        
    def _populate(self, d, l):
        for ident in l:
            if ident == '*':
                for (k, v) in self._get_star():
                    d[k] = v
            else:
                d[ident] = getattr(self, ident)
    
    def _get_star(self):
        if self.callables:
            for key in self.callables:
                yield (key, self.callables[key])
        if self.template:
            def get(key):
                callable_ = self.template._get_def_callable(key)
                return lambda *args, **kwargs:callable_(self.context, *args, **kwargs)
            for k in self.template.module._exports:
                yield (k, get(k))
        if self._module:
            def get(key):
                callable_ = getattr(self._module, key)
                return lambda *args, **kwargs:callable_(self.context, *args, **kwargs)
            for k in dir(self._module):
                if k[0] != '_':
                    yield (k, get(k))
                            
    def __getattr__(self, key):
        if self.callables and key in self.callables:
            return self.callables[key]

        if self.template and self.template.has_def(key):
            callable_ = self.template._get_def_callable(key)
            return lambda *args, **kwargs:callable_(self.context, *args, **kwargs)

        if self._module and hasattr(self._module, key):
            callable_ = getattr(self._module, key)
            return lambda *args, **kwargs:callable_(self.context, *args, **kwargs)

        if self.inherits is not None:
            return getattr(self.inherits, key)
        raise AttributeError("Namespace '%s' has no member '%s'" % (self.name, key))

def supports_caller(func):
    """apply a caller_stack compatibility decorator to a plain Python function."""
    
    def wrap_stackframe(context,  *args, **kwargs):
        context.caller_stack._push_frame()
        try:
            return func(context, *args, **kwargs)
        finally:
            context.caller_stack._pop_frame()
    return wrap_stackframe
        
def capture(context, callable_, *args, **kwargs):
    """execute the given template def, capturing the output into a buffer."""
    
    if not callable(callable_):
        raise exceptions.RuntimeException(
                                "capture() function expects a callable as "
                                "its argument (i.e. capture(func, *args, **kwargs))"
                            )
    context._push_buffer()
    try:
        callable_(*args, **kwargs)
    finally:
        buf = context._pop_buffer()
    return buf.getvalue()

def _decorate_toplevel(fn):
    def decorate_render(render_fn):
        def go(context, *args, **kw):
            def y(*args, **kw):
                return render_fn(context, *args, **kw)
            try:
                y.__name__ = render_fn.__name__[7:]
            except TypeError:
                # < Python 2.4
                pass
            return fn(y)(context, *args, **kw)
        return go
    return decorate_render
    
def _decorate_inline(context, fn):
    def decorate_render(render_fn):
        dec = fn(render_fn)
        def go(*args, **kw):
            return dec(context, *args, **kw)
        return go
    return decorate_render
            
def _include_file(context, uri, calling_uri, **kwargs):
    """locate the template from the given uri and include it in the current output."""
    
    template = _lookup_template(context, uri, calling_uri)
    (callable_, ctx) = _populate_self_namespace(context._clean_inheritance_tokens(), template)
    callable_(ctx, **_kwargs_for_include(callable_, context._orig, **kwargs))
        
def _inherit_from(context, uri, calling_uri):
    """called by the _inherit method in template modules to set up the inheritance chain at the start
    of a template's execution."""
    if uri is None:
        return None
    template = _lookup_template(context, uri, calling_uri)
    self_ns = context['self']
    ih = self_ns
    while ih.inherits is not None:
        ih = ih.inherits
    lclcontext = context.locals_({'next':ih})
    ih.inherits = Namespace("self:%s" % template.uri, lclcontext, template = template, populate_self=False)
    context._data['parent'] = lclcontext._data['local'] = ih.inherits
    callable_ = getattr(template.module, '_mako_inherit', None)
    if callable_ is not None:
        ret = callable_(template, lclcontext)
        if ret:
            return ret

    gen_ns = getattr(template.module, '_mako_generate_namespaces', None)
    if gen_ns is not None:
        gen_ns(context)
    return (template.callable_, lclcontext)

def _lookup_template(context, uri, relativeto):
    lookup = context._with_template.lookup
    if lookup is None:
        raise exceptions.TemplateLookupException("Template '%s' has no TemplateLookup associated" % context._with_template.uri)
    uri = lookup.adjust_uri(uri, relativeto)
    try:
        return lookup.get_template(uri)
    except exceptions.TopLevelLookupException, e:
        raise exceptions.TemplateLookupException(str(e))

def _populate_self_namespace(context, template, self_ns=None):
    if self_ns is None:
        self_ns = Namespace('self:%s' % template.uri, context, template=template, populate_self=False)
    context._data['self'] = context._data['local'] = self_ns
    if hasattr(template.module, '_mako_inherit'):
        ret = template.module._mako_inherit(template, context)
        if ret:
            return ret
    return (template.callable_, context)

def _render(template, callable_, args, data, as_unicode=False):
    """create a Context and return the string output of the given template and template callable."""

    if as_unicode:
        buf = util.FastEncodingBuffer(unicode=True)
    elif template.output_encoding:
        buf = util.FastEncodingBuffer(
                        unicode=as_unicode, 
                        encoding=template.output_encoding, 
                        errors=template.encoding_errors)
    else:
        buf = util.StringIO()
    context = Context(buf, **data)
    context._outputting_as_unicode = as_unicode
    context._with_template = template
    
    _render_context(template, callable_, context, *args, **_kwargs_for_callable(callable_, data))
    return context._pop_buffer().getvalue()

def _kwargs_for_callable(callable_, data):
    argspec = inspect.getargspec(callable_)
    # for normal pages, **pageargs is usually present
    if argspec[2]:
        return data
    
    # for rendering defs from the top level, figure out the args
    namedargs = argspec[0] + [v for v in argspec[1:3] if v is not None]
    kwargs = {}
    for arg in namedargs:
        if arg != 'context' and arg in data and arg not in kwargs:
            kwargs[arg] = data[arg]
    return kwargs

def _kwargs_for_include(callable_, data, **kwargs):
    argspec = inspect.getargspec(callable_)
    namedargs = argspec[0] + [v for v in argspec[1:3] if v is not None]
    for arg in namedargs:
        if arg != 'context' and arg in data and arg not in kwargs:
            kwargs[arg] = data[arg]
    return kwargs
    
def _render_context(tmpl, callable_, context, *args, **kwargs):
    import mako.template as template
    # create polymorphic 'self' namespace for this template with possibly updated context
    if not isinstance(tmpl, template.DefTemplate):
        # if main render method, call from the base of the inheritance stack
        (inherit, lclcontext) = _populate_self_namespace(context, tmpl)
        _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)
    else:
        # otherwise, call the actual rendering method specified
        (inherit, lclcontext) = _populate_self_namespace(context, tmpl.parent)
        _exec_template(callable_, context, args=args, kwargs=kwargs)
        
def _exec_template(callable_, context, args=None, kwargs=None):
    """execute a rendering callable given the callable, a Context, and optional explicit arguments

    the contextual Template will be located if it exists, and the error handling options specified
    on that Template will be interpreted here.
    """
    template = context._with_template
    if template is not None and (template.format_exceptions or template.error_handler):
        error = None
        try:
            callable_(context, *args, **kwargs)
        except Exception, e:
            _render_error(template, context, e)
        except:                
            e = sys.exc_info()[0]
            _render_error(template, context, e)
    else:
        callable_(context, *args, **kwargs)


def _render_error(template, context, error):
    if template.error_handler:
        result = template.error_handler(context, error)
        if not result:
            raise error
    else:
        error_template = exceptions.html_error_template()
        if context._outputting_as_unicode:
            context._buffer_stack[:] = [util.FastEncodingBuffer(unicode=True)]
        else:
            context._buffer_stack[:] = [util.FastEncodingBuffer(
                                            error_template.output_encoding,
                                            error_template.encoding_errors)]
                                            
        context._with_template = error_template
        error_template.render_context(context, error=error)
