dojo.provide("dojox.dtl.html");

dojo.require("dojox.dtl._base");

dojox.dtl.ObjectMap = function(){
	this.contents = [];
}
dojo.extend(dojox.dtl.ObjectMap, {
	get: function(key){
		var contents = this.contents;
		for(var i = 0, content; content = contents[i]; i++){
			if(content[0] === key){
				return content[1];
			}
		}
	},
	put: function(key, value){
		var contents = this.contents;
		for(var i = 0, content; content = contents[i]; i++){
			if(content[0] === key){
				if(arguments.length == 1){
					contents.splice(i, 1);
					return;
				}
				content[1] = value;
				return;
			}
		}
		contents.push([key, value]);
	},
	toString: function(){ return "dojox.dtl.ObjectMap"; }
});

dojox.dtl.html = {
	types: dojo.mixin({change: -11, attr: -12, elem: 1, text: 3}, dojox.dtl.text.types),
	_attributes: {},
	_re: /(^\s+|\s+$)/g,
	_re2: /\b([a-zA-Z]+)="/g,
	_re3: /<!--({({|%).*?(%|})})-->/g,
	_re4: /^function anonymous\(\)\s*{\s*(.*)\s*}$/,
	_trim: function(/*String*/ str){
		return str.replace(this._re, "");
	},
	getTemplate: function(text){
		if(typeof this._commentable == "undefined"){
			// Check to see if the browser can handle comments
			this._commentable = false;
			var div = document.createElement("div");
			div.innerHTML = "<!--Test comment handling, and long comments, using comments whenever possible.-->";
			if(div.childNodes.length && div.childNodes[0].nodeType == 8 && div.childNodes[0].data == "comment"){
				this._commentable = true;
			}
		}

		if(!this._commentable){
			// Strip comments
			text = text.replace(this._re3, "$1");
		}

		var match;
		while(match = this._re2.exec(text)){
			this._attributes[match[1]] = true;
		}
		var div = document.createElement("div");
		div.innerHTML = text;
		var output = { pres: [], posts: []}
		while(div.childNodes.length){
			if(!output.node && div.childNodes[0].nodeType == 1){
				output.node = div.removeChild(div.childNodes[0]);
			}else if(!output.node){
				output.pres.push(div.removeChild(div.childNodes[0]));
			}else{
				output.posts.push(div.removeChild(div.childNodes[0]));
			}
		}

		if(!output.node){
			throw new Error("Template did not provide any content");
		}

		return output;
	},
	tokenize: function(/*Node*/ node, /*Array?*/ tokens, /*Array?*/ preNodes, /*Array?*/ postNodes){
		tokens = tokens || [];
		var first = !tokens.length;
		var types = this.types;

		var children = [];
		for(var i = 0, child; child = node.childNodes[i]; i++){
			children.push(child);
		}

		if(preNodes){
			for(var i = 0, child; child = preNodes[i]; i++){
				this._tokenize(node, child, tokens);
			}
		}

		tokens.push([types.elem, node]);
		tokens.push([types.change, node]);

		for(var key in this._attributes){
			var value = "";
			if(key == "class"){
				value = node.className || value;
			}else if(key == "for"){
				value = node.htmlFor || value;
			}else if(node.getAttribute){
				value = node.getAttribute(key, 2) || value;
				if(key == "href" || key == "src"){
					if(dojo.isIE){
						var hash = location.href.lastIndexOf(location.hash);
						var href = location.href.substring(0, hash).split("/");
						href.pop();
						href = href.join("/") + "/";
						if(value.indexOf(href) == 0){
							value = value.replace(href, "");
						}
						value = value.replace(/%20/g, " ").replace(/%7B/g, "{").replace(/%7D/g, "}").replace(/%25/g, "%");
					}
					if(value.indexOf("{%") != -1 || value.indexOf("{{") != -1){
						node.setAttribute(key, "");
					}
				}
			}
			if(typeof value == "function"){
				value = value.toString().replace(this._re4, "$1");
			}
			if(typeof value == "string" && (value.indexOf("{%") != -1 || value.indexOf("{{") != -1 || (value && dojox.dtl.text.getTag("attr:" + key, true)))){
				tokens.push([types.attr, node, key, value]);
			}
		}

		if(!children.length){
			tokens.push([types.change, node.parentNode, true]);
			if(postNodes){
				for(var i = 0, child; child = postNodes[i]; i++){
					this._tokenize(node, child, tokens);
				}
			}
			return tokens;
		}

		for(var i = 0, child; child = children[i]; i++){
			this._tokenize(node, child, tokens);
		}

		if(node.parentNode && node.parentNode.tagName){
			tokens.push([types.change, node.parentNode, true]);
			node.parentNode.removeChild(node);
		}
		
		if(postNodes){
			for(var i = 0, child; child = postNodes[i]; i++){
				this._tokenize(node, child, tokens);
			}
		}

		if(first){
			tokens.push([types.change, node, true]);
		}

		return tokens;
	},
	_tokenize: function(parent, child, tokens){
		var types = this.types;
		var data = child.data;
		switch(child.nodeType){
			case 1:
				this.tokenize(child, tokens);
				break;
			case 3:
				if(data.match(/[^\s\n]/)){
					if(data.indexOf("{{") != -1 || data.indexOf("{%") != -1){
						var texts = dojox.dtl.text.tokenize(data);
						for(var j = 0, text; text = texts[j]; j++){
							if(typeof text == "string"){
								tokens.push([types.text, text]);
							}else{
								tokens.push(text);
							}
						}
					}else{
						tokens.push([child.nodeType, child]);
					}
				}
				if(child.parentNode) child.parentNode.removeChild(child);
				break;
			case 8:
				if(data.indexOf("{%") == 0){
					tokens.push([types.tag, this._trim(data.substring(2, data.length - 3))]);
				}
				if(data.indexOf("{{") == 0){
					tokens.push([types.varr, this._trim(data.substring(2, data.length - 3))]);
				}
				if(child.parentNode) child.parentNode.removeChild(child);
				break;
		}
	}
}

