/*
How does this work?

There are two phases:

* Compilation phase: the template is parsed and structures are generated
  for a fast execution phase.

* Execution phase: the compiled template is combined with a JavaScript
  object and a DOM element. The resulting rendered template is attached
  to this element.

How does compilation work?

* a separated Section is compiled for each data-with, data-if and
  data-repeat element, or combinations thereof.

* Sections are organized in a tree; each section may have
  sub-sections. The template itself starts with a single Section at
  the top.

* each section also maintains a list of dynamic elements: an element
  with dynamic content. An element becomes dynamic if:

  * it has an attribute which uses variable interpolation.

  * it has a child text node that uses variable interpolation.

  * it has a data-id directive.

  * it has a data-trans directive.

  * it has a data-tvar directive.

* Sections and dynamic elements are found in a section by following a
  path of childNode indexes from the Section they are in. This way we
  avoid having to modify the DOM with temporary ID markers, which is
  relatively slow and does not allow the rendering of a template on an
  element unattached to the document.

when rendering a section:

* clone original section (deep copy) into DOM.

* now find all dynamic elements by id and fill in interpolations
  according to data (and translations).

* render sub-sections and attach them.

*/

if (typeof obviel === "undefined") {
    /*jshint shadow:false */
    var obviel = {};
}

obviel.template = {};

