"""
simple, elegant templating
(part of web.py)
"""

import re, glob, os, os.path
from types import FunctionType as function
from utils import storage, group, utf8
from net import websafe

# differences from python:
#  - for: has an optional else: that gets called if the loop never runs
# differences to add:
#  - you can use the expression inside if, while blocks
#  - special for loop attributes, like django?
#  - you can check to see if a variable is defined (perhaps w/ get func?)
# all these are probably good ideas for python...

# todo:
#  inline tuple
#  relax constraints on spacing
#  continue, break, etc.
#  tracebacks

global_globals = {'None':None, 'False':False, 'True': True}
MAX_ITERS = 100000

WHAT = 0
ARGS = 4
KWARGS = 6
NAME = 2
BODY = 4
CLAUSE = 2
ELIF = 6
ELSE = 8
IN = 6
NAME = 2
EXPR = 4
FILTER = 4
THING = 2
ATTR = 4
ITEM = 4
NEGATE = 4
X = 2
OP = 4
Y = 6
LINENO = -1

# http://docs.python.org/ref/identifiers.html
r_var = '[a-zA-Z_][a-zA-Z0-9_]*'

class ParseError(Exception): pass
class Parser:
    def __init__(self, text, name=""):
        self.t = text
        self.p = 0
        self._lock = [False]
        self.name = name

    def lock(self):
        self._lock[-1] = True

    def curline(self):
        return self.t[:self.p].count('\n')+1

    def csome(self):
        return repr(self.t[self.p:self.p+5]+'...')

    def Error(self, x, y=None):
        if y is None: y = self.csome()
        raise ParseError, "%s: expected %s, got %s (line %s)" % (self.name, x, y, self.curline())

    def q(self, f):
        def internal(*a, **kw):
            checkp = self.p
            self._lock.append(False)
            try:
                q = f(*a, **kw)
            except ParseError:
                if self._lock[-1]:
                    raise
                self.p = checkp
                self._lock.pop()
                return False
            self._lock.pop()
            return q or True
        return internal

    def tokr(self, t):
        text = self.c(len(t))
        if text != t:
            self.Error(repr(t), repr(text))
        return t

    def ltokr(self, *l):
        for x in l:
            o = self.tokq(x)
            if o: return o
        self.Error('one of '+repr(l))

    def rer(self, r):
        x = re.match(r, self.t[self.p:]) #@@re_compile
        if not x:
            self.Error('r'+repr(r))
        return self.tokr(x.group())

    def endr(self):
        if self.p != len(self.t):
            self.Error('EOF')

    def c(self, n=1):
        out = self.t[self.p:self.p+n]
        if out == '' and n != 0:
            self.Error('character', 'EOF')
        self.p += n
        return out

    def lookbehind(self, t):
        return self.t[self.p-len(t):self.p] == t

    def __getattr__(self, a):
        if a.endswith('q'):
            return self.q(getattr(self, a[:-1]+'r'))
        raise AttributeError, a