dojox.dtl.HtmlTemplate = function(/*String|dojo._Url*/ obj){
	// summary: Use this object for HTML templating
	var dd = dojox.dtl;
	var ddh = dd.html;

	if(!obj.node){
		if(typeof obj == "object"){
			obj = dojox.dtl.text.getTemplateString(obj);
		}
		obj = ddh.getTemplate(obj);
	}

	var tokens = ddh.tokenize(obj.node, [], obj.pres, obj.posts);
	var parser = new dd.HtmlParser(tokens);
	this.nodelist = parser.parse();
}
dojo.extend(dojox.dtl.HtmlTemplate, {
	_count: 0,
	_re: /\bdojo:([a-zA-Z0-9_]+)\b/g,
	setClass: function(str){
		this.getRootNode().className = str;
	},
	getRootNode: function(){
		return this.rootNode;
	},
	getBuffer: function(){
		return new dojox.dtl.HtmlBuffer();
	},
	render: function(context, buffer){
		buffer = buffer || this.getBuffer();
		this.rootNode = null;
		var onSetParent = dojo.connect(buffer, "onSetParent", this, function(node){
			if(!this.rootNode){
				this.rootNode = node || true;
			}
		});
		var output = this.nodelist.render(context || new dojox.dtl.Context({}), buffer);
		dojo.disconnect(onSetParent);
		buffer._flushCache();
		return output;
	},
	unrender: function(context, buffer){
		return this.nodelist.unrender(context, buffer);
	},
	toString: function(){ return "dojox.dtl.HtmlTemplate"; }
});

