#
# Copyright 2001 by Object Craft P/L, Melbourne, Australia.
#
# LICENCE - see LICENCE file distributed with this software for details.
#
# Standard albatross tags
#
import time

from albatross.template import Content, EmptyTag, EnclosingTag
from albatross.common import *

# Empty content object which tags can use as default value for
# optional child tag content.  <al-head>, <al-tail>, etc.
not_specified = Content()


# Escape text for attribute values
def escape(text):
    text = str(text)
    text = text.replace('&', '&amp;')
    text = text.replace('<', '&lt;')
    text = text.replace('>', '&gt;',)
    text = text.replace('"', '&quot;')
    text = text.replace("'", '&#39;')
    return text


# Implement the expr, nameattr, etc attribute behaviour
class EvalAttribMixin:

    def assert_has_attribs(self, *attrs):
        """ Check that the tag has at least one of the attributes """
        for attr in attrs:
            if self.has_attrib(attr):
                return
        if len(attrs) == 1:
            self.raise_error('missing %s attribute' % repr(attrs[0]))
        else:
            attr_list = ', '.join([repr(attr) for attr in attrs[:-1]]) 
            self.raise_error('missing %s or %s attribute' %\
                             (attr_list, repr(attrs[-1])))

    def eval_attrib(self, ctx, attr, kind = 'eval'):
        code_attr = 'co_' + attr
        try:
            code_obj = getattr(self, code_attr)
        except AttributeError:
            code_obj = compile(self.get_attrib(attr), '<albatross>', kind)
            setattr(self, code_attr, code_obj)
        return ctx.eval_expr(code_obj)
            
    def get_name(self, ctx):
        alias = self.get_attrib('alias')
        if alias:
            name = ctx.make_alias(alias)
        elif self.has_attrib('nameexpr'):
            name = self.eval_attrib(ctx, 'nameexpr')
        else:
            name = self.get_attrib('name')
        if not name:
            self.raise_error('"name" attribute is null')
        return name

    def get_name_and_value(self, ctx):
        name = self.get_name(ctx)
        if self.has_attrib('expr'):
            return name, self.eval_attrib(ctx, 'expr')
        value = self.get_attrib('value')
        if value is not None:
            return name, value
        else:
            return name, ctx.get_value(name)

# ------------------------------------------------------------------