(function($, module) {

    // will define these later, is to please jshint
    var isHtmlText, trim;
    var formatters, funcs;
    var defaultViewName = null;
    var resolveInObj, getDirective, validateDottedName;
    var cachedTokenize, morphElement;
    var splitNameFormatter, splitNameFormatters;
    var parseTextForPlural, parseTvar, getImplicitCountVariable;
    var checkMessageId, getIndexInParent;
    
    module.NAME_TOKEN = 0;
    module.TEXT_TOKEN = 1;

    // these attributes may not be dynamic, you can only
    // set them dynamically thorugh data-<attrName>
    var SPECIAL_ATTR = {'id': true,  'src': true};

    // special classes that need to be retained even if we have
    // dynamic class attribute (class="{foo}") as otherwise
    // Obviel will break. obviel-template-removal is not in this
    // list because a dynamic class element won't break it anyway,
    // as it will never get rendered in the first place
    var SPECIAL_CLASSES = ['obviel-template-special-attr',
                           'obviel-template-data-el',
                           'obviel-template-data-unwrap'
                          ];
    
    module.CompilationError = function(el, message) {
        this.name = 'CompilationError';
        this.el = el;
        this.message = message;
    };

    module.CompilationError.prototype = new Error();
    module.CompilationError.prototype.constructor = module.CompilationError;
    
    module.CompilationError.prototype.toString = function() {
        return this.message;
    };
    
    module.RenderError = function(el, message) {
        this.name = 'RenderError';
        this.el = el;
        this.message = message;
    };

    module.RenderError.prototype = new Error();
    module.RenderError.prototype.constructor = module.RenderError;
    
    module.RenderError.prototype.toString = function() {
        return this.message;
    };
    
    module.Template = function(text) {
        var parsed;

        // allow us to deal with partial templates
        // (only text or an element plus top-level text,
        // or multiple top-level elements)
        // in the loop below the nodes are removed form the div again
        text = '<div>' + text + '</div>';
        parsed = $(text).get(0);
    
        var parts = [];
        while (parsed.hasChildNodes()) {
            var node = parsed.firstChild;
            parsed.removeChild(node);
            if (node.nodeType === 1) {
                // ELEMENT
                parts.push(new module.Section(node, true));
            } else if (node.nodeType === 3) {
                // TEXT
                parts.push(new module.DynamicText(node, node.nodeValue));
            }
        }
        this.parts = parts;
    };
    
    module.Template.prototype.render = function(el, obj, context) {
        var scope = new module.Scope(obj);
        if (!context) {
            context = { getTranslation: null, getHandler: null };
        }
        // use global getFormatter if nothing more specific was registered
        if (!context.getFormatter) {
            context.getFormatter = module.getFormatter;
        }
        // use global getFunc if nothing more specific was registered
        if (!context.getFunc) {
            context.getFunc = module.getFunc;
        }

        context.subviewPromises = [];
        
        var frag = document.createDocumentFragment();
        
        for (var i = 0; i < this.parts.length; i++) {
            this.parts[i].renderRoot(frag, scope, context);
        }
        
        // now insert frag into actual document
        el.empty();
        el.get(0).appendChild(frag);
        
        // wipe out any elements marked for removal by data-if; these
        // could not be removed previously so as not to break the
        // indexed based access to elements
        $('.obviel-template-removal', el).remove();

        // setting attributes with data (data-id, data-src) become
        // that attribute (id, src)
        $('*.obviel-template-special-attr', el).each(function() {
            var name, value;

            for (name in SPECIAL_ATTR) {
                value = getDirective(this, 'data-' + name);
                if (value !== null) {
                    this.setAttribute(name, value);
                }
            }
            
            var el = $(this);
            el.removeClass('obviel-template-special-attr');
            if (el.attr('class') === '') {
                el.removeAttr('class');
            }
        });

        // swap in those elements that had a dynamic element name
        $('.obviel-template-data-el', el).each(function() {
            var dataEl = getDirective(this, 'data-el');
            var newEl = $(morphElement(this, dataEl));
            newEl.removeClass('obviel-template-data-el');
            if (newEl.attr('class') === '') {
                newEl.removeAttr('class');
            }
        });
        // remove any unwrap tags
        $('.obviel-template-data-unwrap', el).each(function() {
            var frag = document.createDocumentFragment();
            while (this.hasChildNodes()) {
                frag.appendChild(this.removeChild(this.firstChild));
            }
            this.parentNode.insertBefore(frag, this.nextSibling);
            this.parentNode.removeChild(this);
        });

        // need to wait until data-render triggered views are done
        return $.when.apply(null, context.subviewPromises);
    };
    
    module.Section = function(el, rootSection) {
        this.el = el;

        var dataIf = getDirective(el, 'data-if');
        var dataWith = getDirective(el, 'data-with');
        var dataRepeat = getDirective(el, 'data-repeat');
        
        if (!rootSection && !dataIf && !dataWith && !dataRepeat) {
            this.dynamic = false;
            return;
        }

        this.dynamic = true;
        
        if (dataIf) {
            this.dataIf = new module.IfExpression(el, dataIf);
        } else {
            this.dataIf = null;
        }
        if (dataWith) {
            validateDottedName(el, dataWith);
            this.dataWithName = dataWith;
            this.dataWith = module.resolveFunc(dataWith);
        } else {
            this.dataWith = null;
        }
        if (dataRepeat) {
            validateDottedName(el, dataRepeat);
            this.dataRepeat = module.resolveFunc(dataRepeat);
            this.dataRepeatName = dataRepeat.replace('.', '_');
        } else {
            this.dataRepeat = null;
        }

        //this.elFuncs = { funcs: [], sub: {} };
        
        this.dynamicElements = [];
        this.renderElements = [];
        this.subSections = [];
        this.compile(el);
    };

    module.Section.prototype.isDynamic = function() {
        return this.dynamic;
    };
    
    module.Section.prototype.compile = function(el) {
        // always compile any dynamic elements on top element
        var hasMessageId = this.compileDynamicElement(el);
        
        // if we have a message id, we don't want to compile down
        // into the section any further
        // XXX do some checking for illegal constructs
        if (hasMessageId) {
            this.compileFragment(el);
            return;
        }

        // compile any render on top element
        this.compileRender(el);
        
        // now compile sub-elements
        for (var i = 0; i < el.childNodes.length; i++) {
            var node = el.childNodes[i];
            if (node.nodeType !== 1) {
                // skip all non-element nodes
                continue;
            }
            this.compileEl(node);
        }
        
        this.compileFragment(el);
    };

    module.Section.prototype.compileFragment = function(el) {
        var frag = document.createDocumentFragment();
        while (el.hasChildNodes()) {
            frag.appendChild(el.removeChild(el.firstChild));
        }
        this.frag = frag;
    };
    
    module.Section.prototype.compileEl = function(el) {
        // compile element as sub-section
        var isSubSection = this.compileSubSection(el);

        // if it's a sub-section, we're done with it
        // we don't want to compile dynamic elements for it,
        // as that's done in the sub-section. we also don't want
        // to process further child elements, as that's done in the
        // sub section
        if (isSubSection) {
            return;
        }
        
        var hasMessageId = this.compileDynamicElement(el);
        
        // if we have a message id, we don't want to compile down
        // into the element
        // XXX do some checking for illegal constructs
        if (hasMessageId) {
            return;
        }

        this.compileRender(el);

        for (var i = 0; i < el.childNodes.length; i++) {
            var node = el.childNodes[i];
            if (node.nodeType !== 1) {
                // skip all non-element nodes
                continue;
            }
            this.compileEl(node);
        }
    };

    module.Section.prototype.compileDynamicElement = function(el) {
        var dynamicElement = new module.DynamicElement(el);
        if (!dynamicElement.isDynamic()) {
            return false;
        }
        
        this.dynamicElements.push({
            finder: this.getElFinder(el),
            dynamicElement: dynamicElement
        });
        return dynamicElement.contentTrans !== null;
    };
    
    module.Section.prototype.getElIndexes = function(el) {
        var indexes = [];
        var parentNode = this.el;
        var node = el;
        while (node !== parentNode) {
            var children = node.parentNode.childNodes;
            for (var i = 0; i < children.length; i++) {
                if (children[i] === node) {
                    indexes.push(i);
                    break;
                }
            }
            node = node.parentNode;
        }
        indexes.reverse();
        return indexes;
    };
    
    module.Section.prototype.getElFinder = function(el) {
        var indexes = [];
        var parentNode = this.el;
        var node = el;
        while (node !== parentNode) {
            var children = node.parentNode.childNodes;
            for (var i = 0; i < children.length; i++) {
                if (children[i] === node) {
                    indexes.push(i);
                    break;
                }
            }
            node = node.parentNode;
        }
        indexes.reverse();

        var c = new module.Codegen('node');
        for (var j in indexes) {
            c.push('node = node.childNodes[' + indexes[j] + '];');
        }
        c.push('return node;');
        return c.getFunction();
    };
    
    module.Section.prototype.compileRender = function(el) {
        var renderElement = new module.RenderElement(el);

        if (!renderElement.isDynamic()) {
            return;
        }
        
        this.renderElements.push({
            finder: this.getElFinder(el),
            renderElement: renderElement
        });
    };
    
    module.Section.prototype.compileSubSection = function(el) {
        // create sub section with copied contents
        var subSection = new module.Section(el);
        
        if (!subSection.isDynamic()) {
            return false;
        }

        var oldEl = el;
        
        // make shallow copy of the element
        el = oldEl.cloneNode(false);

        // replace original el with shallow clone
        oldEl.parentNode.replaceChild(el, oldEl);
   
        this.subSections.push({
            finder: this.getElFinder(el),
            subSection: subSection
        });
        return true;
    };
    
    module.Section.prototype.render = function(el, scope, context) {
        if (this.dataIf) {
            var dataIf = this.dataIf.resolve(el, scope);
            if (!dataIf) {
                $(el).addClass('obviel-template-removal');
                return;
            }
        }

        if (this.dataRepeat) {
            this.renderRepeat(el, scope, context);
        } else {
            this.renderEl(el, scope, context);
        }
    };

    module.Section.prototype.renderRoot = function(el, scope, context) {
        var topEl = this.el.cloneNode(false);
        // append first, so that parentNode is available
        el.appendChild(topEl);
        this.render(topEl, scope, context);
    };
    
    var repeatInfo = function(index, name, dataRepeat) {
        var even = index % 2 === 0;
        var info = {
            index: index,
            number: index + 1,
            length: dataRepeat.length,
            even: even,
            odd: !even
        };
        var repeat = {};
        $.extend(repeat, info);
        repeat[name] = info;
        return {
            '@repeat': repeat
        };
    };
    
    module.Section.prototype.renderRepeat = function(el, scope, context) {
        var dataRepeat = this.dataRepeat(scope);
        if (!$.isArray(dataRepeat)) {
            throw new module.RenderError(
                el, ("data-repeat must point to an array, not to " +
                     $.type(dataRepeat)));
        }
        // empty array, so don't render any elements
        if (dataRepeat.length === 0) {
            el.parentNode.removeChild(el);
            return;
        }

        // prepare the element to keep cloning back into the
        // DOM for each iteration. this needs to be done here,
        // before its id is removed
        var iterationNode = el.cloneNode(false);

        // store some information about the first iteration
        var insertBeforeNode = el.nextSibling;
        var parentNode = el.parentNode;

        // render the first iteration on the element
        scope.push(repeatInfo(0, this.dataRepeatName, dataRepeat));
        scope.push(dataRepeat[0]);
        this.renderEl(el, scope, context);
        scope.pop();
        scope.pop();

        // now insert the next iterations after the first iteration
        for (var i = 1; i < dataRepeat.length; i++) {
            var iterationClone = iterationNode.cloneNode(false);
            parentNode.insertBefore(iterationClone, insertBeforeNode);

            scope.push(repeatInfo(i, this.dataRepeatName, dataRepeat));
            scope.push(dataRepeat[i]);
            this.renderEl(iterationClone, scope, context);
            scope.pop();
            scope.pop();
        }
    };
    
    module.Section.prototype.renderEl = function(el, scope, context) {
        if (this.dataWith) {
            var dataWith = this.dataWith(scope);
            if (dataWith === undefined) {
                throw new module.RenderError(
                    el, "data-with '" + this.dataWithName + "' " +
                    "could not be found");
            }
            var type = $.type(dataWith);
            if (type !== 'object') {
                throw new module.RenderError(el,
                    "data-with must point to an object, not to " + type);
            }
            scope.push(dataWith);
        }

        if (this.frag.hasChildNodes()) {
            el.appendChild(this.frag.cloneNode(true));
        }
        this.renderDynamicElements(el, scope, context);
        this.renderViews(el, scope, context);
        this.renderSubSections(el, scope, context);

        if (this.dataWith) {
            scope.pop();
        }
    };
    
    module.Section.prototype.renderDynamicElements = function(el, scope,
                                                                context) {
        for (var i in this.dynamicElements) {
            var value = this.dynamicElements[i];
            var dynamicEl = value.finder(el);
            value.dynamicElement.render(dynamicEl, scope, context);
        }
    };

    module.Section.prototype.renderViews = function(el, scope,
                                                     context) {
        for (var i in this.renderElements) {
            var value = this.renderElements[i];
            var renderEl = value.finder(el);
            value.renderElement.render(renderEl, scope, context);
        }
    };

    module.Section.prototype.renderSubSections = function(el, scope,
                                                            context) {
        var toRender = [];
        // first we find all elements. we do this before rendering starts,
        // as rendering can in some cases (data-repeat) insert new elements
        // and that would break the finding
        for (var i in this.subSections) {
            toRender.push({value: this.subSections[i],
                            subSectionEl: this.subSections[i].finder(el)});
        }
        // now we can do the rendering
        for (i in toRender) {
            var r = toRender[i];
            r.value.subSection.render(r.subSectionEl, scope, context);
        }
    };
    
    module.DynamicElement = function(el, allowTvar) {
        this.attrTexts = {};
        this.contentTexts = [];
        this.handlers = [];
        this.contentTrans = null;
        this.funcName = null;
        this.hasDataAttr = false;
        this._dynamic = false;
        this.transInfo = null;
        this.compile(el, allowTvar);
    };

    module.DynamicElement.prototype.isDynamic = function() {
        return this._dynamic;
    };
    
    module.DynamicElement.prototype.compile = function(el, allowTvar) {
        this.transInfo = new module.TransInfo(el, allowTvar);
        
        this.compileAttrTexts(el);
        this.compileContentTexts(el);
        this.compileDataOn(el);
        this.compileCall(el);
        this.compileSpecialAttr(el);
        this.compileDataEl(el);
        this.compileDataAttr(el);
        this.compileDataUnwrap(el);
        this.compileContentTrans(el);
    };
    
    module.DynamicElement.prototype.compileAttrTexts = function(el) {
        var attrText;
        for (var i = 0; i < el.attributes.length; i++) {
            var attr = el.attributes[i];
            if (attr.specified !== true) {
                continue;
            }
            if (attr.value === null) {
                continue;
            }
            var transInfo = this.transInfo.attributes[attr.name];
            if (transInfo === undefined) {
                transInfo = null;
            }
            attrText = new module.DynamicAttribute(el, attr.name, attr.value,
                                                    transInfo);
            if (!attrText.isDynamic()) {
                continue;
            }
            this.attrTexts[attr.name] = attrText;
            this._dynamic = true;
        }
    };
    
    module.DynamicElement.prototype.compileContentTexts = function(el) {
        for (var i = 0; i < el.childNodes.length; i++) {
            var node = el.childNodes[i];
            if (node.nodeType !== 3) {
                continue;
            }
            if (node.nodeValue === null) {
                continue;
            }
            var dynamicText = new module.DynamicText(el, node.nodeValue);
            if (dynamicText.isDynamic()) {
                this.contentTexts.push({
                    index: i,
                    dynamicText: dynamicText
                });
                this._dynamic = true;
            }
        }
    };
    
    module.DynamicElement.prototype.compileDataOn = function(el) {
        var dataOn = getDirective(el, 'data-on');
        if (dataOn === null) {
            return;
        }
        var nameFormatters = splitNameFormatters(el, dataOn);
        if (nameFormatters.length === 0) {
            throw new module.CompilationError(
                el, 'data-on: must have content');
        }
        for (var i = 0; i < nameFormatters.length; i++) {
            var nameFormatter = nameFormatters[i];
    
            if (!nameFormatter.formatter) {
                throw new module.CompilationError(
                    el, "data-on: handler function name is not specified");
            }
            this.handlers.push({eventName: nameFormatter.name,
                                handlerName: nameFormatter.formatter});
        }
        this._dynamic = true;
    };
    
    module.DynamicElement.prototype.compileCall = function(el) {
        var funcName = getDirective(el, 'data-call');
        if (funcName === null) {
            return;
        }
        this.funcName = funcName;
        this._dynamic = true;
    };

    module.DynamicElement.prototype.compileSpecialAttr = function(el) {
        var name, value;

        for (name in SPECIAL_ATTR) {
            if (!el.hasAttribute('data-' + name)) {
                continue;
            }
            value = el.getAttribute('data-' + name);
            if (!value) {
                throw new module.CompilationError(
                    el, "data-" + name + " cannot be empty");
            }
            $(el).addClass('obviel-template-special-attr');
        }
    };
    
    module.DynamicElement.prototype.compileDataEl = function(el) {
        if (!el.hasAttribute('data-el')) {
            return;
        }
        // non-destructively read data-el attribute, leave it in place
        // so variables can be used in it
        var dataEl = el.getAttribute('data-el');
        if (!dataEl) {
            throw new module.CompilationError(el, "data-el cannot be empty");
        }
        $(el).addClass('obviel-template-data-el');
    };

    module.DynamicElement.prototype.compileDataAttr = function(el) {
        if (!el.hasAttribute('data-attr')) {
            return;
        }
        // non-destructively read data-attr attribute, leave it in place
        // so variables can be used in it
        var dataAttr = el.getAttribute('data-attr');
        if (!dataAttr) {
            throw new module.CompilationError(
                el, "data-attr cannot be empty");
        }
        if (!el.hasAttribute('data-value')) {
            throw new module.CompilationError(
                el, "data-attr must be combined with data-value");
        }
        $(el).addClass('obviel-template-removal');
        this.hasDataAttr = true;
        this._dynamic = true;
    };

    module.DynamicElement.prototype.compileDataUnwrap = function(el) {
        if (!el.hasAttribute('data-unwrap')) {
            return;
        }
        $(el).addClass('obviel-template-data-unwrap');
    };

    module.DynamicElement.prototype.compileContentTrans = function(el) {
        if (this.transInfo.content === null) {
            return;
        }
        this._dynamic = true;

        var transInfo = this.transInfo.content;
        
        var singular = new module.ContentTrans(
            transInfo.messageId, transInfo.directive);
        var plural = new module.ContentTrans(
            transInfo.pluralMessageId, transInfo.directive);

        var current = singular;
        // XXX index only relevant for notrans case,
        // never for plural, so we are creating it for no reason in
        // many cases and it's not really relevant?
        var c = 0;
        for (var i = 0; i < el.childNodes.length; i++) {
            var node = el.childNodes[i];
            if (node.nodeType === 3) {
                // TEXTNODE
                var info = parseTextForPlural(node.nodeValue);
                if (info.afterPlural === null) {
                    current.compileNode(node, c);
                    c++;
                } else {
                    current.compileNode(document.createTextNode(
                        info.beforePlural), c);
                    c++;
                    current = plural;
                    current.compileNode(document.createTextNode(
                        info.afterPlural), c);
                    c++;
                }
            } else if (node.nodeType === 1) {
                // ELEMENT NODE
                current.compileNode(node, c);
                c++;
            }
        }

        singular.finalizeCompile(el);
        
        if (current === plural) {
            plural.finalizeCompile(el);
        } else {
            plural = null;
        }

        if (transInfo.countVariable !== null && plural === null) {
            throw new module.CompilationError(
                el, "data-plural used for element content but no || used " +
                    "to indicate plural text");
        }

        if (plural === null) {
            this.contentTrans = singular;
            return;
        }
        
        var countVariable;
        
        if (transInfo.countVariable !== null) {
            countVariable = transInfo.countVariable;
        } else {
            countVariable = getImplicitCountVariable(el, singular, plural);
        }
        
        this.contentTrans = new module.PluralTrans(
            singular,
            plural,
            countVariable);
    };
    
    module.DynamicElement.prototype.render = function(el, scope, context) {
        var self = this;
        
        for (var key in this.attrTexts) {
            this.attrTexts[key].render(el, scope, context);
        }
        
        // fast path without translations; elements do not need to be
        // reorganized
        if (this.contentTrans === null) {
            this.renderNotrans(el, scope, context);
            this.finalizeRender(el, scope, context);
            return;
        }

        this.contentTrans.render(
            el, scope, context,
            function(el, scope, context) {
                self.renderNotrans(el, scope, context);
            });
        this.finalizeRender(el, scope, context);
    };

    module.DynamicElement.prototype.renderNotrans = function(el, scope,
                                                              context) {
        for (var i = 0; i < this.contentTexts.length; i++) {
            var value = this.contentTexts[i];
            el.childNodes[value.index].nodeValue = value.dynamicText.render(
                el, scope, context);
        }
    };
    
    module.DynamicElement.prototype.renderDataAttr = function(el) {
        if (!this.hasDataAttr) {
            return;
        }
        var name = getDirective(el, 'data-attr');
        var value = getDirective(el, 'data-value');

        var parent = $(el.parentNode);
        
        // use jQuery to make attribute access more uniform (style in IE, etc)
        
        if (parent.attr(name)) {
            value = parent.attr(name) + ' ' + value;
        }
        parent.attr(name, value);
    };

    module.DynamicElement.prototype.renderDataOn = function(
        el, context) {
        if (this.handlers.length === 0) {
            return;
        }
        
        for (var i = 0; i < this.handlers.length; i++) {
            var handler = this.handlers[i];
            if (context.getHandler === null ||
                context.getHandler === undefined) {
                throw new module.RenderError(
                    el, "cannot render data-on for event '" +
                        handler.eventName + "' and handler '" +
                        handler.handlerName +
                        "' because no getHandler function " +
                        "was supplied");
            }
            var f = context.getHandler(handler.handlerName);
            if (f === undefined || f === null) {
                throw new module.RenderError(
                    el, "cannot render data-on for event '" +
                        handler.eventName + "' and handler '" +
                        handler.handlerName + "' because handler function " +
                        "could not be found");
            }
            $(el).bind(handler.eventName, f);
        }
    };
    
    module.DynamicElement.prototype.renderDataCall = function(
        el, scope, context) {
        if (this.funcName === null) {
            return;
        }
        var func = context.getFunc(this.funcName);
        if (!func) {
            throw new module.RenderError(
                el, 'cannot render data-call because cannot find function: ' +
                    this.funcName);
        }
        func($(el),
             function(name) { return scope.resolve(name); },
             context);
    };
    
    
    module.DynamicElement.prototype.finalizeRender = function(
        el, scope, context) {
        this.renderDataAttr(el);
        this.renderDataOn(el, context);
        this.renderDataCall(el, scope, context);
    };
    
    module.DynamicText = function(el, text) {
        this.parts = [];
        this.variables = [];
      
        var tokens = module.tokenize(text);
        var dynamic = false;
        
        for (var i in tokens) {
            var token = tokens[i];
            if (token.type === module.TEXT_TOKEN) {
                this.parts.push(token.value);
            } else if (token.type === module.NAME_TOKEN) {
                this.parts.push(null);
                this.variables.push({
                    index: i,
                    value: new module.Variable(el, token.value)
                });
                dynamic = true;
            }
        }
        this._dynamic = dynamic;
    };

    module.DynamicText.prototype.isDynamic = function() {
        return this._dynamic;
    };
    
    module.DynamicText.prototype.render = function(el, scope, context) {
        var result = this.parts.slice(0);
        for (var i in this.variables) {
            var variable = this.variables[i];
            result[variable.index] = variable.value.render(el, scope, context);
        }
        return result.join('');
    };

    module.DynamicText.prototype.renderRoot = function(el, scope, context) {
        var node = document.createTextNode(this.render(el, scope, context));
        el.appendChild(node);
    };
    
    module.DynamicAttribute = function(el, name, value, transInfo) {
        this.name = name;
        this.value = value;
        this.transInfo = transInfo;
        this.attrTrans = null;
        this.dynamicText = null;
        this._dynamic = false;
        this.compileNotrans(el);
        this.compileTrans(el);
    };

    module.DynamicAttribute.prototype.isDynamic = function() {
        return this._dynamic;
    };
    
    module.DynamicAttribute.prototype.compileNotrans = function(el) {
        var dynamicText = new module.DynamicText(el, this.value);
        // if there's nothing dynamic nor anything to translate,
        // we don't have a dynamic attribute at all
        if (!dynamicText.isDynamic() && this.transInfo === null) {
            return;
        }
        if (SPECIAL_ATTR[this.name] !== undefined) {
            throw new module.CompilationError(
                el, ("not allowed to use variables (or translation) " +
                     "in " + this.name + " attribute. " +
                     "Use data-" + this.name + " instead"));
        }
        this.dynamicText = dynamicText;
        this._dynamic = true;
    };
    
    module.DynamicAttribute.prototype.compileTrans = function(el) {
        if (this.transInfo === null) {
            return;
        }
        
        var parts = this.value.split('||');
        if (parts.length === 1) {
            if (this.transInfo.countVariable !== null) {
                throw new module.CompilationError(
                    el, "data-plural used for attribute content but no || " +
                    "used to indicate plural text: " +
                    this.transInfo.countVariable);
            }
            this.attrTrans = new module.AttributeTrans(
                el, this.name, this.value, this.transInfo.messageId);
            this._dynamic = true;
            return;
        }
        
        var singular = new module.AttributeTrans(
            el, this.name, parts[0],
            this.transInfo.messageId);
        var plural = new module.AttributeTrans(
            el, this.name, parts[1],
            this.transInfo.pluralMessageId);
        
        var countVariable;

        if (this.transInfo.countVariable !== null) {
            countVariable = this.transInfo.countVariable;
        } else {
            countVariable = getImplicitCountVariable(
                el, singular, plural);
        }

        this._dynamic = true;
        this.attrTrans = new module.PluralTrans(
            singular, plural, countVariable);
    };
    
    module.DynamicAttribute.prototype.render = function(el, scope, context) {
        var self = this;
        // fast path without translations
        if (context.getTranslation === undefined ||
            context.getTranslation === null ||
            this.attrTrans === null) {
            this.renderNotrans(el, scope, context);
            return;
        }
        this.attrTrans.render(
            el, scope, context,
            function(el, scope, context) {
                self.renderNotrans(el, scope, context);
            });
    };
    
    module.DynamicAttribute.prototype.renderNotrans = function(
        el, scope, context) {
        var classes = this.getSpecialClasses(el);
        el.setAttribute(this.name,
                        this.dynamicText.render(el, scope, context));
        this.restoreSpecialClasses(el, classes);
    };
    
    module.DynamicAttribute.prototype.getSpecialClasses = function(el) {
        var i = 0,
            specialClass,
            wrapped,
            classes = [];
        if (this.name === 'class') {
            wrapped = $(el);
            for (i = 0; i < SPECIAL_CLASSES.length; i++) {
                specialClass = SPECIAL_CLASSES[i];
                if (wrapped.hasClass(specialClass)) {
                    classes.push(specialClass);
                }
            }
        }
        return classes;
    };

    module.DynamicAttribute.prototype.restoreSpecialClasses = function(
        el, classes) {
        var i = 0,
            wrapped = $(el);
        for (i = 0; i < classes.length; i++) {
            wrapped.addClass(classes[i]);
        }
    };
    
    module.Variable = function(el, name) {
        var r = splitNameFormatter(el, name);
        this.name = r.name;
        this.formatter = r.formatter;
        this.fullName = name;
        validateDottedName(el, this.name);
        this.getValue = module.resolveFunc(r.name);
    };
    
    module.Variable.prototype.render = function(el, scope, context) {
        var result = this.getValue(scope);
        if (result === undefined) {
            throw new module.RenderError(el, "variable '" + this.name + "' " +
                                         "could not be found");
        }

        if (this.formatter !== null) {
            var formatter = context.getFormatter(this.formatter);
            if (!formatter) {
                throw new module.RenderError(
                    el, "cannot find formatter with name: " +
                        this.formatter);
            }
            result = formatter(result);
        }
        
        // if we want to render an object, pretty-print it
        var type = $.type(result);
        if (type === 'object' || type === 'array') {
            return JSON.stringify(result, null, 4);
        }
        return result;
    };

    module.RenderElement = function(el) {
        var dataRender = getDirective(el, 'data-render');
        if (dataRender === null) {
            this.dynamic = false;
            return;
        }
        validateDottedName(el, dataRender);
        this.dynamic = true;
        var r = splitNameFormatter(el, dataRender);
        this.objName = r.name;
        this.getValue = module.resolveFunc(r.name);
        this.viewName = r.formatter;
        if (this.viewName === null) {
            this.viewName = defaultViewName;
        }
    };
    
    module.RenderElement.prototype.isDynamic = function() {
        return this.dynamic;
    };

    module.RenderElement.prototype.render = function(el, scope, context) {
        var obj = this.getValue(scope);
        if (obj === undefined) {
            throw new module.RenderError(
                el, "data-render object '" + this.propertyName + "' " +
                    "could not be found");
        }
        var type = $.type(obj);
        if (type !== 'object' && type !== 'string') {
            throw new module.RenderError(
                el,
                "data-render must point to an object or string (URL), not to " + type);
        }
        
        // empty element
        while (el.hasChildNodes()) {
            el.removeChild(el.firstChild);
        }
        try {
            context.subviewPromises.push($(el).render(obj, this.viewName));
        } catch(e) {
            if (e instanceof obviel.LookupError) {
                throw new module.RenderError(el, e.toString());
            } else {
                throw e;
            }
        }
    };
    
    module.TransInfo = function(el, allowTvar) {
        this.content = null;
        this.attributes = {};
        this.anyTranslations = false;
        this.allowTvar = allowTvar;
        this.compile(el);
    };

    module.TransInfo.prototype.compile = function(el) {
        this.compileDataTrans(el);
        if (this.content !== null && el.hasAttribute('data-render')) {
            throw new module.CompilationError(
                el,
                "data-render not allowed when content is marked with data-trans");
        }
        this.compileDataTvar(el);
        this.compileDataPlural(el);
    };
    
    module.TransInfo.prototype.compileDataTrans = function(el) {
        var dataTrans = null;
        if (el.hasAttribute('data-trans')) {
            dataTrans = el.getAttribute('data-trans');
            el.removeAttribute('data-trans');
        }

        if (dataTrans === null) {
            return;
        }
        
        dataTrans = trim(dataTrans);
        
        // empty string will mean translating text content only
        if (dataTrans === '') {
            this.content = {id: '.', countVariable: null,
                            directive: 'dataTrans',
                            messageId: null, pluralMessageId: null};
            this.attributes = {};
            this.anyTranslations = true;
            return;
        }
        
        // split with space character
        var parts = dataTrans.split(' ');
        
        for (var i in parts) {
            var part = trim(parts[i]);
            if (part === '') {
                continue;
            }
            var transInfo = this.compileDataTransPart(el, part);
            if (transInfo.id === '.') {
                this.content = transInfo;
                this.anyTranslations = true;
            } else {
                this.attributes[transInfo.id] = transInfo;
                this.anyTranslations = true;
            }
        }
    };

    
    module.TransInfo.prototype.compileDataTransPart = function(el, text) {
        var parts = text.split(':');
        // there is nothing to translate
        if (parts.length === 1) {
            return {id: parts[0], countVariable: null,
                    messageId: null, pluralMessageId: null};
        }
        // too many :
        if (parts.length > 2) {
            throw new module.CompilationError(
                el, "illegal content in data-trans");
        }
        // we are referring to the text if we have no actual
        // content identifier
        if (parts[0] === '') {
            return {id: '.', countVariable: null,
                    directive: 'data-trans',
                    messageId: parts[1], pluralMessageId: null};
        }
        // we really do want to have the attribute we are trying to translate
        if (parts[0] !== '.' && !el.hasAttribute(parts[0])) {
            throw new module.CompilationError(
                el, "data-trans refers to a non-existent attribute");
        }
        return {id: parts[0], countVariable: null,
                messageId: parts[1], pluralMessageId: null};
    };


    module.TransInfo.prototype.compileDataTvar = function(el) {
        var dataTvar = null;
        if (el.hasAttribute('data-tvar')) {
            dataTvar = el.getAttribute('data-tvar');
            el.removeAttribute('data-tvar');
        }

        if (dataTvar === null) {
            return;
        }
        
        // we are not in a data-trans, so we cannot allow data-tvar here
        if (!this.allowTvar) {
            throw new module.CompilationError(
                el, ("data-tvar is not allowed outside data-trans or " +
                     "other data-tvar"));
        }
        // we already have content due to data-trans, this is an error
        if (this.content !== null) {
            throw new module.CompilationError(
                el, ("data-trans for element content and " +
                     "data-tvar cannot be both on same element"));
        }
        var tvarInfo = parseTvar(el, dataTvar);
        this.content = {id: '.',
                        directive: 'data-tvar',
                        messageId: tvarInfo.messageId,
                        pluralMessageId: tvarInfo.pluralMessageId,
                        countVariable: null};
    };
    
    module.TransInfo.prototype.compileDataPlural = function(el) {
        var dataPlural = null;
        if (el.hasAttribute('data-plural')) {
            dataPlural = el.getAttribute('data-plural');
            el.removeAttribute('data-plural');
        }

        if (dataPlural === null) {
            return;
        }
        
        dataPlural = trim(dataPlural);
        
        // empty string is not allowed for data-plural
        if (dataPlural === '') {
            throw new module.CompilationError(
                el, 'data-plural cannot be empty');
        }
        
        // split with space character
        var parts = dataPlural.split(' ');
        
        for (var i in parts) {
            var part = trim(parts[i]);
            if (part === '') {
                continue;
            }
            var pluralInfo = this.compileDataPluralPart(el, part);
            if (pluralInfo.id === '.') {
                if (this.content === null) {
                    throw new module.CompilationError(
                        el, "data-plural indicates element content not " +
                            "marked with data-trans");
                }
                this.content.countVariable = pluralInfo.countVariable;
                
            } else {
                var attrInfo = this.attributes[pluralInfo.id];
                if (attrInfo === undefined) {
                    throw new module.CompilationError(
                        el, "data-plural indicates attribute not " +
                            "marked with data-trans: " + pluralInfo.id);
                }
                attrInfo.countVariable = pluralInfo.countVariable;
            }
        }
    };
    
    module.TransInfo.prototype.compileDataPluralPart = function(el, text) {
        var parts = text.split(':');
        // only a count variable for content
        if (parts.length === 1) {
            return {id: '.', countVariable: parts[0]};
        }
        // too many :
        if (parts.length > 2) {
            throw new module.CompilationError(
                el, "illegal content in data-plural");
        }
        // we are referring to the text if we have no actual
        // content identifier
        if (parts[0] === '') {
            return {id: '.', countVariable: parts[1]};
        }
        return {id: parts[0], countVariable: parts[1]};
    };

    module.AttributeTrans = function(el, name, value, messageId) {
        this.messageId = null;
        this.variables = {};
        this.name = name;
        this.compile(el, value);
        if (messageId !== null) {
            this.messageId = messageId;
        }
    };

    module.AttributeTrans.prototype.compile = function(el, value) {
        var result = [];
        // can use cachedTokenize to speed up rendering slightly later
        var tokens = cachedTokenize(value);
        for (var i = 0; i < tokens.length; i++) {
            var token = tokens[i];
            if (token.type === module.NAME_TOKEN) {
                var nameFormatter = splitNameFormatter(el, token.value);
                var variable = this.variables[nameFormatter.name];
                if (variable !== undefined &&
                    variable.fullName !== token.value) {
                    throw new module.CompilationError(
                        el, ("same variables in translation " +
                             "must all use same formatter"));
                }
                this.variables[nameFormatter.name] = new module.Variable(
                    el, token.value);
                result.push('{' + nameFormatter.name + '}');
            } else {
                result.push(token.value);
            }
        }
        var messageId = result.join('');
        this.validateMessageId(el, messageId);
        this.messageId = messageId;
    };
    
    module.AttributeTrans.prototype.validateMessageId = function(
        el, messageId) {
        if (messageId === '') {
            throw new module.CompilationError(
                el, "data-trans used on attribute with no text to translate");
        }
        checkMessageId(el, messageId, 'attribute');
    };

    module.AttributeTrans.prototype.getVariable = function(el, scope,
                                                            context, name) {
        var variable = this.variables[name];
        if (variable === undefined) {
            throw new module.RenderError(
                el, "unknown variable in translation: " + name);
        }
        return variable.render(el, scope, context);
    };

    module.AttributeTrans.prototype.renderTranslation = function(
        el, scope, context, translation) {
        var result = [];

        var tokens = cachedTokenize(translation);
        
        for (var i = 0; i < tokens.length; i++) {
            var token = tokens[i];
            if (token.type === module.TEXT_TOKEN) {
                result.push(token.value);
            } else if (token.type === module.NAME_TOKEN) {
                result.push(this.getVariable(el, scope, context,
                                              token.value));
            }
        }
        
        el.setAttribute(this.name, result.join(''));
    };
    
    module.AttributeTrans.prototype.render = function(el, scope, context,
                                                      renderNotrans) {
        var getTranslation = context.getTranslation;

        var translation = context.getTranslation(this.messageId);
        if (translation === this.messageId) {
            // if translation is original message id, we can use fast path
            renderNotrans(el, scope, context);
            return;
        }

        this.renderTranslation(el, scope, context, translation);
    };

    module.ContentTrans = function(messageId, directiveName) {
        this.parts = [];
        this.messageId = null;
        this.tvars = {};
        this.variables = {};
        this.directiveName = directiveName;
        this.explicitMessageId = messageId;
    };

    module.ContentTrans.prototype.compileNode = function(node, index) {
        if (node.nodeType === 3) {
            // TEXTNODE
            var text = this.compileText(node);
            this.parts.push(text);
        } else if (node.nodeType === 1) {
            // ELEMENT NODE
            this.checkDataTransRestrictions(node);
            var tvarNode = node.cloneNode(true);
            var tvarInfo = this.compileTvar(tvarNode);
            this.parts.push("{" + tvarInfo.tvar + "}");
            // XXX dynamicNotrans is a bit ugly
            this.tvars[tvarInfo.tvar] = {
                node: tvarNode,
                index: index,
                dynamic: new module.DynamicElement(tvarNode, true),
                dynamicNotrans: new module.DynamicElement(node, true),
                render: tvarInfo.render
            };
        }
        // COMMENTNODE
        // no need to do anything, index for tvars will be correct
        // CDATASECTIONNODE
        // browser differences are rather severe, we just
        // don't support this in output as it's of a limited utility
        // see also:
        // http://reference.sitepoint.com/javascript/CDATASection
        // PROCESSINGINSTRUCTIONNODE
        // we also don't support processing instructions in any
        // consistent way; again they have limited utility
        // ATTRIBUTENODE, DOCUMENTFRAGMENTNODE, DOCUMENTNODE,
        // DOCUMENTTYPENODE, ENTITYNODE, NOTATIONNODE do not
        // occur under elements
        // ENTITYREFERENCENODE does not occur either in FF, as this will
        // be merged with text nodes
    };

    module.ContentTrans.prototype.finalizeCompile = function(el) {
        var messageId = this.parts.join('');
        messageId = module.normalizeSpace(trim(messageId));
        this.validateMessageId(el, messageId);
        this.messageId = messageId;
        if (this.explicitMessageId !== null) {
            this.messageId = this.explicitMessageId;
        }
    };
    
    module.ContentTrans.prototype.checkDataTransRestrictions = function(
        el) {
        // XXX more restrictions?
        if (el.hasAttribute('data-if')) {
            throw new module.CompilationError(
                el,
                "inside data-trans element data-if may not be used");
        }
        if (el.hasAttribute('data-with')) {
            throw new module.CompilationError(
                el,
                "inside data-trans element data-with may not be used");
        }
        if (el.hasAttribute('data-repeat')) {
            throw new module.CompilationError(
                el,
                "inside data-trans element data-repeat may not be used");
        }
    };
    
    module.ContentTrans.prototype.compileText = function(node) {
        // need to extract all variables for tvar uniqueness checking
        var result = [];
        var tokens = module.tokenize(node.nodeValue);
        for (var i = 0; i < tokens.length; i++) {
            var token = tokens[i];
            if (token.type === module.NAME_TOKEN) {
                var nameFormatter = splitNameFormatter(node, token.value);
                var variable = this.variables[nameFormatter.name];
                if (variable !== undefined &&
                    variable.fullName !== token.value) {
                    throw new module.CompilationError(
                        node, ("same variables in translation " +
                               "must all use same formatter"));
                }
                this.variables[nameFormatter.name] = new module.Variable(
                    node.parentNode, token.value);
                if (this.tvars[nameFormatter.name] !== undefined) {
                    throw new module.CompilationError(
                        node, "data-tvar must be unique within " +
                            this.directiveName + ":" +
                            token.value);
                }
                result.push('{' + nameFormatter.name + '}');
            } else {
                result.push(token.value);
            }
        }
        return result.join('');
    };
    
    module.ContentTrans.prototype.validateMessageId = function(
        el, messageId) {
        // data-tvar doesn't need any of these checks
        if (this.directiveName === 'data-tvar') {
            return;
        }
        if (messageId === '') {
            throw new module.CompilationError(
                el, "data-trans used on element with no text to translate");
        }
        checkMessageId(el, messageId, 'element');
    };
    
    module.ContentTrans.prototype.implicitTvar = function(node, render) {
        if (render !== null) {
            // data-render exists on element, use name as tvar name
            return render.objName;
        }
        // only if we have a single text child node that is a variable
        // by itself do we have an implicit tvar
        if (node.childNodes.length !== 1) {
            return null;
        }
        if (node.childNodes[0].nodeType !== 3) {
            return null;
        }
        var tokens = module.tokenize(node.childNodes[0].nodeValue);

        if (tokens.length !== 1 || tokens[0].type !== module.NAME_TOKEN) {
            return null;
        }
        var nameFormatter = splitNameFormatter(node, tokens[0].value);
        return nameFormatter.name;
    };

    module.ContentTrans.prototype.compileTvar = function(el) {
        var tvar = null;
        if (el.hasAttribute('data-tvar')) {
            tvar = el.getAttribute('data-tvar');
        }
        if (tvar !== null) {
            var tvarInfo = parseTvar(el, tvar);
            tvar = tvarInfo.tvar;
        }
        var render = null;
        if (el.hasAttribute('data-render')) {
            render = new module.RenderElement(el);
        }
        if (tvar === null) {
            tvar = this.implicitTvar(el, render);
            if (tvar === null) {
                throw new module.CompilationError(
                    el, this.directiveName + " element has sub-elements " +
                        "that are not marked with data-tvar");
            }
        }
        
        if (this.tvars[tvar] !== undefined ||
            this.variables[tvar] !== undefined) {
            throw new module.CompilationError(
                el, "data-tvar must be unique within " +
                    this.directiveName + ": " + tvar);
        }
        return {tvar: tvar, render: render};
    };

    module.ContentTrans.prototype.getTvarNode = function(
        el, scope, context, name) {
        var tvarInfo = this.tvars[name];
        if (tvarInfo === undefined) {
            return null;
        }
      
        var tvarNode = tvarInfo.node.cloneNode(true);

        tvarInfo.dynamic.render(tvarNode, scope, context);
        if (tvarInfo.render !== null) {
            tvarInfo.render.render(tvarNode, scope, context);
        }
        return tvarNode;
    };

    module.ContentTrans.prototype.getVariableNode = function(
        el, scope, context, name) {
        var variable = this.variables[name];
        if (variable === undefined) {
            throw new module.RenderError(
                el, "unknown variable in translation: " + name);
        }
        return document.createTextNode(variable.render(el, scope, context));
    };

    module.ContentTrans.prototype.getNode = function(
        el, scope, context, name) {
        var tvarNode = this.getTvarNode(el, scope, context,
                                           name);
        if (tvarNode !== null) {
            return tvarNode;
        }
        return this.getVariableNode(el, scope, context, name);
    };

    module.ContentTrans.prototype.renderTranslation = function(
        el, scope, context, translation) {
        var tokens = cachedTokenize(translation);

        var frag = document.createDocumentFragment();
        
        for (var i = 0; i < tokens.length; i++) {
            var token = tokens[i];
            if (token.type === module.TEXT_TOKEN) {
                frag.appendChild(document.createTextNode(token.value));
            } else if (token.type === module.NAME_TOKEN) {
                frag.appendChild(this.getNode(el, scope, context,
                                               token.value));
            }
        }

        // remove any previous contents
        while (el.hasChildNodes()) {
            el.removeChild(el.firstChild);
        }
        // move new contents in place
        el.appendChild(frag);
    };
    
    module.ContentTrans.prototype.render = function(el, scope, context,
                                                    renderNotrans) {
        var messageId = this.messageId;
        var translation = messageId;
        var getTranslation = context.getTranslation;
        if (getTranslation !== null && getTranslation !== undefined) {
            translation = getTranslation(messageId);
        }

        if (translation === messageId) {
            renderNotrans(el, scope, context);
            this.renderNotransTvars(el, scope, context);
            return;
        }
        
        this.renderTranslation(el, scope, context, translation);
    };

    module.ContentTrans.prototype.renderNotransTvars = function(
        el, scope, context) {
        var children = el.childNodes;
        for (var key in this.tvars) {
            var info = this.tvars[key];
            // this can use index, as we're not in a plural
            info.dynamicNotrans.render(children[info.index], scope, context);
        }
    };
    
    module.PluralTrans = function(singular,
                                  plural,
                                  countVariable) {
        this.singular = singular;
        this.plural = plural;
        this.countVariable = countVariable;
    };
    
    module.PluralTrans.prototype.getCount = function(el, scope) {
        var result = scope.resolve(this.countVariable);
        if (typeof result !== 'number') {
            throw new module.RenderError(
                el, "count variable in plural is not a number: " + result);
        }
        return result;
    };
    
    module.PluralTrans.prototype.render = function(
        el, scope, context, renderNotrans) {
        var count = this.getCount(el, scope);
        var translation = context.getPluralTranslation(
            this.singular.messageId,
            this.plural.messageId,
            count);
        // we use the source language plural form as the basis for
        // translation, meaning its tvars will end up in the translated
        // content if referred to by variable in the translation
        this.plural.renderTranslation(el, scope, context, translation);
    };
    
    module.IfExpression = function(el, text) {
        this.el = el;
        text = trim(text);
        this.notEnabled = text.charAt(0) === '!';
        var name = null;
        if (this.notEnabled) {
            name = text.slice(1);
        } else {
            name = text;
        }
        validateDottedName(el, name);
        this.dataIf = module.resolveFunc(name);
    };

    module.IfExpression.prototype.resolve = function(el, scope) {
        var result = this.dataIf(scope),
            tf;
        if ($.isArray(result) && result.length === 0) {
            tf = false;
        } else {
            if (result) {
                tf = true;
            } else {
                tf = false;
            }
        }
        if (this.notEnabled) {
            return (!tf);
        }
        return tf;
    };
    
    module.Scope = function(obj) {
        this.stack = [obj];
    };
    
    module.Scope.prototype.push = function(obj) {
        this.stack.push(obj);
    };
    
    module.Scope.prototype.pop = function() {
        this.stack.pop();
    };
    
    // note that this function is not used outside of the
    // translation system; instead function generated with the code
    // generator is used
    module.Scope.prototype.resolve = function(dottedName) {
        if (dottedName === '@.') {
            return this.stack[this.stack.length - 1];
        } else if (dottedName === '@open') {
            return '{';
        } else if (dottedName === '@close') {
            return '}';
        } else if (dottedName === '@doublepipe') {
            return '||';
        }
        var names = dottedName.split('.');
        for (var i = this.stack.length - 1; i >= 0; i--) {
            var obj = this.stack[i];
            var result = resolveInObj(obj, names);
            if (result !== undefined) {
                return result;
            }
        }
        return undefined;
    };

    resolveInObj = function(obj, names) {
        for (var i in names) {
            var name = names[i];
            obj = obj[name];
            if (obj === undefined) {
                return undefined;
            }
        }
        return obj;
    };
    
    module.resolveFunc = function(dottedName) {
        var c = new module.Codegen('scope');
        if (dottedName === '@.') {
            c.push('return scope.stack[scope.stack.length - 1];');
            return c.getFunction();
        } else if (dottedName === '@open') {
            c.push('return "{";');
        } else if (dottedName === '@close') {
            c.push('return "}";');
        } else if (dottedName === '@doublepipe') {
            c.push('return "||";');
        }
        
        c.push('for (var i = scope.stack.length - 1; i >= 0; i--) {');
        c.push('  var obj = scope.stack[i];');
        var names = dottedName.split('.');
        var name = null;
        for (var i = 0; i < names.length - 1; i++) {
            name = names[i];
            c.push('  obj = obj["' + name + '"];');
            c.push('  if (obj === undefined) {');
            c.push('    continue;');
            c.push('  }');
        }
        name = names[names.length - 1];
        c.push('  obj = obj["' + name + '"];');
        c.push('  if (obj !== undefined) {');
        c.push('    return obj;');
        c.push('  }');
        
        c.push('}');
        c.push('return undefined;' );
        return c.getFunction();
    };

    module.Registry = function() {
        this.clear();
    };
    
    module.Registry.prototype.register = function(name, f) {
        this.registrations[name] = f;
    };

    module.Registry.prototype.get = function(name) {
        return this.registrations[name];
    };

    module.Registry.prototype.clear = function() {
        this.registrations = {};
    };
    
    formatters = new module.Registry();

    module.registerFormatter = function(name, f) {
        formatters.register(name, f);
    };

    module.getFormatter = function(name) {
        return formatters.get(name);
    };
    
    module.clearFormatters = function() {
        formatters.clear();
    };
    
    funcs = new module.Registry();

    module.registerFunc = function(name, f) {
        funcs.register(name, f);
    };

    module.getFunc = function(name) {
        return funcs.get(name);
    };
    
    module.clearFuncs = function() {
        funcs.clear();
    };
   
    defaultViewName = 'default';

    module.setDefaultViewName = function(name) {
        defaultViewName = name;
    };

    morphElement = function(el, name) {
        var newEl = document.createElement(name);
        var i;
        
        // copy over all attributes from old element
        for (i = 0; i < el.attributes.length; i++) {
            var attr = el.attributes[i];
            if (attr.specified !== true) {
                continue;
            }
            if (attr.value === null) {
                continue;
            }
            newEl.setAttribute(attr.name, attr.value);
        }
        
        // copy over all sub-elements from old element
        for (i = 0; i < el.childNodes.length; i++) {
            var child = el.childNodes[i];
            newEl.appendChild(child);
        }

        // copy all events
        var events = $(el).data('events');
        if (events !== undefined) {
            $.each(events, function(key, value) {
                $.each(value, function(sub, v) {
                    $(newEl).bind(v.type, v.handler);
                });
            });
        }

        // put new element in its place
        el.parentNode.replaceChild(newEl, el);


        return newEl;
    };
    
    getDirective = function(el, name) {
        var value = null;
        if (!el.hasAttribute(name)) {
            return null;
        }
        
        value = el.getAttribute(name);
        if (value === null || value === '') {
            throw new module.CompilationError(
                el, name + " may not be empty");
        }
        
        el.removeAttribute(name);
        
        return value;
    };

    splitNameFormatters = function(el, text) {
        var parts = trim(text).split(' ');
        var result = [];
        for (var i = 0; i < parts.length; i++) {
            var part = parts[i];
            result.push(splitNameFormatter(el, part));
        }
        return result;
    };
    
    splitNameFormatter = function(el, name) {
        var nameParts = name.split('|');
        if (nameParts.length === 1) {
            return {
                name: nameParts[0],
                formatter: null
            };
        }
        if (nameParts.length !== 2) {
            throw new module.CompilationError(
                el, "variable may only have a single | in it");
        }
        return {
            name: nameParts[0],
            formatter: nameParts[1]
        };
    };

    checkMessageId = function(el, messageId, elementOrAttribute) {
        // cache the tokens so we get things faster during rendering time
        var tokens = cachedTokenize(messageId);
        var nameTokens = 0;
        for (var i in tokens) {
            var token = tokens[i];
            // if we run into any non-whitespace text token at all,
            // we assume we have something to translate
            if (token.type === module.TEXT_TOKEN) {
                if (trim(token.value) !== '') {
                    nameTokens = null;
                    break;
                }
            } else if (token.type === module.NAME_TOKEN) {
                nameTokens++;
            }
        }
        // we have found non-empty text tokens we can translate them
        if (nameTokens === null) {
            return;
        }
        // if we find no or only a single name token, we consider this
        // an error. more than one name tokens are considered translatable,
        // at least in their order
        if (nameTokens <= 1) {
            throw new module.CompilationError(
                el, "data-trans used on " + elementOrAttribute +
                    " with no text to translate");
        }
    };

    parseTvar = function(el, tvar) {
        var parts = tvar.split(':');
        if (parts.length === 1) {
            return {
                tvar: parts[0],
                messageId: null,
                pluralMessageId: null
            };
        } else if (parts.length === 0 ||
                   parts.length > 2 ||
                   parts[0] === '' ||
                   parts[1] === '') {
            throw new module.CompilationError(
                el, "illegal content in data-tvar");
        }
        return {
            tvar: parts[0],
            messageId: parts[1],
            pluralMessageId: null
        };
    };

    parseTextForPlural = function(text) {
        var beforePlural = [];
        var afterPlural = [];
        
        var current = beforePlural;
        var tokens = module.tokenize(text);
        for (var i = 0; i < tokens.length; i++) {
            var token = tokens[i];
            if (token.type === module.NAME_TOKEN) {
                current.push('{' + token.value + '}');
            } else {
                var value = token.value;
                // XXX shouldn't allow || twice
                var index = value.indexOf('||');
                if (index !== -1) {
                    current.push(value.slice(0, index));
                    current = afterPlural;
                    value = value.slice(index + 2);
                }
                current.push(value);
            }
        }

        beforePlural = beforePlural.join('');
        
        if (current === afterPlural) {
            afterPlural = afterPlural.join('');
        } else {
            afterPlural = null;
        }
        
        return {beforePlural: beforePlural,
                afterPlural: afterPlural};
    };

    var getAllVariableNames = function(singular, plural) {
        var names = $.extend({}, singular.variables);
        if (plural !== null) {
            $.extend(names, plural.variables);
        }
        return names;
    };
    
    getImplicitCountVariable = function(el, singular, plural) {
        var names = getAllVariableNames(singular, plural);
        
        var result = null;
        for (var name in names) {
            if (result === null) {
                result = name;
            } else {
                // if we already have seen a variable, then getting
                // a second variable means we have multiple variables
                // and therefore we can't find a single count variable
                result = null;
                break;
            }
        }
        if (result === null) {
            throw new module.CompilationError(
                el, "could not determine implicit count variable for plural");
        }
        return result;
    };

    // this might be too restrictive; we can open it up more on the long
    // run. We definitely don't want to open to {}, \, ' and ", as this
    // could potentially be used to sneak code into the
    // resolve function code generator.
    var validNameRe = new RegExp('[A-Za-z0-9_]+');
    
    validateDottedName = function(el, dottedName) {
        dottedName = trim(dottedName);
        if (dottedName === '') {
            throw new module.CompilationError(el, 'name cannot be empty');
        }
        if (dottedName === '@.') {
            return;
        }
        var parts = dottedName.split('.');
        for (var i = 0; i < parts.length; i++) {
            if (!validNameRe.exec(parts[i])) {
                throw new module.CompilationError(
                    el, "invalid name: " + dottedName);
            }
        }
    };

    getIndexInParent = function(el) {
        var parent = el.parentNode;
        if (parent === null || parent.nodeType !== 1) {
            return null;
        }
        var c = 0,
            found = null,
            node;
        for (var i = 0; i < parent.childNodes.length; i++) {
            node = parent.childNodes[i];
            if (node.nodeName !== el.nodeName) {
                continue;
            }
            if (parent.childNodes[i] === el) {
                found = c;
            }
            c++;
        }
        if (c <= 1) {
            return null;
        }
        return found + 1;
    };
    
    module.getXpath = function(el) {
        var index, name, path = [];
        while (el !== null && el.nodeType === 1) {
            name = el.tagName.toLowerCase();
            index = getIndexInParent(el);
            if (index !== null) {
                name += '[' + index + ']';
            }
            path.push(name);
            el = el.parentNode;
        }
        path.reverse();
        return '/' + path.join('/');
    };

    var startsWith = function(s, startswith) {
        return (s.slice(0, startswith.length) === startswith);
    };

    trim = function(s) {
        return s.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
    };

    module.normalizeSpace = function(s) {
        return s.replace(/^\s+|\s+$/g, '').replace(
                /\s{2,}/g, ' ').replace(/\s/g, ' ');
    };
    
    isHtmlText = function(text) {
        return trim(text).charAt(0) === '<';
    };

    module.Codegen = function(args) {
        this.args = args;
        this.result = [];
    };

    module.Codegen.prototype.push = function(s) {
        this.result.push(s);
    };

    module.Codegen.prototype.getFunction = function() {
        /*jshint evil:true*/
        var code = this.result.join('');
        return new Function(this.args, code);
    };

    module.variables = function(text, variables) {
        var tokens = module.tokenize(text);
        var result = [];
        for (var i = 0; i < tokens.length; i++) {
            var token = tokens[i];
            if (token.type === module.NAME_TOKEN) {
                result.push(variables[token.value]);
            } else if (token.type === module.TEXT_TOKEN) {
                result.push(token.value);
            }
        }
        return result.join('');
    };
    
    module.tokenize = function(text) {
        // fast path for empty text
        if (text === '') {
            return [];
        }
        var result = [];
        var index = 0;
        var lastIndex = 0;
        var textToken = '';
        while (true) {
            var openIndex = text.indexOf('{', index);
            if (openIndex === -1) {
                textToken = text.slice(lastIndex);
                if (textToken !== '') {
                    result.push({
                        type: module.TEXT_TOKEN,
                        value: textToken
                    });
                }
                break;
            }
            var nextChar = text.charAt(openIndex + 1);
            if (nextChar === '' ||
                nextChar === ' ' ||
                nextChar === '\t' ||
                nextChar === '\n') {
                index = openIndex + 1;
                continue;
            }
            index = openIndex + 1;
            var closeIndex = text.indexOf('}', index);
            if (closeIndex === -1) {
                textToken = text.slice(lastIndex);
                if (textToken !== '') {
                    result.push({
                        type: module.TEXT_TOKEN,
                        value: textToken
                    });
                }
                break;
            }
            textToken = text.slice(lastIndex, openIndex);
            if (textToken !== '') {
                result.push({
                    type: module.TEXT_TOKEN,
                    value: textToken
                });
            }
            var nameToken = text.slice(index, closeIndex);
            var trimmedNameToken = trim(nameToken);
            if (trimmedNameToken === '') {
                result.push({
                    type: module.TEXT_TOKEN,
                    value: '{' + nameToken + '}'
                });
            } else {
                result.push({
                    type: module.NAME_TOKEN,
                    value: trimmedNameToken
                });
            }
            index = closeIndex + 1;
            lastIndex = index;
        }
        
        return result;
    };

    var _tokenCache = {};

    var MAXCACHEDTEXTLENGTH = 100;
    
    cachedTokenize = function(text) {
        // don't cache if it's too big
        if (text.length > MAXCACHEDTEXTLENGTH) {
            return module.tokenize(text);
        }
        var cached = _tokenCache[text];
        if (cached !== undefined) {
            return cached;
        }
        var result = module.tokenize(text);
        _tokenCache[text] = result;
        return result;
    };
    
}(jQuery, obviel.template));