dojox.dtl.HtmlBuffer = function(/*Node*/ parent){
	// summary: Allows the manipulation of DOM
	// description:
	//		Use this to append a child, change the parent, or
	//		change the attribute of the current node.
	this._parent = parent;
	this._cache = [];
}
dojo.extend(dojox.dtl.HtmlBuffer, {
	concat: function(/*DOMNode*/ node){
		if(!this._parent) return this;
		if(node.nodeType){
			var caches = this._getCache(this._parent);
			if(node.parentNode === this._parent){
				// If we reach a node that already existed, fill in the cache for this same parent
				var i = 0;
				for(var i = 0, cache; cache = caches[i]; i++){
					this.onAddNode(node);
					this._parent.insertBefore(cache, node);
				}
				caches.length = 0;
			}
			if(!node.parentNode || !node.parentNode.tagName){
				if(!this._parent.childNodes.length){
					this.onAddNode(node);
					this._parent.appendChild(node);
				}else{
					caches.push(node);
				}
			}
		}
		return this;
	},
	remove: function(obj){
		if(typeof obj == "string"){
			this._parent.removeAttribute(obj);
		}else{
			if(obj.parentNode === this._parent){
				this.onRemoveNode();
				this._parent.removeChild(obj);
			}
		}
		return this;
	},
	setAttribute: function(key, value){
		if(key == "class"){
			this._parent.className = value;
		}else if(key == "for"){
			this._parent.htmlFor = value;
		}else if(this._parent.setAttribute){
			this._parent.setAttribute(key, value);
		}
		return this;
	},
	setParent: function(node, /*Boolean?*/ up){
		if(!this._parent) this._parent = node;
		var caches = this._getCache(this._parent);
		if(caches && caches.length && up){
			for(var i = 0, cache; cache = caches[i]; i++){
				if(cache !== this._parent && (!cache.parentNode || !cache.parentNode.tagName)){
					this.onAddNode(cache);
					this._parent.appendChild(cache);
				}
			}
			caches.length = 0;
		}

		this.onSetParent(node, up);
		this._parent = node;
		return this;
	},
	getParent: function(){
		return this._parent;
	},
	onSetParent: function(){
		// summary: Stub called when setParent is used.
	},
	onAddNode: function(){
		// summary: Stub called when new nodes are added
	},
	onRemoveNode: function(){
		// summary: Stub called when nodes are removed
	},
	_getCache: function(node){
		for(var i = 0, cache; cache = this._cache[i]; i++){
			if(cache[0] === node){
				return cache[1];
			}
		}
		var arr = [];
		this._cache.push([node, arr]);
		return arr;
	},
	_flushCache: function(node){
		for(var i = 0, cache; cache = this._cache[i]; i++){
			if(!cache[1].length){
				this._cache.splice(i--, 1);
			}
		}
	},
	toString: function(){ return "dojox.dtl.HtmlBuffer"; }
});

dojox.dtl.HtmlNode = function(node){
	// summary: Places a node into DOM
	this.contents = node;
}
dojo.extend(dojox.dtl.HtmlNode, {
	render: function(context, buffer){
		return buffer.concat(this.contents);
	},
	unrender: function(context, buffer){
		return buffer.remove(this.contents);
	},
	clone: function(buffer){
		return new dojox.dtl.HtmlNode(this.contents);
	},
	toString: function(){ return "dojox.dtl.HtmlNode"; }
});

dojox.dtl.HtmlNodeList = function(/*Node[]*/ nodes){
	// summary: A list of any HTML-specific node object
	// description:
	//		Any object that's used in the constructor or added
	//		through the push function much implement the
	//		render, unrender, and clone functions.
	this.contents = nodes || [];
}
dojo.extend(dojox.dtl.HtmlNodeList, {
	parents: new dojox.dtl.ObjectMap(),
	push: function(node){
		this.contents.push(node);
	},
	unshift: function(node){
		this.contents.unshift(node);
	},
	render: function(context, buffer, /*Node*/ instance){
		if(instance){
			var parent = buffer.getParent();
		}
		for(var i = 0; i < this.contents.length; i++){
			buffer = this.contents[i].render(context, buffer);
			if(!buffer) throw new Error("Template node render functions must return their buffer");
		}
		if(parent){
			buffer.setParent(parent, true);
		}
		return buffer;
	},
	unrender: function(context, buffer){
		for(var i = 0; i < this.contents.length; i++){
			buffer = this.contents[i].unrender(context, buffer);
			if(!buffer) throw new Error("Template node render functions must return their buffer");
		}
		return buffer;
	},
	clone: function(buffer){
		// summary:
		//		Used to create an identical copy of a NodeList, useful for things like the for tag.
		var dd = dojox.dtl;
		var ddh = dd.html;
		var parent = buffer.getParent();
		var contents = this.contents;
		var nodelist = new dd.HtmlNodeList();
		var cloned = [];
		for(var i = 0; i < contents.length; i++){
			var clone = contents[i].clone(buffer);
			if(clone instanceof dd.ChangeNode || clone instanceof dd.HtmlNode){
				var item = this.parents.get(clone.contents);
				if(item){
					clone.contents = item;
				}else if(parent !== clone.contents && clone instanceof dd.HtmlNode){
					var node = clone.contents;
					clone.contents = clone.contents.cloneNode(false);
					cloned.push(node);
					this.parents.put(node, clone.contents);
				}
			}
			nodelist.push(clone);
		}

		for(var i = 0, clone; clone = cloned[i]; i++){
			this.parents.put(clone);
		}

		return nodelist;
	},
	toString: function(){ return "dojox.dtl.HtmlNodeList"; }
});