# The HTML <INPUT> tag is wrapped so that variable values can be
# automatically pulled out of the execution context and rendered as
# the VALUE attribute.  When using Albatross application objects, the
# corresponding browser request is automatically merged back into the
# execution context.
class Input(EmptyTag, EvalAttribMixin):

    name = 'al-input'

    def __init__(self, ctx, filename, line_num, attribs):
        EmptyTag.__init__(self, ctx, filename, line_num, attribs)
        self.__itype = self.get_attrib('type', 'text').lower()
        try:
            self.unbound_to_html = self.type_dict[self.__itype]
        except KeyError:
            self.raise_error('unrecognised <INPUT TYPE="%s">' % self.__itype)
        self.assert_has_attribs('name', 'nameexpr', 'alias',
                                'prevpage', 'nextpage', 
                                'treefold', 'treeselect', 'treeellipsis')

    def write_attribs_except(self, ctx, *attribs):
        EmptyTag.write_attribs_except(self, ctx,
               'name', 'nameexpr', 'alias',
               'expr', 'valueexpr',
               'list', 'noescape',
               'nextpage', 'prevpage',
               'treefold', 'treeselect', 'treeellipsis', 'node',  *attribs)

    def get_name(self, ctx):
        for name in ('prevpage', 'nextpage'):
            if self.has_attrib(name):
                iter_name = self.get_attrib(name)
                return '%s,%s' % (name, iter_name)
        for name in ('treefold', 'treeselect', 'treeellipsis'):
            if self.has_attrib(name):
                iter_name = self.get_attrib(name)
                if self.has_attrib('node'):
                    node = self.eval_attrib(ctx, 'node')
                else:
                    iter = ctx.get_value(iter_name)
                    node = iter.value()
                return '%s,%s,%s' % (name, iter_name, node.albatross_alias())
        return EvalAttribMixin.get_name(self, ctx)

    def get_name_value_checked(self, ctx, default = None):
        # Name
        name = self.get_name(ctx)
        # Value
        if self.has_attrib('expr'):
            value = self.eval_attrib(ctx, 'expr')
        else:
            value = ctx.get_value(name)
        # Checked value
        if self.has_attrib('valueexpr'):
            return name, value, self.eval_attrib(ctx, 'valueexpr')
        elif self.has_attrib('value'):
            return name, value, self.get_attrib('value')
        else:
            return name, value, default


    def to_html(self, ctx):
        ctx.write_content('<input')
        self.unbound_to_html(self, ctx)
        ctx.write_content(' />')

    def generic_to_html(self, ctx):
        self.write_attribs_except(ctx, 'value')
        name, value = self.get_name_and_value(ctx)
        if name:
            ctx.write_content(' name="%s"' % name)
        if value is not None:
            if self.has_attrib('noescape'):
                ctx.write_content(' value="%s"' % value)
            else:
                ctx.write_content(' value="%s"' % escape(value))
        if not self.has_attrib('disabled'):
            ctx.input_add(self.__itype, name, value, self.has_attrib('list'))

    def radio_to_html(self, ctx):
        self.write_attribs_except(ctx, 'value', 'checked')
        name, value, value_attr = self.get_name_value_checked(ctx)
        ctx.write_content(' name="%s"' % name)
        ctx.write_content(' value="%s"' % value_attr)
        if str(value) == str(value_attr):
            ctx.write_content(' checked')
        if not self.has_attrib('disabled'):
            ctx.input_add('radio', name, value_attr, self.has_attrib('list'))

    def checkbox_to_html(self, ctx):
        self.write_attribs_except(ctx, 'value', 'checked')
        name, value, value_attr = self.get_name_value_checked(ctx, 'on')
        if value and self.has_attrib('treeselect'):
            value = 'on'
        ctx.write_content(' name="%s"' % name)
        ctx.write_content(' value="%s"' % value_attr)
        if type(value) in (type([]), type(())):
            if str(value_attr) in map(str, value):
                ctx.write_content(' checked')
        elif str(value) and str(value) == str(value_attr):
            ctx.write_content(' checked')
        if not self.has_attrib('disabled'):
            ctx.input_add('checkbox', name, value_attr, self.has_attrib('list'))

    def generic_novalue_to_html(self, ctx):
        # value= not applicable
        self.write_attribs_except(ctx, 'value')
        name = self.get_name(ctx)
        ctx.write_content(' name="%s"' % name)
        if not self.has_attrib('disabled'):
            ctx.input_add(self.__itype, name, None, self.has_attrib('list'))

    type_dict = {
        'text': generic_to_html, 'password': generic_to_html,
        'radio': radio_to_html, 'checkbox': checkbox_to_html,
        'submit': generic_to_html, 'reset': generic_to_html,
        'image': generic_novalue_to_html, 'file': generic_novalue_to_html,
        'hidden': generic_to_html, 'button': generic_to_html, }


# Allows the application need to modify the HREF attribute of the <A>
# tag to include extra information from the execution context.
class Href(EnclosingTag, EvalAttribMixin):

    name = 'al-a'

    def __init__(self, ctx, filename, line_num, attribs):
        EnclosingTag.__init__(self, ctx, filename, line_num, attribs)

    def to_html(self, ctx):
        ctx.write_content('<a')
        self.write_attribs_except(ctx, 'expr', 'href',
                                  'nextpage', 'prevpage',
                                  'treefold', 'treeselect', 'treeellipsis',
                                  'node')
        href = None
        for name in ('prevpage', 'nextpage'):
            if self.has_attrib(name):
                href = '%s?%s,%s=1' % (ctx.current_url(),
                                       name, self.get_attrib(name))
                break
        for name in ('treefold', 'treeselect', 'treeellipsis'):
            if self.has_attrib(name):
                iter_name = self.get_attrib(name)
                iter = ctx.get_value(iter_name)
                if self.has_attrib('node'):
                    node = self.eval_attrib(ctx, 'node')
                else:
                    node = iter.value()
                href = '%s?%s,%s,%s=1' % (ctx.current_url(),
                                          name, iter_name,
                                          node.albatross_alias())
                break
        if not href:
            if self.has_attrib('expr'):
                href = self.eval_attrib(ctx, 'expr')
            else:
                href = self.get_attrib('href')
            pos = href.find('?')
            if pos < 0:
                if href.find('=') >= 0:
                    # make into request on current page
                    href = '%s?%s' % (ctx.current_url(), href)
                else:
                    # make in request from full base_url path
                    href = ctx.redirect_url(href)
        ctx.write_content(' href="%s"' % href)
        ctx.write_content('>')
        EnclosingTag.to_html(self, ctx)
        ctx.write_content('</a>')