class TemplateParser(Parser):
    def __init__(self, *a, **kw):
        Parser.__init__(self, *a, **kw)
        self.curws = ''
        self.curind = ''

    def o(self, *a):
        return a+('lineno', self.curline())

    def go(self):
        # maybe try to do some traceback parsing/hacking
        return self.gor()

    def gor(self):
        header = self.defwithq()
        results = self.lines(start=True)
        self.endr()
        return header, results

    def ws(self):
        n = 0
        while self.tokq(" "): n += 1
        return " " * n

    def defwithr(self):
        self.tokr('$def with ')
        self.lock()
        self.tokr('(')
        args = []
        kw = []
        x = self.req(r_var)
        while x:
            if self.tokq('='):
                v = self.exprr()
                kw.append((x, v))
            else:
                args.append(x)
            x = self.tokq(', ') and self.req(r_var)
        self.tokr(')\n')
        return self.o('defwith', 'null', None, 'args', args, 'kwargs', kw)

    def literalr(self):
        o = (
          self.req('"[^"]*"') or #@@ no support for escapes
          self.req("'[^']*'")
        )
        if o is False:
            o = self.req('\-?[0-9]+(\.[0-9]*)?')
            if o is not False:
                if '.' in o: o = float(o)
                else: o = int(o)

        if o is False: self.Error('literal')
        return self.o('literal', 'thing', o)

    def listr(self):
        self.tokr('[')
        self.lock()
        x = []
        if not self.tokq(']'):
            while True:
                t = self.exprr()
                x.append(t)
                if not self.tokq(', '): break
            self.tokr(']')
        return self.o('list', 'thing', x)

    def dictr(self):
        self.tokr('{')
        self.lock()
        x = {}
        if not self.tokq('}'):
            while True:
                k = self.exprr()
                self.tokr(': ')
                v = self.exprr()
                x[k] = v
                if not self.tokq(', '): break
            self.tokr('}')
        return self.o('dict', 'thing', x)

    def parenr(self):
        self.tokr('(')
        self.lock()
        o = self.exprr() # todo: allow list
        self.tokr(')')
        return self.o('paren', 'thing', o)

    def atomr(self):
        """returns var, literal, paren, dict, or list"""
        o = (
          self.varq() or
          self.parenq() or
          self.dictq() or
          self.listq() or
          self.literalq()
        )
        if o is False: self.Error('atom')
        return o

    def primaryr(self):
        """returns getattr, call, or getitem"""
        n = self.atomr()
        while 1:
            if self.tokq('.'):
                v = self.req(r_var)
                if not v:
                    self.p -= 1 # get rid of the '.'
                    break
                else:
                    n = self.o('getattr', 'thing', n, 'attr', v)
            elif self.tokq('('):
                args = []
                kw = []

                while 1:
                    # need to see if we're doing a keyword argument
                    checkp = self.p
                    k = self.req(r_var)
                    if k and self.tokq('='): # yup
                        v = self.exprr()
                        kw.append((k, v))
                    else:
                        self.p = checkp
                        x = self.exprq()
                        if x: # at least it's something
                            args.append(x)
                        else:
                            break

                    if not self.tokq(', '): break
                self.tokr(')')
                n = self.o('call', 'thing', n, 'args', args, 'kwargs', kw)
            elif self.tokq('['):
                v = self.exprr()
                self.tokr(']')
                n = self.o('getitem', 'thing', n, 'item', v)
            else:
                break

        return n

    def exprr(self):
        negate = self.tokq('not ')
        x = self.primaryr()
        if self.tokq(' '):
            operator = self.ltokr('not in', 'in', 'is not', 'is', '==', '!=', '>=', '<=', '<', '>', 'and', 'or', '*', '+', '-', '/', '%')
            self.tokr(' ')
            y = self.exprr()
            x = self.o('test', 'x', x, 'op', operator, 'y', y)

        return self.o('expr', 'thing', x, 'negate', negate)

    def varr(self):
        return self.o('var', 'name', self.rer(r_var))

    def liner(self):
        out = []
        o = self.curws
        while 1:
            c = self.c()
            self.lock()
            if c == '\n':
                self.p -= 1
                break
            if c == '$':
                if self.lookbehind('\\$'):
                    o = o[:-1] + c
                else:
                    filter = not bool(self.tokq(':'))

                    if self.tokq('{'):
                        out.append(o)
                        out.append(self.o('itpl', 'name', self.exprr(), 'filter', filter))
                        self.tokr('}')
                        o = ''
                    else:
                        g = self.primaryq()
                        if g:
                            out.append(o)
                            out.append(self.o('itpl', 'name', g, 'filter', filter))
                            o = ''
                        else:
                            o += c
            else:
                o += c
        self.tokr('\n')
        if not self.lookbehind('\\\n'):
            o += '\n'
        else:
            o = o[:-1]
        out.append(o)
        return self.o('line', 'thing', out)

    def varsetr(self):
        self.tokr('$var ')
        self.lock()
        what = self.rer(r_var)
        self.tokr(':')
        body = self.lines()
        return self.o('varset', 'name', what, 'body', body)

    def ifr(self):
        self.tokr("$if ")
        self.lock()
        expr = self.exprr()
        self.tokr(":")
        ifc = self.lines()

        elifs = []
        while self.tokq(self.curws + self.curind + '$elif '):
            v = self.exprr()
            self.tokr(':')
            c = self.lines()
            elifs.append(self.o('elif', 'clause', v, 'body', c))

        if self.tokq(self.curws + self.curind + "$else:"):
            elsec = self.lines()
        else:
            elsec = None

        return self.o('if', 'clause', expr, 'then', ifc, 'elif', elifs, 'else', elsec)

    def forr(self):
        self.tokr("$for ")
        self.lock()
        v = self.setabler()
        self.tokr(" in ")
        g = self.exprr()
        self.tokr(":")
        l = self.lines()

        if self.tokq(self.curws + self.curind + '$else:'):
            elsec = self.lines()
        else:
            elsec = None

        return self.o('for', 'name', v, 'body', l, 'in', g, 'else', elsec)

    def whiler(self):
        self.tokr('$while ')
        self.lock()
        v = self.exprr()
        self.tokr(":")
        l = self.lines()

        if self.tokq(self.curws + self.curind + '$else:'):
            elsec = self.lines()
        else:
            elsec = None

        return self.o('while', 'clause', v, 'body', l, 'null', None, 'else', elsec)

    def assignr(self):
        self.tokr('$ ')
        assign = self.rer(r_var) # NOTE: setable
        self.tokr(' = ')
        expr = self.exprr()
        self.tokr('\n')

        return self.o('assign', 'name', assign, 'expr', expr)

    def commentr(self):
        self.tokr('$#')
        self.lock()
        while self.c() != '\n': pass
        return self.o('comment')

    def setabler(self):
        out = [self.varr()] #@@ not quite right
        while self.tokq(', '):
             out.append(self.varr())
        return out

    def lines(self, start=False):
        """
        This function gets called from two places:
          1. at the start, where it's matching the document itself
          2. after any command, where it matches one line or an indented block
        """
        o = []
        if not start: # try to match just one line
            singleline = self.tokq(' ') and self.lineq()
            if singleline:
                return [singleline]
            else:
                self.rer(' *') #@@slurp space?
                self.tokr('\n')
                oldind = self.curind
                self.curind += '    '
        while 1:
            oldws = self.curws
            t = self.tokq(oldws + self.curind)
            if not t: break

            self.curws += self.ws()
            x = t and (
              self.varsetq() or
              self.ifq() or
              self.forq() or
              self.whileq() or
              self.assignq() or
              self.commentq() or
              self.lineq())
            self.curws = oldws
            if not x:
                break
            elif x[WHAT] == 'comment':
                pass
            else:
                o.append(x)

        if not start: self.curind = oldind
        return o