dojox.dtl.HtmlVarNode = function(str){
	// summary: A node to be processed as a variable
	// description:
	//		Will render an object that supports the render function
	// 		and the getRootNode function
	this.contents = new dojox.dtl.Filter(str);
	this._lists = {};
}
dojo.extend(dojox.dtl.HtmlVarNode, {
	render: function(context, buffer){
		this._rendered = true;
		var dd = dojox.dtl;
		var ddh = dd.html;
		var str = this.contents.resolve(context);
		if(str && str.render && str.getRootNode){
			var root = this._curr = str.getRootNode();
			var lists = this._lists;
			var list = lists[root];
			if(!list){
				list = lists[root] = new dd.HtmlNodeList();
				list.push(new dd.ChangeNode(buffer.getParent()));
				list.push(new dd.HtmlNode(root));
				list.push(str);
				list.push(new dd.ChangeNode(buffer.getParent(), true));
			}
			return list.render(context, buffer);
		}else{
			if(!this._txt) this._txt = document.createTextNode(str);
			if(this._txt.data != str) this._txt.data = str;
			return buffer.concat(this._txt);
		}
		return buffer;
	},
	unrender: function(context, buffer){
		if(this._rendered){
			this._rendered = false;
			if(this._curr){
				return this._lists[this._curr].unrender(context, buffer);
			}else if(this._txt){
				return buffer.remove(this._txt);
			}
		}
		return buffer;
	},
	clone: function(){
		return new dojox.dtl.HtmlVarNode(this.contents.contents);
	},
	toString: function(){ return "dojox.dtl.HtmlVarNode"; }
});

dojox.dtl.ChangeNode = function(node, /*Boolean?*/ up){
	// summary: Changes the parent during render/unrender
	this.contents = node;
	this._up = up;
}
dojo.extend(dojox.dtl.ChangeNode, {
	render: function(context, buffer){
		return buffer.setParent(this.contents, this._up);
	},
	unrender: function(context, buffer){
		return buffer.setParent(this.contents);
	},
	clone: function(buffer){
		return new dojox.dtl.ChangeNode(this.contents, this._up);
	},
	toString: function(){ return "dojox.dtl.ChangeNode"; }
});

dojox.dtl.AttributeNode = function(key, value){
	// summary: Works on attributes
	this._key = key;
	this._value = value;
	this._tpl = new dojox.dtl.Template(value);
	this.contents = "";
}
dojo.extend(dojox.dtl.AttributeNode, {
	render: function(context, buffer){
		var key = this._key;
		var value = this._tpl.render(context);
		if(this._rendered){
			if(value != this.contents){
				this.contents = value;
				return buffer.setAttribute(key, value);
			}
		}else{
			this._rendered = true;
			this.contents = value;
			return buffer.setAttribute(key, value);
		}
		return buffer;
	},
	unrender: function(context, buffer){
		if(this._rendered){
			this._rendered = false;
			this.contents = "";
			return buffer.remove(this.contents);
		}
		return buffer;
	},
	clone: function(){
		return new dojox.dtl.AttributeNode(this._key, this._value);
	},
	toString: function(){ return "dojox.dtl.AttributeNode"; }
});