# Allows the application need to modify the SRC attribute of the <IMG>
# tag to include extra information from the execution context.
class Img(EmptyTag, EvalAttribMixin):

    name = 'al-img'

    def __init__(self, ctx, filename, line_num, attribs):
        EmptyTag.__init__(self, ctx, filename, line_num, attribs)
        self.assert_has_attribs('expr')

    def to_html(self, ctx):
        ctx.write_content('<img')
        self.write_attribs_except(ctx, 'expr', 'src', 'noescape')
        src = self.eval_attrib(ctx, 'expr')
        if self.has_attrib('noescape'):
            ctx.write_content(' src="%s"' % src)
        else:
            ctx.write_content(' src="%s"' % escape(src))
        ctx.write_content(' />')


# Using the albatross <FORM> tag allows Albatross to record the
# contents of each form.  This is used to register valid browser
# requests with the toolkit.
#
# We use a content_trap to capture the enclosed content so we know before
# emiting the <form> element whether or not to use a multipart/form-data
# encoding (required to make <input type="file"> work correctly).
class Form(EnclosingTag):

    name = 'al-form'

    def to_html(self, ctx):
        ctx.push_content_trap()
        ctx.form_open()
        EnclosingTag.to_html(self, ctx)
        use_multipart_enc = ctx.form_close()
        content = ctx.pop_content_trap()
        ctx.write_content('<form')
        self.write_attribs_except(ctx)
        if not self.has_attrib('action'):
            ctx.write_content(' action="%s"' % ctx.current_url())
        if use_multipart_enc and not self.has_attrib('enctype'):
            ctx.write_content(' enctype="multipart/form-data"')
        ctx.write_content('>')
        ctx.write_content(content)
        ctx.write_content('</form>')


# Applications which need to know the list of valid <al-select>
# options will need to use <al-option> tags in template files.  The
# alternative is to define the option list in Python code.
# 
# Two forms implemented:
# <al-option>12</al-option>
# <al-option value="12">Arbitrary text</al-option>
# 
# In the first form, we need to evaluate the content of the tag before
# we output the start tag so we can determine whether or not to set
# the selected attribute on the option, hence the use of the content_trap.
class Option(EnclosingTag):

    name = 'al-option'

    def __init__(self, ctx, filename, line_num, attribs):
        EnclosingTag.__init__(self, ctx, filename, line_num, attribs)

    def to_html(self, ctx, select_value):
        if self.has_attrib('value'):
            value = self.get_attrib('value')
        else:
            ctx.push_content_trap()
            EnclosingTag.to_html(self, ctx)
            value = ctx.pop_content_trap()
        ctx.write_content('<option')
        self.write_attribs_except(ctx)
        if type(select_value) is type(''):
            if value == select_value:
                ctx.write_content(' selected')
        elif type(select_value) in (type([]), type(())):
            if value in select_value:
                ctx.write_content(' selected')
        ctx.write_content('>')
        if self.has_attrib('value'):
            EnclosingTag.to_html(self, ctx)
        else:
            ctx.write_content(value)
        ctx.write_content('</option>')


# Some <al-select> tags have their option list supplied by an
# application value.  There are two forms:
# <al-select name="var">
#  <al-option>12</al-option>
# </al-select>
# <al-select name="var" optionexpr="range(12)"/>
class Select(EnclosingTag, EvalAttribMixin):

    name = 'al-select'

    def __init__(self, ctx, filename, line_num, attribs):
        EnclosingTag.__init__(self, ctx, filename, line_num, attribs)
        self.assert_has_attribs('name', 'alias', 'nameexpr')
        self.options = []

    def to_html(self, ctx):
        name, value = self.get_name_and_value(ctx)
        ctx.write_content('<select')
        self.write_attribs_except(ctx, 'optionexpr', 'alias', 'name',
                                  'nameexpr', 'noescape')
        ctx.write_content(' name="%s">' % name)
        if type(value) in (type([]), type(())):
            value = map(str, value)
        else:
            value = str(value)
        if self.has_attrib('optionexpr'):
            seq = self.eval_attrib(ctx, 'optionexpr')
            for item in seq:
                self.option_to_html(ctx, item, value)
        else:
            for item in self.options:
                item.to_html(ctx, value)
        ctx.write_content('</select>')
        if not self.has_attrib('disabled'):
            ctx.input_add('select', name, None, 
                          self.has_attrib('list') or self.has_attrib('multiple'))

    def option_to_html(self, ctx, item, select_value):
        if type(item) is type(()):
            value, text = map(str, item)
        else:
            value = str(item)
            text = value
        ctx.write_content('<option')
        if type(select_value) is type(''):
            if value == select_value:
                ctx.write_content(' selected')
        elif type(select_value) in (type([]), type(())):
            if value in select_value:
                ctx.write_content(' selected')
        if type(item) is type(()):
            ctx.write_content(' value="%s"' % escape(value))
        ctx.write_content('>')
        if self.has_attrib('noescape'):
            ctx.write_content(text)
        else:
            ctx.write_content(escape(text))
        ctx.write_content('</option>')
        ctx.write_content('\n')

    def append(self, tag):
        if isinstance(tag, Option):
            self.options.append(tag)
        else:
            EnclosingTag.append(self, tag)