class Stowage(storage):
    def __str__(self): return self.get('_str')
    #@@ edits in place
    def __add__(self, other):
        if isinstance(other, (unicode, str)):
            self._str += other
            return self
        else:
            raise TypeError, 'cannot add'
    def __radd__(self, other):
        if isinstance(other, (unicode, str)):
            self._str = other + self._str
            return self
        else:
            raise TypeError, 'cannot add'

class WTF(AssertionError): pass
class SecurityError(Exception):
    """The template seems to be trying to do something naughty."""
    pass




Required = object()
class Template:
    globals = {}
    content_types = {
        '.html' : 'text/html; charset=utf-8',
        '.txt' : 'text/plain',
    }

    def __init__(self, text, filter=None, filename=""):
        self.filter = filter
        self.filename = filename
        # universal newlines:
        text = text.replace('\r\n', '\n').replace('\r', '\n').expandtabs()
        if not text.endswith('\n'): text += '\n'
        header, tree = TemplateParser(text, filename).go()
        self.tree = tree
        if header:
            self.h_defwith(header)
        else:
            self.args, self.kwargs = (), {}

    def __call__(self, *a, **kw):
        d = self.globals.copy()
        d.update(self._parseargs(a, kw))
        f = Fill(self.tree, d=d)
        if self.filter: f.filter = self.filter

        import webapi as web
        if 'headers' in web.ctx and self.filename:
            content_type = self.find_content_type()
            if content_type:
                web.header('Content-Type', content_type, unique=True)

        return f.go()

    def find_content_type(self):
        for ext, content_type in self.content_types.iteritems():
            if self.filename.endswith(ext):
                return content_type

    def _parseargs(self, inargs, inkwargs):
        # difference from Python:
        #   no error on setting a keyword arg twice
        d = {}
        for arg in self.args:
            d[arg] = Required
        for kw, val in self.kwargs:
            d[kw] = val

        for n, val in enumerate(inargs):
            if n < len(self.args):
                d[self.args[n]] = val
            elif n < len(self.args)+len(self.kwargs):
                kw = self.kwargs[n - len(self.args)][0]
                d[kw] = val

        for kw, val in inkwargs.iteritems():
            d[kw] = val

        unset = []
        for k, v in d.iteritems():
            if v is Required:
                unset.append(k)
        if unset:
            raise TypeError, 'values for %s are required' % unset

        return d

    def h_defwith(self, header):
        assert header[WHAT] == 'defwith'
        f = Fill(self.tree, d={})

        self.args = header[ARGS]
        self.kwargs = []
        for var, valexpr in header[KWARGS]:
            self.kwargs.append((var, f.h(valexpr)))

    def __repr__(self):
        return "<Template: %s>" % self.filename

