if ( !framework.isDeclared("org.iit.Template") ) {

/**
 * data = [
 *     {
           name: "Pit",
           age: 30,
           kids: [
               { name: "Sid" },
               { name: "Joe" }
           ]
       },
       {
           name: "Joly",
           age: 30,
           kids: []
       }
 * ]
 **/

/**
 * <table>
 *   <thead>
 *       <tr><td>Номер</td><td>Имя</td><td>Дети</td></tr>
 *   </thead>
 *   <tr forEach="." filter="age &gt; 18" class="{counter%2==0?'odd':'even'}"><!-- context data[i] -->
 *     <td>{index}</td>
 *     <td>{name}</td>
 *     <td>
 *       <table if="kids && kids.length &gt; 0">
 *         <tr forEach="kids" class="{counter%2==0?'odd':'even'}"><!-- context is data[i].kids[j] -->
 *           <td>{name}</td>
 *         </tr>
 *       </table>
 *     </td>
 *   </tr>
 * </table>
 *
 **/

framework.declareClass("org.iit.Template", org.iit.Widget,
    function(config) {
        org.iit.Template.superclass.constructor.apply(this, arguments);

        this.templateText = config.templateText||null;

        this.init();
    },
    {
        init: function() {
            if ( this.templateText !== null ) {
                var matcher = { pos: 0 };
                this.section = new org.iit.template.Section(this.templateText, this, matcher);
                if ( matcher.pos != this.templateText.length ) {
                    throw new Error("syntax error on position " + matcher.pos + " no opening tag");
                }
            } else {
                this.section = new org.iit.template.Section(this._element.childNodes, this);
            }
        },
        applyTemplate: function(data) {
            var context = { parent: null, data: data };
            var output = this.section.applyTemplate(context);
            var html = output.join("");
            if ( this._element ) {
                this._element.innerHTML = html;
            }
            return html;
        }
    },
    {
        createFromHtml: function(element) {
            var config = { id: element.getAttribute("id"), element: element };
            var template = new org.iit.Template(config);
            return template;
        }
    }
);

framework.declareClass("org.iit.template.Section", null,
    function(node, parent, matcher) {
        this.parsed = [];
        this._length = 0;
        this.parent = parent instanceof org.iit.template.Section?parent:null;
        this.template = parent instanceof org.iit.Template?parent:parent&&parent.template||null;

        if ( framework.isString(node) ) {
            var m = matcher||{ pos: 0 };
            m.pos = this.parseHtml(node, m.pos);
        } else {
            if ( typeof node.length == "undefined" ) {
                this.parseNode(node);
            } else {
                this.parseNodeList(node);
            }
        }
    },
    {
        _pushParsed: function(obj) {
            if ( this._length > 0 ) {
                if ( framework.isString(obj) && framework.isString(this.parsed[this._length-1]) ) {
                    this.parsed[this._length-1] += obj;
                } else {
                    this._length = this.parsed.push(obj);
                }
            } else {
                this._length = this.parsed.push(obj);
            }
        },
        parseNodeList: function(nodes) {
            var i, l = nodes.length;
            for ( i = 0; i < l; i++ ) {
                this.parseNode(nodes[i]);
            }
        },
        parseHtml: function(html, pos) {
            var p, pClose = html.substr(pos).search(org.iit.template.Section.CLOSE_TAG_PATTERN);
            while ( (p = html.substr(pos).search(org.iit.template.Section.OPEN_TAG_PATTERN)) != -1 && p < pClose ) {
                if ( p != 0 ) {//text before tag
                    this.parseText(html.substr(pos, p));
                }
                pos = this.parseElementHtml(html, pos + p);
                pClose = html.substr(pos).search(org.iit.template.Section.CLOSE_TAG_PATTERN);
            }
            if ( pos < html.length ) {
                if ( (p = html.substr(pos).search(org.iit.template.Section.CLOSE_TAG_PATTERN)) != -1 ) {
                    this.parseText(html.substr(pos, p));
                    pos = pos + p;
                } else {
                    this.parseText(html.substr(pos));
                    pos = html.length;
                }
            }
            return pos;
        },
        parseElementHtml: function(html, pos) {
            var matches, p = 0, attr, attributes = {}, closeMatch, matcher = { pos: 0 }, l, c;
            matches = html.substr(pos).match(org.iit.template.Section.OPEN_TAG_PATTERN);
            var l = matches[2].length;
            if ( matches[2].charAt(l-1) == "/" ) {
                l--;
            }
            while ( (c = matches[2].charCodeAt(l-1)) == 9 || c == 10 || c == 13 || c == 32 ) {
                l--;
            }
            while ( p < l ) {
                if ( attr = matches[2].substr(p).match(org.iit.template.Section.ATTRIBUTE_PATTERN) ) {
                    attributes[attr[1].toLowerCase()] = attr[3];
                    p += attr[0].length;
                } else {
                    throw new Error("syntax error at position " + (pos+p) + ", attribute expected");
                }
            }
            if ( attributes["foreach"] ) {
                matcher.pos = pos;
                matcher.attributes = attributes;
                matcher.tagName = matches[1];
                matcher.html = matches[0];
                this._pushParsed(new org.iit.template.ForEachSection(html, this, matcher));
                return matcher.pos;
            } else if ( attributes["if"] ) {
                matcher.pos = pos;
                matcher.attributes = attributes;
                matcher.tagName = matches[1];
                matcher.html = matches[0];
                this._pushParsed(new org.iit.template.IfSection(html, this, matcher));
                return matcher.pos;
            } else {
                return this._processElement(html, pos, matches[1], attributes, matches[0]);
            }
        },
        _processElement: function(html, pos, tagName, attributes, elementHtml) {
            var attr, l = elementHtml.length, p, matches;
            this._pushParsed("<" + tagName);

            for ( attr in attributes ) {
                if ( attr == "styletemplate" ) {
                    this._pushParsed(" style=\"");
                } else if ( attr == "srctemplate" ) {
                    this._pushParsed(" src=\"");
                } else {
                    this._pushParsed(" " + attr + "=\"");
                }
                this.parseText(attributes[attr]);
                this._pushParsed("\"");
            }
            if ( elementHtml.charAt(l-2) == "/" ) {//closed tag
                this._pushParsed("/>");
                return pos + l;
            } else {
                this._pushParsed(">");
                pos = this.parseHtml(html, pos + l);
                if ( (p = html.substr(pos).search(org.iit.template.Section.CLOSE_TAG_PATTERN)) != -1 ) {
                    if ( p != 0 ) throw new Error("syntax error at position " + pos);
                    matches = html.substr(pos).match(org.iit.template.Section.CLOSE_TAG_PATTERN);
                    if ( tagName != matches[1] ) {
                        throw new Error("syntax error at position " + pos + " closing tag '" + tagName + "', '" + matches[1] + "' found");
                    }
                    this._pushParsed("</" + tagName + ">");
                    return pos + matches[0].length;
                } else {
                    throw new Error("syntax error at position " + pos + " closing tag '" + tagName + "' expected");
                }
            }
        },
        parseNode: function(node) {
            switch ( node.nodeType ) {
                case 1:/* Element */
                    this.parseElement(node);
                    break;
                case 3:/* Text */
                    this.parseText(node.nodeValue);
                    break;
                default: 
                    /* Ignore other types */
                    break;
            }
        },
        parseElement: function(node) {
            if ( (attr = node.getAttributeNode("forEach")) != null ) {
                this._pushParsed(new org.iit.template.ForEachSection(node, this));
            } else if ( (attr = node.getAttributeNode("if")) != null ) {
                this._pushParsed(new org.iit.template.IfSection(node, this));
            } else {
                this._pushParsed("<" + node.tagName);

                var i, l = node.attributes.length;
                for ( i = 0; i < l; i++ ) {
                    if ( node.attributes[i].specified && (node.attributes[i].nodeValue || node.attributes[i].nodeName.toLowerCase() == "style") ) {
                        if ( node.attributes[i].nodeName.toLowerCase() == "styletemplate" ) {
                            this._pushParsed(" style=\"");
                            this.parseText(node.attributes[i].nodeValue + "");
                            this._pushParsed("\"");
                        } else if ( node.attributes[i].nodeName.toLowerCase() == "srctemplate" ) {
                            this._pushParsed(" src=\"");
                            this.parseText(node.attributes[i].nodeValue + "");
                            this._pushParsed("\"");
                        } else if ( node.attributes[i].nodeName.toLowerCase() == "style" && node.attributes[i].nodeValue == null ) {
                            this._pushParsed(" style=\"");
                            this._pushParsed(node.style.cssText.replace(/"/g, "'"));
                            this._pushParsed("\"");
                        } else {
                            this._pushParsed(" " + node.attributes[i].nodeName + "=\"");
                            this.parseText(node.attributes[i].nodeValue + "");
                            this._pushParsed("\"");
                        }
                    }
                }
                l = node.childNodes.length;
                if ( l > 0 ) {
                    this._pushParsed(">");
                    for ( i = 0; i < l; i++ ) {
                        this.parseNode(node.childNodes[i]);
                    }
                    this._pushParsed("</" + node.tagName + ">");
                } else {
                    if ( org.iit.template.Section.NO_CLOSING_TAG.indexOf(" " + node.tagName.toUpperCase() + " ") != -1 ) {
                        this._pushParsed(" />");
                    } else {
                        this._pushParsed("></" + node.tagName + ">");
                    }
                }
            }
        },
        parseText: function(text) {
            var pattern = /{(?!{)([^}]+)}/, pos = 0, p, l = text.length;

            while ( pos < l && (p = text.substr(pos).search(pattern)) != -1 ) {
                if ( p > 0 ) {
                    this._pushParsed(text.substr(pos, p));
                }
                var matches = text.substr(pos).match(pattern);
                this._pushParsed(this.getPlaceholderFunc(matches[1]));
                pos += p + matches[0].length;
            }
            this._pushParsed(text.substr(pos));
        },
        applyTemplate: function(context) {
            var output = [];
            for ( var i = 0; i < this.parsed.length; i++ ) {
                switch ( typeof this.parsed[i] ) {
                    case "string":
                        output.push(this.parsed[i]);
                        break;
                    case "function":
                        output.push(framework.runInScope(this.template, this.parsed[i])(context));
                        break;
                    case "object":
                        output = output.concat(this.parsed[i].applyTemplate(context));
                        break;
                }
            }
            return output;
        },
        getPlaceholderFunc: function(expression) {
            return new Function("context",
                "var parent=context.parent?context.parent.data:null;\n"
              + "var root=context.root?context.root.data:context.data;\n"
              + "var counter=context.counter;\n"
              + "var index=context.index;\n"
              + "var returnValue = null;\n"
              + "if ( typeof context != 'undefined' && typeof context.data != 'undefined' && !!context.data ) {\n"
              + "    with ( context.data ) {\n"
              + "        returnValue = (" + (expression=="."?"context.data":expression) + ");\n"
              + "    }\n"
              + "} else {\n"
              + "    returnValue = (" + (expression=="."?"context.data":expression) + ");\n"
              + "}\n"
              + "return returnValue;\n"
            );
        }
    },
    {
        NO_CLOSING_TAG: " BR IMG ",
        OPEN_TAG_PATTERN: /<([A-z]+)([^>]*)>/,
        CLOSE_TAG_PATTERN: /<\/([A-z]+)>/,
        ATTRIBUTE_PATTERN: /^\s+([A-z0-9-]+)\s*=\s*("|')((?:.|\n|\r)*?)\2/
    }
);

framework.declareClass("org.iit.template.ForEachSection", org.iit.template.Section,
    function(node, parent, matcher) {
        this.parsed = [];
        this._length = 0;
        this.parent = parent;
        this.template = parent&&parent.template||null;

        if ( framework.isString(node) ) {
            var selector = matcher.attributes["foreach"];

            this.selector = new Function("context",
                (selector==""||selector==".")?"return context.data;\n":"return context.data." + selector + ";\n"
            );
            delete matcher.attributes["foreach"];

            var attr = matcher.attributes["filter"];
            this.filter = matcher.attributes["filter"]?this.getPlaceholderFunc(matcher.attributes["filter"]):null;
            delete matcher.attributes["filter"];

            matcher.pos = this._processElement(node, matcher.pos, matcher.tagName, matcher.attributes, matcher.html);
        } else {
            var selector = node.getAttribute("forEach");

            this.selector = new Function("context",
                (selector==""||selector==".")?"return context.data;\n":"return context.data." + selector + ";\n"
            );
            node.removeAttribute("forEach");

            var attr = node.getAttributeNode("filter");
            this.filter = attr?this.getPlaceholderFunc(attr.nodeValue):null;
            node.removeAttribute("filter");
            this.parseNode(node);
        }
    },
    {
        applyTemplate: function(context) {
            var newContext = { parent: context, index: -1, counter: -1, data: null, root: context.root||context };
            var arr = this.selector(context), index, counter = 0;
            var output = [];
            if ( !arr ) return output;
            for ( index in arr ) {
                newContext.data = arr[index];
                newContext.index = index;
                if ( this.filter && this.filter(newContext) || !this.filter ) {
                    newContext.counter = counter;
                    output = output.concat(org.iit.template.ForEachSection.superclass.applyTemplate.apply(this, [newContext]));
                    counter++;
                }
            }
            return [output.join("")];
        }
    }
);

framework.declareClass("org.iit.template.IfSection", org.iit.template.Section,
    function(node, parent, matcher) {
        this.parsed = [];
        this._length = 0;
        this.parent = parent;
        this.template = parent&&parent.template||null;

        var isString = framework.isString(node);

        if ( isString ) {
            var filter = matcher.attributes["if"];
            delete matcher.attributes["if"];
        } else {
            var filter = node.getAttribute("if");
            node.removeAttribute("if");
        }

        this.filter = new Function("context",
            "var parent=context.parent?context.parent.data:null;\n"
          + "var root=context.root?context.root.data:context.data;\n"
          + "var returnValue = null;\n" 
          + "if ( typeof context != 'undefined' && typeof context.data != 'undefined' && !!context.data ) {\n"
          + "    with (context.data) {\n"
          + "        returnValue = (" + filter + ");\n"
          + "    }\n"
          + "} else {\n"
          + "    returnValue = (" + filter + ");\n"
          + "}\n"
          + "return returnValue;\n"
        );

        if ( isString ) {
            matcher.pos = this._processElement(node, matcher.pos, matcher.tagName, matcher.attributes, matcher.html);
        } else {
            this.parseNode(node);
        }
    },
    {
        applyTemplate: function(context) {
            var output = [];

            if ( this.filter(context) ) {
                output = output.concat(org.iit.template.IfSection.superclass.applyTemplate.apply(this, [context]));
            }

            return [output.join("")];
        }
    }
);

}