class TextArea(EnclosingTag, EvalAttribMixin):

    name = 'al-textarea'

    def __init__(self, ctx, filename, line_num, attribs):
        EnclosingTag.__init__(self, ctx, filename, line_num, attribs)
        self.assert_has_attribs('name', 'alias', 'nameexpr')

    def to_html(self, ctx):
        name = self.get_name(ctx)
        ctx.write_content('<textarea')
        self.write_attribs_except(ctx, 'alias', 'name', 'nameexpr', 'list',
                                  'noescape')
        ctx.write_content(' name="%s">' % name)
        if ctx.has_value(name):
            value = ctx.get_value(name)
            if not value:
                ctx.write_content('')
            elif self.has_attrib('noescape'):
                ctx.write_content(value)
            else:
                ctx.write_content(escape(value))
        else:
            EnclosingTag.to_html(self, ctx)
        ctx.write_content('</textarea>')
        if not self.has_attrib('disabled'):
            ctx.input_add('textarea', name, None, self.has_attrib('list'))


# ------------------------------------------------------------------

# Send HTML accumulated in execution context to output.
class Flush(EmptyTag):

    name = 'al-flush'

    def to_html(self, ctx):
        ctx.flush_content()


# Do not execute enclosed content
class Comment(EnclosingTag):

    name = 'al-comment'

    def to_html(self, ctx):
        pass


# Include a template file. Can name the file directly, or can use an
# expression to yield a filename.
class Include(EmptyTag, EvalAttribMixin):

    name = 'al-include'

    def __init__(self, ctx, filename, line_num, attribs):
        EmptyTag.__init__(self, ctx, filename, line_num, attribs)
        self.assert_has_attribs('name', 'expr')

    def to_html(self, ctx):
        if self.has_attrib('expr'):
            name = self.eval_attrib(ctx, 'expr')
        else:
            name = self.get_attrib('name')
        templ = ctx.load_template(name)
        templ.to_html(ctx)


# Conditional content - emulates Python
#     if expr:
#     elif expr:
#     else:
#
# <al-if expr="value > 12">
#  Value > 12
# <al-elif expr="value > 5">
#  Value > 5 and <= 12
# <al-else>
#  Value <= 5
# </al-if>
class Elif(EmptyTag, EvalAttribMixin):

    name = 'al-elif'

    def __init__(self, ctx, filename, line_num, attribs):
        EmptyTag.__init__(self, ctx, filename, line_num, attribs)
        self.assert_has_attribs('expr')

    def is_true(self, ctx):
        return self.eval_attrib(ctx, 'expr')


class Else(EmptyTag):

    name = 'al-else'


class If(EnclosingTag, EvalAttribMixin):

    name = 'al-if'

    def __init__(self, ctx, filename, line_num, attribs):
        EnclosingTag.__init__(self, ctx, filename, line_num, attribs)
        self.assert_has_attribs('expr')
        self.elif_list = []
        self.else_content = not_specified
        self.current_content = self.content

    def to_html(self, ctx):
        if self.eval_attrib(ctx, 'expr'):
            EnclosingTag.to_html(self, ctx)
            return
        for tag, content in self.elif_list:
            if tag.is_true(ctx):
                content.to_html(ctx)
                return
        self.else_content.to_html(ctx)

    def append(self, tag):
        if isinstance(tag, Elif):
            self.current_content = Content()
            self.elif_list.append((tag, self.current_content))
        elif isinstance(tag, Else):
            self.current_content = Content()
            self.else_content = self.current_content
        else:
            self.current_content.append(tag)


# Execute some Python code
#
# <al-exec expr="keys = os.environ.keys(); keys.sort()">
class Exec(EmptyTag, EvalAttribMixin):

    name = 'al-exec'

    def __init__(self, ctx, filename, line_num, attribs):
        EmptyTag.__init__(self, ctx, filename, line_num, attribs)
        self.assert_has_attribs('expr')

    def to_html(self, ctx):
        self.eval_attrib(ctx, 'expr', 'exec')