dojox.dtl.HtmlTextNode = function(str){
	// summary: Adds a straight text node without any processing
	this.contents = document.createTextNode(str);
}
dojo.extend(dojox.dtl.HtmlTextNode, {
	render: function(context, buffer){
		return buffer.concat(this.contents);
	},
	unrender: function(context, buffer){
		return buffer.remove(this.contents);
	},
	clone: function(){
		return new dojox.dtl.HtmlTextNode(this.contents.data);
	},
	toString: function(){ return "dojox.dtl.HtmlTextNode"; }
});

dojox.dtl.HtmlParser = function(tokens){
	// summary: Turn a simple array into a set of objects
	// description:
	//	This is also used by all tags to move through
	//	the list of nodes.
	this.contents = tokens;
}
dojo.extend(dojox.dtl.HtmlParser, {
	parse: function(/*Array?*/ stop_at){
		var dd = dojox.dtl;
		var ddh = dd.html;
		var types = ddh.types;
		var terminators = {};
		var tokens = this.contents;
		if(!stop_at){
			stop_at = [];
		}
		for(var i = 0; i < stop_at.length; i++){
			terminators[stop_at[i]] = true;
		}
		var nodelist = new dd.HtmlNodeList();
		while(tokens.length){
			var token = tokens.shift();
			var type = token[0];
			var value = token[1];
			if(type == types.change){
				nodelist.push(new dd.ChangeNode(value, token[2]));
			}else if(type == types.attr){
				var fn = dojox.dtl.text.getTag("attr:" + token[2], true);
				if(fn){
					nodelist.push(fn(null, token[2] + " " + token[3]));
				}else{
					nodelist.push(new dd.AttributeNode(token[2], token[3]));
				}
			}else if(type == types.elem){
				var fn = dojox.dtl.text.getTag("node:" + value.tagName.toLowerCase(), true);
				if(fn){
					// TODO: We need to move this to tokenization so that it's before the
					// 				node and the parser can be passed here instead of null
					nodelist.push(fn(null, value, value.tagName.toLowerCase()));
				}
				nodelist.push(new dd.HtmlNode(value));
			}else if(type == types.varr){
				nodelist.push(new dd.HtmlVarNode(value));
			}else if(type == types.text){
				nodelist.push(new dd.HtmlTextNode(value.data || value));
			}else if(type == types.tag){
				if(terminators[value]){
					tokens.unshift(token);
					return nodelist;
				}
				var cmd = value.split(/\s+/g);
				if(cmd.length){
					cmd = cmd[0];
					var fn = dojox.dtl.text.getTag(cmd);
					if(typeof fn != "function"){
						throw new Error("Function not found for ", cmd);
					}
					var tpl = fn(this, value);
					if(tpl){
						nodelist.push(tpl);
					}
				}
			}
		}

		if(stop_at.length){
			throw new Error("Could not find closing tag(s): " + stop_at.toString());
		}

		return nodelist;
	},
	next: function(){
		// summary: Used by tags to discover what token was found
		var token = this.contents.shift();
		return {type: token[0], text: token[1]};
	},
	skipPast: function(endtag){
		return dojox.dtl.Parser.prototype.skipPast.call(this, endtag);
	},
	getVarNode: function(){
		return dojox.dtl.HtmlVarNode;
	},
	getTextNode: function(){
		return dojox.dtl.HtmlTextNode;
	},
	getTemplate: function(/*String*/ loc){
		return new dojox.dtl.HtmlTemplate(dojox.dtl.html.getTemplate(loc));
	},
	toString: function(){ return "dojox.dtl.HtmlParser"; }
});

dojox.dtl.register.tag("dojox.dtl.tag.event", "dojox.dtl.tag.event", [[/(attr:)?on(click|key(up))/i, "on"]]);
dojox.dtl.register.tag("dojox.dtl.tag.html", "dojox.dtl.tag.html", ["html", "attr:attach", "attr:tstyle"]);