class Handle:
    def __init__(self, parsetree, **kw):
        self._funccache = {}
        self.parsetree = parsetree
        for (k, v) in kw.iteritems(): setattr(self, k, v)

    def h(self, item):
        return getattr(self, 'h_' + item[WHAT])(item)

class Fill(Handle):
    builtins = global_globals
    def filter(self, text):
        if text is None: return ''
        else: return utf8(text)
        # often replaced with stuff like net.websafe

    def h_literal(self, i):
        item = i[THING]
        if isinstance(item, (unicode, str)) and item[0] in ['"', "'"]:
            item = item[1:-1]
        elif isinstance(item, (float, int)):
            pass
        return item

    def h_list(self, i):
        x = i[THING]
        out = []
        for item in x:
            out.append(self.h(item))
        return out

    def h_dict(self, i):
        x = i[THING]
        out = {}
        for k, v in x.iteritems():
            out[self.h(k)] = self.h(v)
        return out

    def h_paren(self, i):
        item = i[THING]
        if isinstance(item, list):
            raise NotImplementedError, 'tuples'
        return self.h(item)

    def h_getattr(self, i):
        thing, attr = i[THING], i[ATTR]
        thing = self.h(thing)
        if attr.startswith('_') or attr.startswith('func_') or attr.startswith('im_'):
            raise SecurityError, 'tried to get ' + attr
        try:
            if thing in self.builtins:
                raise SecurityError, 'tried to getattr on ' + repr(thing)
        except TypeError:
            pass # raised when testing an unhashable object
        try:
            return getattr(thing, attr)
        except AttributeError:
            if isinstance(thing, list) and attr == 'join':
                return lambda s: s.join(thing)
            else:
                raise

    def h_call(self, i):
        call = self.h(i[THING])
        args = [self.h(x) for x in i[ARGS]]
        kw = dict([(x, self.h(y)) for (x, y) in i[KWARGS]])
        return call(*args, **kw)

    def h_getitem(self, i):
        thing, item = i[THING], i[ITEM]
        thing = self.h(thing)
        item = self.h(item)
        return thing[item]

    def h_expr(self, i):
        item = self.h(i[THING])
        if i[NEGATE]:
            item = not item
        return item

    def h_test(self, item):
        ox, op, oy = item[X], item[OP], item[Y]
        # for short-circuiting to work, we can't eval these here
        e = self.h
        if op == 'is':
            return e(ox) is e(oy)
        elif op == 'is not':
            return e(ox) is not e(oy)
        elif op == 'in':
            return e(ox) in e(oy)
        elif op == 'not in':
            return e(ox) not in e(oy)
        elif op == '==':
            return e(ox) == e(oy)
        elif op == '!=':
            return e(ox) != e(oy)
        elif op == '>':
            return e(ox) > e(oy)
        elif op == '<':
            return e(ox) < e(oy)
        elif op == '<=':
            return e(ox) <= e(oy)
        elif op == '>=':
            return e(ox) >= e(oy)
        elif op == 'and':
            return e(ox) and e(oy)
        elif op == 'or':
            return e(ox) or e(oy)
        elif op == '+':
            return e(ox) + e(oy)
        elif op == '-':
            return e(ox) - e(oy)
        elif op == '*':
            return e(ox) * e(oy)
        elif op == '/':
            return e(ox) / e(oy)
        elif op == '%':
            return e(ox) % e(oy)
        else:
            raise WTF, 'op ' + op

    def h_var(self, i):
        v = i[NAME]
        if v in self.d:
            return self.d[v]
        elif v in self.builtins:
            return self.builtins[v]
        elif v == 'self':
            return self.output
        else:
            raise NameError, 'could not find %s (line %s)' % (repr(i[NAME]), i[LINENO])

    def h_line(self, i):
        out = []
        for x in i[THING]:
            #@@ what if x is unicode
            if isinstance(x, str):
                out.append(x)
            elif x[WHAT] == 'itpl':
                o = self.h(x[NAME])
                if x[FILTER]:
                    o = self.filter(o)
                else:
                    o = (o is not None and utf8(o)) or ""
                out.append(o)
            else:
                raise WTF, x
        return ''.join(out)

    def h_varset(self, i):
        self.output[i[NAME]] = ''.join(self.h_lines(i[BODY]))
        return ''

    def h_if(self, i):
        expr = self.h(i[CLAUSE])
        if expr:
            do = i[BODY]
        else:
            for e in i[ELIF]:
                expr = self.h(e[CLAUSE])
                if expr:
                    do = e[BODY]
                    break
            else:
                do = i[ELSE]
        return ''.join(self.h_lines(do))

    def h_for(self, i):
        out = []
        assert i[IN][WHAT] == 'expr'
        invar = self.h(i[IN])
        forvar = i[NAME]
        if invar:
            for nv in invar:
                if len(forvar) == 1:
                    fv = forvar[0]
                    assert fv[WHAT] == 'var'
                    self.d[fv[NAME]] = nv # same (lack of) scoping as Python
                else:
                    for x, y in zip(forvar, nv):
                        assert x[WHAT] == 'var'
                        self.d[x[NAME]] = y

                out.extend(self.h_lines(i[BODY]))
        else:
            if i[ELSE]:
                out.extend(self.h_lines(i[ELSE]))
        return ''.join(out)

    def h_while(self, i):
        out = []
        expr = self.h(i[CLAUSE])
        if not expr:
            return ''.join(self.h_lines(i[ELSE]))
        c = 0
        while expr:
            c += 1
            if c >= MAX_ITERS:
                raise RuntimeError, 'too many while-loop iterations (line %s)' % i[LINENO]
            out.extend(self.h_lines(i[BODY]))
            expr = self.h(i[CLAUSE])
        return ''.join(out)

    def h_assign(self, i):
        self.d[i[NAME]] = self.h(i[EXPR])
        return ''

    def h_comment(self, i): pass

    def h_lines(self, lines):
        if lines is None: return []
        return map(self.h, lines)

    def go(self):
        self.output = Stowage()
        self.output._str = ''.join(map(self.h, self.parsetree))
        if self.output.keys() == ['_str']:
            self.output = self.output['_str']
        return self.output