# Iterated content - emulates Python
#     for iter in sequence:
class ListIterator:

    def __init__(self):
        self._index = self._start = self._count = 0
        self._seq = self._have_value = None
        self._pagesize = 0

    def __getstate__(self):
        return self._start, self._pagesize

    def __setstate__(self, tup):
        self._start, self._pagesize = tup
        self._index = self._start
        self._have_value = self._seq = None

    def __len__(self):
        if self._pagesize:
            return self._pagesize
        elif self._seq is not None:
            return len(self._seq)
        else:
            return 0

    # ----------------------------------------------------
    # Pagination functionality.

    def pagesize(self):
        return self._pagesize

    def set_pagesize(self, size):
        self._pagesize = size

    def has_prevpage(self):
        return self._start > 0

    def has_nextpage(self):
        if not self._pagesize:
            raise ApplicationError('iterator not in page mode')
        return len(self._seq) > self._start + self._pagesize

    def get_backdoor(self, op):
        return 1

    def set_backdoor(self, op, value):
        if value:
            if op == 'nextpage':
                self._start = self._start + self._pagesize
                self._index = self._start
            elif op == 'prevpage':
                self._start = self._start - self._pagesize
                if self._start < 0:
                    self._start = 0
                self._index = self._start

    # ----------------------------------------------------
    # Sequence control.

    def has_sequence(self):
        return self._seq is not None

    def set_sequence(self, seq):
        self._seq = seq

    # ----------------------------------------------------
    # Iteration.  Make sure that each element is only retrieved from
    # the sequence once.  This allows programs to make use of objects
    # which are not really sequences; popen().readline() for instance.

    def reset_index(self):
        self._index = self._start

    def reset_count(self):
        self._count = 0

    def index(self):
        return self._index

    def count(self):
        return self._count

    def start(self):
        return self._start

    def value(self):
        return self._value

    def clear_value(self):
        self._have_value = None

    def has_value(self):
        if self._have_value is None:
            try:
                self._value = self._seq[self._index]
                self._have_value = 1
            except IndexError:
                self._have_value = 0
        return self._have_value

    def set_value(self, value):
        self._value = value
        self._have_value = 1

    def next(self):
        self._index = self._index + 1
        self._count = self._count + 1
        self._have_value = None
        self.has_value()


# EXPR:seq
#   Yields an object which implements sequence protocol.  The result
#   is assigned to the ITER attribute.  If not present, the ITER
#   attribute must already have a sequence in it.
# PAGESIZE:int
#   Turns on pagination.  If this attribute is used anywhere with the
#   named ITER then that page size becomes the default for all other
#   uses of the ITER.  Places the ITER into the session.
# CONTINUE:bool
#   Instructs the tag not to reset the index() to the start() - it is
#   continued on from the previous ITER use.
# PREPARE:bool
#   Tells the tag to attach the sequence to the ITER and perform
#   pagination calculations.  This makes the ITER available for tests
#   before the content is rendered.
# COLS:int
#   Instructs the tag to render the content in multiple columns.
#   Items are arranged to flow down columns by default.
# FLOW:across/down
#   Define the order in which multicolumn output is arranged.
class For(EnclosingTag, EvalAttribMixin):

    name = 'al-for'

    def __init__(self, ctx, filename, line_num, attribs):
        EnclosingTag.__init__(self, ctx, filename, line_num, attribs)
        self.assert_has_attribs('iter')
        if self.has_attrib('flow'):
            if self.get_attrib('flow').lower() not in ('across', 'down'):
                self.raise_error('"flow" must be "across" or "down"')

    def to_html(self, ctx):
        # If PAGESIZE or CONTINUE defined then the iterator will not
        # be recreated if it already exists.
        name = self.get_attrib('iter')
        iter = None
        if self.has_attrib('pagesize') or self.has_attrib('continue'):
            iter = ctx.get_value(name)
        if iter is None:
            iter = ListIterator()
            ctx.set_value(name, iter)
        # If iterator does not have a sequence yet, the EXPR attribute
        # must be present
        if self.has_attrib('expr'):
            seq = self.eval_attrib(ctx, 'expr')
            iter.set_sequence(seq)
        elif not iter.has_sequence():
            self.raise_error('no sequence present; expected "expr" attribute')
        # If PAGESIZE is defined then place the iterator into the
        # session.
        if self.has_attrib('pagesize'):
            ctx.add_session_vars(name)
            pagesize = int(self.get_attrib('pagesize'))
            iter.set_pagesize(pagesize)
        # If CONTINUE if defined then the contents will flow on from
        # the previous use of the iterator
        if not self.has_attrib('continue'):
            iter.reset_index()
        iter.reset_count()
        iter.has_value()
        # If PREPARE defined then no rendering performed
        if self.has_attrib('prepare'):
            return
        # Render the sequence contents
        if self.has_attrib('cols'):
            if self.get_attrib('flow', 'down').lower() == 'down':
                self.cols_down_html(ctx, iter)
            else:
                self.cols_across_html(ctx, iter)
        else:
            pagesize = iter.pagesize()
            while iter.has_value():
                if pagesize and iter.count() >= pagesize:
                    break
                EnclosingTag.to_html(self, ctx)
                iter.next()
            iter.clear_value()

    def cols_down_html(self, ctx, iter):
        num_cols = int(self.get_attrib('cols'))
        num = len(iter)
        num_rows = num / num_cols
        num_extra = num % num_cols
        for row in range(num_rows + (num_extra > 0)):
            index = row
            seq = []
            for col in range(num_cols):
                if row == num_rows and col == num_extra:
                    break
                seq.append(iter._seq[index])
                index = index + num_rows + (col < num_extra)
                if index >= num:
                    break
            iter.set_value(seq)
            EnclosingTag.to_html(self, ctx)

    def cols_across_html(self, ctx, iter):
        num_cols = int(self.get_attrib('cols'))
        pagesize = iter.pagesize()
        num = len(iter)
        index = 0
        while index < num:
            seq = iter._seq[index:index + num_cols]
            iter.set_value(seq)
            EnclosingTag.to_html(self, ctx)
            index = index + num_cols


