/**
 * 
 * JavaScript Template v1.5r (refactored)
 *
 * Template includes:
 * 
 * ~ variables substitution:
 * 
 * ++ <span class="jst-expr">
 *      Any <a href="?id=${id}">${expression.length}</a> with HTML, no other jst tags
 *    </span>
 * 
 * ~ visibility:
 * 
 * ++ <div class="jst-show-if{data.length != 0}">content</div>
 * 
 * ~ loops:
 * 
 * ++ <ol>
 *      <li class="jst-item-of(array) jst-expr">${_index_}:${_value_} of ${_total_}</li>
 *    </ol>
 * ++ <dl class="jst-each(object)">
 *      <dt class="jst-expr">${_key_}</dt>
 *      <dd class="jst-expr">${_value_}</dd>
 *    </dl>
 *
 * jst-expr -- Not to use jst-expr on table, tbody, thead, tfoot, tr
 * jst-attr(name:expr) - repeatable
 * jst-show-if(expr) - executed first
 * jst-item-of(expr) - executed second
 * jst-each(expr) - object (_key_,_value_) of array (_index_ from 1,_value_ or object item context,_total_)
 * jst-join(expr[:separator]) - like jst-expr joined with seprator
 * jst-template(name[:expr]) - render template with context
 * jst-with(obj) - context changing
 * 
 **/
 
jst = function(template) // ---------------------------------------------------
{
    if (this == window) 
        return new jst(template);
    if (template) 
        this.load(template);
}

jst.templates = {};

jst.prototype.template = null;

jst.prototype.isLoaded = function() // ----------------------------------------
{
    return !! this.constructor.templates[this.template];
}

jst.prototype.setNodes = function(nodes) // -----------------------------------
{
    this.constructor.templates[this.template] = nodes;
}

jst.prototype.getNodes = function() // ----------------------------------------
{
    return this.constructor.templates[this.template];
}

jst.prototype.load = function(template) // ------------------------------------
{
    this.template = template;
    if (this.isLoaded()) 
        return;
    var node = jst._node(template);
    var nodes = [];
    jst._each(node, function(child){
        nodes.push(child);
    });
    this.setNodes(nodes);
}

jst.prototype.render = function(context) // -----------------------------------
{
    jst._clear(this.template);
    this.renderTo(this.template, context);
}

jst.prototype.renderTo = function(destination, context) // --------------------
{
    var template = jst._node(this.template);
    destination  = jst._node(destination);

    this.load(this.template);

    destination.style.display = ''; // show destination

    var self = this;
    jst._each(this.getNodes(), function (node) {
        self._render({
            node        : node, 
            destination : destination, 
            context     : context
        });
    });
}

jst.prototype._render = function(options) // ----------------------------------
{
    if (! options.renderers) 
        options.renderers = this.parseOptions(options.node);
    if (! options.attributes)
        options.attributes = [];

    while (options.renderers.length > 0) {
        var r         = options.renderers.shift();
        options.value = r.value;
        if (r.renderer.apply(this, [options]))
            return;
    }
    options.value     = null;
    this.renderers._default.apply(this, [options]);
}

jst.prototype.renderers = {}; // ==============================================

jst.prototype.renderers['with'] = function (opt) // ---------------------------
{
    var new_context = this.evaluate(opt.value, opt.context);
    new_context._parent_ = opt.context;
    opt.context = new_context;
};

jst.prototype.renderers.item_of = function(opt) // ----------------------------
{
    var array = this.evaluate(opt.value, opt.context);
    if (! array) return true;
    
    for (var i = 0; i < array.length; i++) {
        var options = jst._clone_opt(opt);

        options.context = array[i].constructor == Object ? 
            array[i] : { _value_ : array[i] };
        options.context._parent_ = opt.context; 
        options.context._index_  = i + 1;
        options.context._total_  = array.length;
        
        this._render(options);
    }
    return true; // no further processing
};

jst.prototype.renderers.show_if = function(opt) // ----------------------------
{
    return ! this.evaluate(opt.value, opt.context);
};

jst.prototype.renderers.attr = function(opt) // -------------------------------
{
    var p     = opt.value.indexOf(':');
    var name  = opt.value.substr(0, p).trim();
    var value = opt.value.substr(p + 1).trim();
    value     = this.evaluate(value, opt.context);
    opt.attributes.push({ name: name, value: value });
};

jst.prototype.renderers.template = function(opt) // ---------------------------
{
    var pos = opt.value.indexOf(':');
    var name;
    var context;
    if (pos == -1) {
        name    = opt.value;
        context = '_context_';
    }
    else {
        name    = opt.value.substr(0, pos).trim();
        context = opt.value.substr(pos + 1);
    }
    jst(name).renderTo(
        jst._append(opt),
        this.evaluate(context, opt.context)
    );
    return true; // no further processing
};

jst.prototype.renderers.each = function(opt) // -------------------------------
{
    var object = this.evaluate(opt.value, opt.context);
    if (! object) 
        return true;

    var clone = jst._append(opt);
    var self  = this;
    var iterator;
    
    if (object.constructor == Array)
        iterator = function (item, index) {
            var options = jst._clone_opt(opt);
            options.context = item.constructor == Object ?
                item : { _value_ : item };
                
            options.context._parent_ = opt.context; 
            options.context._index_  = index + 1;
            options.context._total_  = object.length;

            options.destination = clone;
            options.no_append   = true;

            self._render(options);
        };
    else
        iterator = function (key, value) {
            var options = jst._clone_opt(opt);
            options.context = value.constructor == Object ?
                value : { _value_ : value };

            options.context._parent_ = opt.context; 
            options.context._key_    = key;

            options.destination = clone;
            options.no_append   = true;

            self._render(options);
        };
    
    jst._each(object, iterator);

    return true; // no further processing
};