class render:
    def __init__(self, loc='templates/', cache=True):
        self.loc = loc
        if cache:
            self.cache = {}
        else:
            self.cache = False

    def _do(self, name, filter=None):
        if self.cache is False or name not in self.cache:

            tmplpath = os.path.join(self.loc, name)
            p = [f for f in glob.glob(tmplpath + '.*') if not f.endswith('~')] # skip backup files
            if not p and os.path.isdir(tmplpath):
                return render(tmplpath, cache=self.cache)
            elif not p:
                raise AttributeError, 'no template named ' + name

            p = p[0]
            c = Template(open(p).read(), filename=p)
            if self.cache is not False: self.cache[name] = (p, c)

        if self.cache is not False: p, c = self.cache[name]

        if p.endswith('.html') or p.endswith('.xml'):
            if not filter: c.filter = websafe
        return c

    def __getattr__(self, p):
        return self._do(p)

def frender(fn, *a, **kw):
    return Template(open(fn).read(), *a, **kw)

def test():
    import sys
    verbose = '-v' in sys.argv
    def assertEqual(a, b):
        if a == b:
            if verbose:
                sys.stderr.write('.')
                sys.stderr.flush()
        else:
            assert a == b, "\nexpected: %s\ngot: %s" % (repr(b), repr(a))

    from utils import storage, group

    class t:
        def __init__(self, text):
            self.text = text

        def __call__(self, *a, **kw):
            return TestResult(self.text, Template(self.text)(*a, **kw))

    class TestResult:
        def __init__(self, source, value):
            self.source = source
            self.value = value

        def __eq__(self, other):
            if self.value == other:
                if verbose:
                    sys.stderr.write('.')
            else:
                print >> sys.stderr, 'FAIL:', repr(self.source), 'expected', repr(other), ', got', repr(self.value)
                sys.stderr.flush()

    t('1')() == '1\n'
    t('$def with ()\n1')() == '1\n'
    t('$def with (a)\n$a')(1) == '1\n'
    t('$def with (a=0)\n$a')(1) == '1\n'
    t('$def with (a=0)\n$a')(a=1) == '1\n'
    t('$if 1: 1')() == '1\n'
    t('$if 1:\n    1')() == '1\n'
    t('$if 0: 0\n$elif 1: 1')() == '1\n'
    t('$if 0: 0\n$elif None: 0\n$else: 1')() == '1\n'
    t('$if (0 < 1) and (1 < 2): 1')() == '1\n'
    t('$for x in [1, 2, 3]: $x')() == '1\n2\n3\n'
    t('$for x in []: 0\n$else: 1')() == '1\n'
    t('$def with (a)\n$while a and a.pop(): 1')([1, 2, 3]) == '1\n1\n1\n'
    t('$while 0: 0\n$else: 1')() == '1\n'
    t('$ a = 1\n$a')() == '1\n'
    t('$# 0')() == ''
    t('$def with (d)\n$for k, v in d.iteritems(): $k')({1: 1}) == '1\n'
    t('$def with (a)\n$(a)')(1) == '1\n'
    t('$def with (a)\n$a')(1) == '1\n'
    t('$def with (a)\n$a.b')(storage(b=1)) == '1\n'
    t('$def with (a)\n$a[0]')([1]) == '1\n'
    t('${0 or 1}')() == '1\n'
    t('$ a = [1]\n$a[0]')() == '1\n'
    t('$ a = {1: 1}\n$a.keys()[0]')() == '1\n'
    t('$ a = []\n$if not a: 1')() == '1\n'
    t('$ a = {}\n$if not a: 1')() == '1\n'
    t('$ a = -1\n$a')() == '-1\n'
    t('$ a = "1"\n$a')() == '1\n'
    t('$if 1 is 1: 1')() == '1\n'
    t('$if not 0: 1')() == '1\n'
    t('$if 1:\n    $if 1: 1')() == '1\n'
    t('$ a = 1\n$a')() == '1\n'
    t('$ a = 1.\n$a')() == '1.0\n'
    t('$({1: 1}.keys()[0])')() == '1\n'
    t('$for x in [1, 2, 3]:\n\t$x')() == '    1\n    2\n    3\n'
    t('$def with (a)\n$:a')(1) == '1\n'
    t('$def with (a)\n$a')(u'\u203d') == u'\xe2\x80\xbd\n'
    t(u'$def with (f)\n$:f("x")')(lambda x: x) == 'x\n'

    j = Template("$var foo: bar")()
    assertEqual(str(j), '')
    assertEqual(j.foo, 'bar\n')
    if verbose: sys.stderr.write('\n')


if __name__ == "__main__":
    test()