class TreeIterator:

    def __init__(self):
        self._stack = []

    def __setstate__(self, *tup):
        self._stack = []

    def tree_depth(self):
        return self._tree_depth

    def depth(self):
        return len(self._stack)

    def span(self):
        return self._tree_depth - len(self._stack)

    def line(self, depth):
        return self._line[depth]

    def set_line(self, line):
        self._line = line

    def value(self):
        return self._value

    def set_value(self, node):
        self._value = node

    def is_open(self):
        return hasattr(self._value, 'children')

    def node_use_ellipsis(self, ctx, node):
        return 0

    def use_ellipsis(self):
        return 0

    def node_type(self):
        return 0

    def node_is_open(self, ctx, node):
        return hasattr(node, 'children')

    def is_selected(self):
        return 0

    def has_children(self):
        return hasattr(self._value, 'children')

    def get_backdoor(self, op, key):
        return None

    def set_backdoor(self, op, key, value):
        pass


class LazyTreeIterator(TreeIterator):

    def __init__(self, single_select = 0):
        TreeIterator.__init__(self)
        self._single_select = single_select
        self._key = None
        self._type = 0
        self._selected_aliases = {}
        self._open_aliases = {}

    def __getstate__(self):
        return self._single_select, self._open_aliases, self._selected_aliases

    def __setstate__(self, tup):
        TreeIterator.__setstate__(self, tup)
        self._key = None
        self._single_select, self._open_aliases, self._selected_aliases = tup

    def set_value(self, node, type = 0):
        TreeIterator.set_value(self, node)
        if node:
            self._key = node.albatross_alias()
        self._type = type

    def is_open(self):
        if not hasattr(self._value, 'children'):
            return 0
        return self._open_aliases.has_key(self._key)

    def load_children(self, ctx, node):
        if not node.children_loaded:
            node.load_children(ctx)
            node.children_loaded = 1

    def node_is_open(self, ctx, node):
        if not hasattr(node, 'children'):
            return 0
        node_open = self._open_aliases.has_key(node.albatross_alias())
        if node_open:
            self.load_children(ctx, node)
        return node_open

    def is_selected(self):
        return self._selected_aliases.has_key(self._key)

    def node_type(self):
        return self._type

    def close_all(self):
        self._open_aliases = {}

    def deselect_all(self):
        self._selected_aliases = {}

    def get_selected_aliases(self):
        aliases = self._selected_aliases.keys()
        aliases.sort()
        return aliases

    def select_alias(self, alias, value = 1):
        if value:
            self._selected_aliases[alias] = 1
        elif self._selected_aliases.has_key(alias):
            del self._selected_aliases[alias]
        
    def set_selected_aliases(self, aliases):
        self._selected_aliases = {}
        for alias in aliases:
            self._selected_aliases[alias] = 1

    def get_open_aliases(self):
        aliases = self._open_aliases.keys()
        aliases.sort()
        return aliases

    def open_alias(self, alias, open = 1):
        if open:
            self._open_aliases[alias] = 1
        elif self._open_aliases.has_key(alias):
            del self._open_aliases[alias]

    def set_open_aliases(self, aliases):
        self._open_aliases = {}
        for alias in aliases:
            self._open_aliases[alias] = 1

    def get_backdoor(self, op, key):
        if op == 'treeselect':
            return self._selected_aliases.has_key(key)
        elif op == 'treefold':
            return 1
        else:
            return None

    def set_backdoor(self, op, key, value):
        if op == 'treeselect':
            if value:
                if self._single_select:
                    self._selected_aliases = { key: 1 }
                else:
                    self._selected_aliases[key] = 1
            elif self._selected_aliases.has_key(key):
                del self._selected_aliases[key]
        elif op == 'treefold':
            if value:
                if self._open_aliases.has_key(key):
                    del self._open_aliases[key]
                else:
                    self._open_aliases[key] = 1