jst.prototype.renderers.expr = function (opt) // ------------------------------
{
    jst._updateInnerHtml(
       opt.no_append ? opt.destination : jst._append(opt), 
       this.expression(opt.node.innerHTML, opt.context),
        opt.no_append ? opt.destination.innerHTML : ''
    );
    return true; // no further processing
};

jst.prototype.renderers._default = function (opt) // --------------------------
{
    var clone = jst._append(opt);
    var self  = this;
    jst._each(opt.node, function(child) {
        var options         = jst._clone_opt(opt);
        options.destination = clone;
        options.node        = child;
        options.renderers   = null;
        options.attributes  = null;
        self._render(options);
    });
};

jst.prototype.parseOptions = function(node) // --------------------------------
{
    if (node.nodeName == '#text' || ( !node.className && !node._jst_options )) 
       return [];
    if (node._jst_options)
        return node._jst_options.copy();

    var options = [];

    var paramstr = node.className;
    var regexp   = new RegExp(
    '\\bjst-([\\w\\-]+)((\\([^\\)]+\\))|(\\{[^\\}]+\\})|(#[^#]+#))?\\s*', 'g');
    var css      = '';
    var lastpos  = 0;
    var res      = regexp.exec(paramstr);
 
    while (res) {
        var cstart = regexp.lastIndex - res[0].length;
        css += paramstr.substr(lastpos, cstart - lastpos);
        lastpos = regexp.lastIndex;

        var value = res[2] ? res[2].substr(1,res[2].length - 2) : true;
        var name  = res[1].replace('-', '_');
        
        if (! this.renderers[name]) continue; // no such option

        options.push({ renderer: this.renderers[name], value: value });
            
        res = regexp.exec(paramstr);
    }
    node.className = (css + paramstr.substr(lastpos)).trim();
    node._jst_options = options.copy();
    return options;
}

// evaluate "var + 1" expression
jst.prototype.evaluate = function(code, context) // ---------------------------
{
    var execute = function(___code) {
        var ___s = '';
        for (var ___k in this)
           ___s += 'var ' + ___k + ' = this.' + ___k + ';';
        eval(___s);
        var _context_ = this;
        var ___res;
        try
        {
           eval("___res = " + ___code);
        }
        catch(e) {}
        return ___res; 
    };
    return execute.apply(context, [code]);
}

// evaluate "some ${var} expression"
jst.prototype.expression = function(code, context) // -------------------------
{
    var regexp = new RegExp('\\$\\{([^\\}]+)\\}|\\$%7B(([^%]|%[^7]|%.[^D])+)%7D', 'g');
    var result = '';
    var last   = 0;
    var res    = regexp.exec(code);

    while (res) {
        var start = regexp.lastIndex - res[0].length;
        result   += code.substr(last, start - last);
        
        result   += this.evaluate(
            res[1] ? res[1] : unescape(res[2]), 
            context
        );
        last      = regexp.lastIndex;

        res = regexp.exec(code);
    }
    result += code.substr(last);
    return result;
}

// utils ======================================================================

jst._append = function(opt) // ------------------------------------------------
{
    if (opt.no_append)
        return opt.destination;
    var clone = opt.node.cloneNode(false);
    opt.destination.appendChild(clone);
    jst._each(opt.attributes, function(attr) {
        if (attr.name == 'class')
            clone.className = clone.className + ' ' + attr.value;
        else if (attr.name == 'for')
            clone.htmlFor = attr.value;
        else
            clone.setAttribute(attr.name, attr.value);
    });
    return clone;
}

jst._updateInnerHtml = function(node, html) // ---------------------------------
{
    // use jQuery, as there are problems with innerHTML in tables in IE
    // jQuery(node).html(html);
    // Not to use jst-expr on table, tbody, thead, tfoot, tr
    node.innerHTML = html;
}

jst._node = function(id) { 
    if (typeof(id) != 'string') 
        return id;
    return document.getElementById(id); 
};

jst._each = function(obj, callback) // ----------------------------------
{
    if (obj.constructor == Array)
        for (var i = 0; i < obj.length; i++) {
            if (callback(obj[i], i) === false)
                return;
        }
    else if (obj.nodeName) {
        var child = obj.firstChild;
        while (child) {
            var c = child;
            child = child.nextSibling;
            if (callback(c) === false)
                return;
        } 
    }
    else for (var key in obj)
        if (callback(key, obj[key]) === false)
            return;
}

jst._clear = function(node) // -------------------------------------------------
{
    var node = jst._node(node);
    jst._each( node, function(child) { node.removeChild(child); });
}

jst._clone_opt = function(opt) // ---------------------------------------------
{
    var clone = {};
    for (var key in opt)
       clone[key] = opt[key];
    clone.renderers  = opt.renderers.copy();
    clone.attributes = opt.attributes.copy();
    clone.no_append  = false;
    return clone;
}

// lang =======================================================================

String.prototype.trim = function() // -----------------------------------------
{
    var l = 0, i = 0;
    var whitespace = " \t\n\r";

    var string = this;
   
    l = string.length;
    for (i = 0; i < l; i++) {
        if (whitespace.indexOf(string.charAt(i)) === -1) {
            string = string.substring(i);
            break;
        }
    }
    
    l = string.length;
    for (i = l - 1; i >= 0; i--) {
        if (whitespace.indexOf(string.charAt(i)) === -1) {
            string = string.substring(0, i + 1);
            break;
        }
    }
    if (whitespace.indexOf(string.charAt(0)) !== -1) return "";
    return string;
}

Array.prototype.copy = function() // ------------------------------------------
{
    return [].concat(this);
}