/*
 * Patroon - Javascript Template Engine
 *
 * Copyright (c) 2008 Matthias Georgi (matthias-georgi.de)
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */
 
 /*
  * Mone (simonefabiano.com) slightly modified this library:
  * - add namespace Patroon to avoid the use of the window.Template identifier
  * - avoid the use of css classes to expand data. Use the same syntax but 
  * 	applied to the patroon attribute (eg instead of class="comment"
  *	use patroon="comment").
  * - add exception catching to avoid errors on non-existing properties
  * 	in the data object
  * - avoid the use of innerHTML, append a text node instead. [values
  *	can't contain html snippets now] this way data fetched from a third
  *	party site can be rendered with no XSS risks.
  * - as a consequence of the previous change, helpers now must return text
  *	or a DOM node. Html snippets aren't supported.
  * - modify the linkTo helper to return a DOM node instead of an html snippet.
  * - add a html helper to render html snippets.
  * - use chartAt instead of using a string as an array.
  * - remove jQuery binding.
  * - do not add a span node to substitute a text node
  * - add the display_if attribute. It is an attribute that is evaluated as other
  *	attributes. If it's content results in a null/undefined/0/empty string the
  * node that has such attribute is not displayed. TODO need to search for a better
  * synthax...
  *
  * Mone didn't studied nor tested completely the Template class (btw he used it): 
  * if you find some bugs related to his changes don't hesitate to write him an email. 
  * You can find his email address on his website (simonefabiano.com).
  * Mone is writing in third person because of his addiction to Twitter and similar services.
  */
  
if (window.Patroon) {
	throw("Patroon name conflict??");
}

Patroon = {};

Patroon.Template = function(id) {
    this.element = document.getElementById(id);
    this.element.id = null;
    this.eval = {};

    if (this.element) {
        this.element.parentNode.removeChild(this.element);
        this.compileNode(this.element);
    } else {
        throw "template not found: " + id;
    }
};


Patroon.Helper = {

    linkTo: function(text, url) {
        if (url.indexOf('http://') == -1 && url[0] != '/' && url[0] != '#') {
            url = 'http://' + url;
        }
        var aNode = document.createElement("A");
        aNode.href = url;
		aNode.appendChild(document.createTextNode(text));
		return aNode;
    },

    html: function(text,isDiv) {
        var tag = isDiv ? "div" : "span";
        var el = document.createElement(tag);
		el.innerHTML = text;
        return el;
    },
    
    image: function(src) {
    	var img = document.createElement("img");
    	img.src = src;
    	return img;
    }

};

Patroon.Template.prototype = {

    expand: function(data) {
        var node = this.element.cloneNode(true);
        this.expandData(data, node);
        return node;
    },

    expandData: function(data, node) {
        if (data.constructor == Array) {
            this.expandArray(data, node);
        }
        else {
            this.expandObject(data, node);
        }
    },

    expandArray: function(data, node) {
        var parent = node.parentNode;
        var sibling = node.nextSibling;
        parent.removeChild(node);
        for (var i = 0; i < data.length; i++) {
            var child = node.cloneNode(true);
            parent.insertBefore(child, sibling);
            this.expandData(data[i], child);
        }
    },

    expandByContext: function(object, node) {
        var found = false;
        var patroonAttr = node.getAttribute("patroon");
        if (patroonAttr) {
	        var names = patroonAttr.split(" ");
	
	        for (var i = 0; i < names.length; i++) {
	            var name = names[i];
	            if (object[name]) {
	                this.expandData(object[name], node);
	                found = true;
	            }
	        }
		}

        if (!found) {
            this.expandObject(object, node);
        }
    },

    expandObject: function(object, node) {
    	var displayIf = node.getAttribute("display_if");
    	if (displayIf && this.eval[displayIf]) {
    		var value = this.eval[displayIf](object);
    		if (!value || value == "") {
    			if (node.parentNode) {
    				node.parentNode.removeChild(node);
    			} else {
    				node.style.display = "none";
    			}
    			return;
    		}
    	}
    
        var i;
        var nodes = [];

        for (i = 0; i < node.childNodes.length; i++) {
            nodes.push(node.childNodes[i]);
        }

        for (i = 0; i < node.attributes.length; i++) {
            var attr = node.attributes[i];
            if (this.eval[attr.value]) {
         		var expandedValues = this.eval[attr.value](object);
            	
            	var value = "";
            	for (var j = 0; j < expandedValues.length; j++) {
            		if (!expandedValues[j].nodeType) {
                		value += expandedValues[j];
                	} //else I hope that no one will use a node type helper inside an attribute...
            	}
            	
            	if(attr.name == "id") {
            		//some IEs does not permit to edit the id of a node through the attributes array
            		node.id = value;
            	} else {
            		attr.value = value;
            	}
                
            }
        }
	
		for (i = 0; i < nodes.length; i++) {
        	var child = nodes[i];
        	if (child.nodeType == 1) {
            	this.expandByContext(object, child);
        	}
          
        	if (child.nodeType == 3 && this.eval[child.nodeValue])  {
                var expandedValues = this.eval[child.nodeValue](object);
               
                for (var j = 0; j < expandedValues.length; j++) {
	               	if (expandedValues[j].nodeType) {
	               		//this value was generated by a helper and is (probably...) a DOM node
	               		child.parentNode.insertBefore(expandedValues[j],child);
	               	} else {
	               		//simple text
	               		child.parentNode.insertBefore(document.createTextNode(expandedValues[j]),child);
	               	}
				}
	     
	                
	    		child.parentNode.removeChild(child);
                
			}
        }
    },


    compile: function(str) {
        var len = str.length;
        var expr = false;
        var cur = '';
        var out = [];
        var braces = 0;


        for (var i = 0; i < len; i++) {
            var c = str.charAt(i);

            if (expr) {
                if (c == '{') {
                    braces += 1;
                }
                if (c == '}') {
                    braces -= 1;
                    if (braces == 0) {
                        expr = false;
                        if (cur.length > 0) {
                            out.push("(" + cur + ")");
                        }
                        cur = "";
                    }
                } else {
                    cur += c;
                }
            } else {
                switch (c) {
                case "'":
                    cur += "\\'";
                    break;
                case "\\":
                    cur += "\\\\";
                    break;
                case '{':
                    expr = true;
                    braces += 1;
                    if (cur.length > 0) {
                        out.push("'" + cur + "'");
                    }
                    cur = "";
                    break;
                case "\n":
                    break;
                default:
                    cur += c;
                }
            }
        }

        if (cur.length > 0) {
            out.push("'" + cur + "'");
        }
	
        this.eval[str] = this.getDataClosure(out);
    },
    
    getDataClosure: function(out) {
    	return function() {
      		var data = arguments[0];
      		with(Patroon.Helper) {
      			with (data) {
      				var result = [];
      				for (var i = 0; i < out.length; i++) {
      					try {
      						result.push(eval(out[i]));
      					} catch(e) {
      						//out[i] has no sense
      					}
      				}
      				return result;	
      			}
      		}
      	}
    
    },
    
    compileNode: function(node) {
        var i;

        if (node.nodeType == 1) {
            for (i = 0; i < node.attributes.length; i++) {
                var value = node.attributes[i].value;
                if (value.indexOf('{') > -1) {
                    this.compile(value);
                }
            }
            for (i = 0; i < node.childNodes.length; i++) {
                this.compileNode(node.childNodes[i]);
            }
        }

        if (node.nodeType == 3 && node.nodeValue.indexOf('{') > -1)  {
            this.compile(node.nodeValue);
        }

    }

};