class EllipsisTreeIterator(LazyTreeIterator):

    def __init__(self):
        LazyTreeIterator.__init__(self)
        self._noellipsis_alias = None

    def __setstate__(self, tup):
        LazyTreeIterator.__setstate__(self, tup)
        self._noellipsis_alias = None

    def node_use_ellipsis(self, ctx, node):
        if self._key == self._noellipsis_alias:
            return 0
        else:
            subnodes_open = 0
            for sub_node in self.value().children:
                if self.node_is_open(ctx, sub_node):
                    subnodes_open = 1
            return subnodes_open

    def get_backdoor(self, op, key):
        if op == 'treeellipsis':
            return 1
        else:
            return LazyTreeIterator.get_backdoor(self, op, key)

    def set_backdoor(self, op, key, value):
        if op == 'treeellipsis':
            if value:
                self._noellipsis_alias = key
        else:
            LazyTreeIterator.set_backdoor(self, op, key, value)


# Format tree structured data.
class Tree(EnclosingTag, EvalAttribMixin):

    name = 'al-tree'

    def __init__(self, ctx, filename, line_num, attribs):
        EnclosingTag.__init__(self, ctx, filename, line_num, attribs)
        self.assert_has_attribs('expr')
        self.assert_has_attribs('iter')

    def to_html(self, ctx):
        # Get iterator out of context - allows tree state to be
        # restored from session
        name = self.get_attrib('iter')
        iter = ctx.get_value(name)
        if iter is None:
            if self.has_attrib('ellipsis'):
                iter = EllipsisTreeIterator()
            elif self.has_attrib('lazy'):
                iter = LazyTreeIterator(self.has_attrib('single'))
            else:
                iter = TreeIterator()
            ctx.set_value(name, iter)
            ctx.add_session_vars(name)
        tree = self.eval_attrib(ctx, 'expr')
        iter._tree_depth = self.find_tree_depth(ctx, iter, tree)
        self.node_to_html(ctx, iter, tree)

    def find_tree_depth(self, ctx, iter, tree):
        deepest = 0
        iter.set_value(tree)
        if iter.node_is_open(ctx, tree):
            for node in tree.children:
                depth = self.find_tree_depth(ctx, iter, node)
                if depth > deepest:
                    deepest = depth
        return deepest + 1

    def node_to_html(self, ctx, iter, tree):
        parent = None
        line = []
        for node in iter._stack:
            if parent:
                if node is parent.children[-1]:
                    line.append(0)
                else:
                    line.append(1)
            parent = node
        if parent:
            if tree is parent.children[-1]:
                line.append(2)
            else:
                line.append(1)
        iter.set_line(line)
        iter.set_value(tree)
        EnclosingTag.to_html(self, ctx)
        iter._stack.append(tree)
        if iter.node_is_open(ctx, tree):
            if iter.node_use_ellipsis(ctx, tree):
                skipped = 0
                for sub_node in tree.children:
                    if iter.node_is_open(ctx, sub_node):
                        if skipped:
                            iter.set_value(tree, 1)
                            EnclosingTag.to_html(self, ctx)
                            skipped = 0
                        self.node_to_html(ctx, iter, sub_node)
                    else:
                        skipped = 1
                if skipped:
                    iter.set_value(tree, 1)
                    EnclosingTag.to_html(self, ctx)
            else:
                for node in tree.children:
                    self.node_to_html(ctx, iter, node)
        del iter._stack[-1]


# Include execution context values
#
# <al-value expr="name">
# <al-value expr="name" date="%d %b %Y">
# <al-value expr="name" lookup="name_lookup">
#
# When present the DATE attribute specifies a format string which is
# passed to time.strftime() to format the expression value.  The
# LOOKUP value uses the expression value as an index into the named
# lookup table and renders the value as the corresponding table entry.
class Value(EmptyTag, EvalAttribMixin):

    name = 'al-value'

    def __init__(self, ctx, filename, line_num, attribs):
        EmptyTag.__init__(self, ctx, filename, line_num, attribs)
        self.assert_has_attribs('expr')

    def to_html(self, ctx):
        value = self.eval_attrib(ctx, 'expr')
        format = self.get_attrib('date')
        if format:
            value = time.strftime(format, time.localtime(value))
            ctx.write_content(value)
            return
        lookup_name = self.get_attrib('lookup')
        if lookup_name:
            lookup = ctx.get_lookup(lookup_name)
            if not lookup:
                self.raise_error('undefined lookup "%s"' % lookup_name)
            lookup.lookup_html(ctx, value)
            return
        if self.has_attrib('noescape'):
            ctx.write_content(str(value))
        else:
            ctx.write_content(escape(value))


# Specify one entry in a lookup table
#
# <al-item expr="">
#  The enclosed content is evaluated when the table is
#  accessed, not when the table is contructed.  The EXPR
#  attribute is evaluated when the table is evaluated.
#  This allows the application to establish a context in
#  which the item EXPR attributes can be resolved to a
#  constant.
# </al-item>
class Item(EnclosingTag, EvalAttribMixin):

    name = 'al-item'

    def __init__(self, ctx, filename, line_num, attribs):
        EnclosingTag.__init__(self, ctx, filename, line_num, attribs)
        self.assert_has_attribs('expr')


# Specify a lookup table for translating a value to arbitrary content
# 
# <al-lookup name="name">
#  <al-item expr="1">Content</al-item>
#  <al-item expr="'hello'">Content</al-item>
#  <al-item expr="value + 3">Content</al-item>
# </al-lookup>
class Lookup(EnclosingTag):

    name = 'al-lookup'

    def __init__(self, ctx, filename, line_num, attribs):
        EnclosingTag.__init__(self, ctx, filename, line_num, attribs)
        self.assert_has_attrib('name')
        ctx.register_lookup(self.get_attrib('name'), self)
        self.item_list = []

    def append(self, tag):
        if isinstance(tag, Item):
            self.item_list.append(tag)
        else:
            EnclosingTag.append(self, tag)

    def _build_item_dict(self, ctx):
        if not hasattr(self, 'item_dict'):
            dict = {}
            for item in self.item_list:
                dict[item.eval_attrib(ctx, 'expr')] = item
            self.item_dict = dict

    def to_html(self, ctx):
        self._build_item_dict(ctx)

    def lookup_html(self, ctx, value):
        self._build_item_dict(ctx)
        item = self.item_dict.get(value, self.content)
        item.to_html(ctx)


# ------------------------------------------------------------------
# Macro expansions
# 
# Usearg and Macro do not produce output when evaluated - only when
# expanded via <al-expand>
class Usearg(EmptyTag):

    name = 'al-usearg'

    def to_html(self, ctx):
        content = ctx.get_macro_arg(self.get_attrib('name'))
        # Peel one level of arguments off stack to evaluate enclosed
        # content then restore the stack afterwards.  Nested macros
        # cause infinite recursion otherwise.
        args = ctx.pop_macro_args()
        content.to_html(ctx)
        ctx.push_macro_args(args)


class Macro(EnclosingTag):

    name = 'al-macro'

    def __init__(self, ctx, filename, line_num, attribs):
        EnclosingTag.__init__(self, ctx, filename, line_num, attribs)
        self.assert_has_attrib('name')
        ctx.register_macro(self.get_attrib('name'), self)

    def to_html(self, ctx):
        pass

    def expand_html(self, ctx):
        EnclosingTag.to_html(self, ctx)


class Setarg(EnclosingTag):

    name = 'al-setarg'


class Expand(EnclosingTag):

    name = 'al-expand'

    def __init__(self, ctx, filename, line_num, attribs):
        EnclosingTag.__init__(self, ctx, filename, line_num, attribs)
        self.arg_dict = {}

    def append(self, tag):
        if isinstance(tag, Setarg):
            self.arg_dict[tag.get_attrib('name')] = tag
        else:
            EnclosingTag.append(self, tag)

    def to_html(self, ctx):
        macro = ctx.get_macro(self.get_attrib('name'))
        if not macro:
            self.raise_error('undefined macro "%s"' % self.get_attrib('name'))
        args = {}
        args[None] = self.content
        for name, value in self.arg_dict.items():
            args[name] = value
        ctx.push_macro_args(args)
        macro.expand_html(ctx)
        ctx.pop_macro_args()


tags = (Input, Href, Img, Form, Option, Select, TextArea,
        Flush, Comment, Include, Elif, Else, If, Exec, For, Tree,
        Value, Item, Lookup, Usearg, Macro, Setarg, Expand)
