/*\
title: js/Renderer.js

Renderer objects encapsulate a tree of nodes that are capable of rendering and selectively updating an
HTML representation of themselves. The following node types are defined:
* ''MacroNode'' - represents an invocation of a macro
* ''ElementNode'' - represents a single HTML element
* ''TextNode'' - represents an HTML text node
* ''EntityNode'' - represents an HTML entity node
* ''RawNode'' - represents a chunk of unparsed HTML text

These node types are implemented with prototypal inheritance from a base Node class. One
unusual convenience in the implementation is that the node constructors can be called without an explicit
`new` keyword: the constructors check for `this` not being an instance of themselves, and recursively invoke
themselves with `new` when required.

Convenience functions are provided to wrap up the construction of some common interface elements:
* ''ErrorNode''
* ''LabelNode''
* ''SplitLabelNode''

\*/
(function(){

/*jshint node: true, browser: true */
"use strict";

var utils = require("./Utils.js"),
	ArgParser = require("./ArgParser.js").ArgParser,
	Dependencies = require("./Dependencies.js").Dependencies,
	esprima = require("esprima");

/*
Intialise the renderer object
	nodes: an array of Node objects
	dependencies: an optional Dependencies object
	store: a reference to the WikiStore object to use for rendering these nodes
*/
var Renderer = function(nodes,dependencies,store) {
	this.nodes = nodes;
	this.dependencies = dependencies;
	this.store = store;
};

/*
The base class for all types of renderer nodes
*/
var Node = function(children) {
	if(this instanceof Node) {
		this.children = children;
	} else {
		return new Node(children);
	}
};

Node.prototype.clone = function() {
	// By default we don't actually clone nodes, we just re-use them (we do clone macros and elements)
	return this;
};

Node.prototype.broadcastEvent = function(event) {
	return true;
};

Node.prototype.execute = 
Node.prototype.render =
Node.prototype.renderInDom =
Node.prototype.refresh =
Node.prototype.refreshInDom = function() {
	// All these methods are no-ops by default
};

/*
Construct a renderer node representing a macro invocation
	macroName: name of the macro
	srcParams: a hashmap of parameters (each can be a string, or a fn(tiddler,store,utils) for evaluated parameters)
	children: optional array of child nodes
	store: reference to the WikiStore associated with this macro
	dependencies: optional Dependencies object representing the dependencies of this macro

Note that the dependencies will be evaluated if not provided.
*/
var MacroNode = function(macroName,srcParams,children,store,dependencies) {
	if(this instanceof MacroNode) {
		// Save the details
		this.macroName = macroName;
		this.macro = store.macros[macroName];
		this.children = children;
		this.store = store;
		this.srcParams = typeof srcParams === "string" ? this.parseMacroParamString(srcParams) : srcParams;
		// Evaluate the dependencies if required
		this.dependencies = dependencies ? dependencies : this.evaluateDependencies();
	} else {
		return new MacroNode(macroName,srcParams,children,store,dependencies);
	}
};

MacroNode.prototype = new Node();
MacroNode.prototype.constructor = MacroNode;

/*
Evaluate the dependencies of this macro invocation. If the macro provides an `evaluateDependencies` method
then it is invoked to evaluate the dependencies. Otherwise it generates the dependencies based on the
macro parameters provided
*/
MacroNode.prototype.evaluateDependencies = function() {
	if(this.srcParams && this.macro) {
		if(this.macro.evaluateDependencies) {
			// Call the evaluateDependencies method if the macro provides it
			return this.macro.evaluateDependencies.call(this);
		} else {
			// Figure out the dependencies from the metadata and parameters
			var dependencies = new Dependencies();
			if(this.macro.dependentAll) {
				dependencies.dependentAll = true;
			}
			if(this.macro.dependentOnContextTiddler) {
				dependencies.dependentOnContextTiddler = true;
			}
			for(var m in this.macro.params) {
				var paramInfo = this.macro.params[m];
				if(m in this.srcParams && paramInfo.type === "tiddler") {
					if(typeof this.srcParams[m] === "function") {
						dependencies.dependentAll = true;
					} else {
						dependencies.addDependency(this.srcParams[m],!paramInfo.skinny);
					}
				}
			}
			return dependencies;
		}
	}
};

MacroNode.prototype.parseMacroParamString = function(paramString) {
	/*jslint evil: true */
	var params = {},
		args = new ArgParser(paramString,{defaultName: "anon", cascadeDefaults: this.macro.cascadeDefaults}),
		self = this,
		insertParam = function(name,arg) {
			if(arg.evaluated) {
				params[name] = eval(esprima.generate( // (function(tiddler,store,utils) {return {paramOne: 1};})
					{
						"type": "Program",
						"body": [
							{
								"type": "ExpressionStatement",
								"expression": {
									"type": "FunctionExpression",
									"id": null,
									"params": [
										{
											"type": "Identifier",
											"name": "tiddler"
										},
										{
											"type": "Identifier",
											"name": "store"
										},
										{
											"type": "Identifier",
											"name": "utils"
										}
									],
									"body": {
										"type": "BlockStatement",
										"body": [
											{
												"type": "ReturnStatement",
												"argument": esprima.parse("(" + arg.string + ")").body[0].expression
											}
										]
									}
								}
							}
						]
					}
				));
			} else {
				params[name] = arg.string;
			}
		};
	for(var m in this.macro.params) {
		var param = this.macro.params[m],
			arg;
		if("byPos" in param && args.byPos[param.byPos] && (args.byPos[param.byPos].n === "anon" || args.byPos[param.byPos].n === m)) {
			arg = args.byPos[param.byPos].v;
			insertParam(m,arg);
		} else {
			arg = args.getValueByName(m);
			if(!arg && param.byName === "default") {
				arg = args.getValueByName("anon");
			}
			if(arg) {
				insertParam(m,arg);
			}
		}
	}
	return params;
};

MacroNode.prototype.hasParameter = function(name) {
	return this.params.hasOwnProperty(name);
};

MacroNode.prototype.clone = function() {
	return new MacroNode(this.macroName,this.srcParams,this.cloneChildren(),this.store,this.dependencies);
};

MacroNode.prototype.cloneChildren = function() {
	var childClones;
	if(this.children) {
		childClones = [];
		for(var t=0; t<this.children.length; t++) {
			childClones.push(this.children[t].clone());
		}
	}
	return childClones;
};

MacroNode.prototype.execute = function(parents,tiddlerTitle) {
	// Evaluate macro parameters to get their values
	this.params = {};
	var tiddler = this.store.getTiddler(tiddlerTitle);
	if(!tiddler) {
		tiddler = {title: tiddlerTitle};
	}
	for(var p in this.srcParams) {
		if(typeof this.srcParams[p] === "function") {
			this.params[p] = this.srcParams[p](tiddler,this.store,utils);
		} else {
			this.params[p] = this.srcParams[p];
		}
	}
	// Save the context tiddler
	this.tiddlerTitle = tiddlerTitle;
	// Save a reference to the array of parents
	this.parents = parents || [];
	// Render the macro to get its content
	this.content = this.macro.execute.call(this);
};

MacroNode.prototype.render = function(type) {
	var output = [];
	for(var t=0; t<this.content.length; t++) {
		output.push(this.content[t].render(type));
	}
	return output.join("");
};

MacroNode.prototype.renderInDom = function(parentDomNode,insertBefore) {
	// Create the wrapper node for the macro
	var macroContainer = document.createElement(this.macro.wrapperTag || "span");
	this.domNode = macroContainer;
	if(insertBefore) {
		parentDomNode.insertBefore(macroContainer,insertBefore);	
	} else {
		parentDomNode.appendChild(macroContainer);
	}
	// Add some debugging information to it
	macroContainer.setAttribute("data-tw-macro",this.macroName);
	// Ask the macro to add event handlers to the node
	if(this.macro.addEventHandlers) {
		this.macro.addEventHandlers.call(this);
	} else {
		for(var e in this.macro.events) {
			// Register this macro node to handle the event via the handleEvent() method
			macroContainer.addEventListener(e,this,false);
		}
	}
	// Render the content of the macro
	for(var t=0; t<this.content.length; t++) {
		this.content[t].renderInDom(macroContainer);
	}
	// Call the macro renderInDom method if it has one
	if(this.macro.renderInDom) {
		this.macro.renderInDom.call(this);
	}
};

MacroNode.prototype.refresh = function(changes) {
	var t,
		self = this;
	// Check if any of the dependencies of this macro node have changed
	if(this.dependencies.hasChanged(changes,this.tiddlerTitle)) {
		// Re-execute the macro if so
		this.execute(this.parents,this.tiddlerTitle);
	} else {
		// Refresh any children
		for(t=0; t<this.content.length; t++) {
			this.content[t].refresh(changes);
		}
	}
};

MacroNode.prototype.refreshInDom = function(changes) {
	var t,
		self = this;
	// Ask the macro to rerender itself if it can
	if(this.macro.refreshInDom) {
		this.macro.refreshInDom.call(this,changes);
	} else {
		// Check if any of the dependencies of this macro node have changed
		if(this.dependencies.hasChanged(changes,this.tiddlerTitle)) {
			// Manually reexecute and rerender this macro
			while(this.domNode.hasChildNodes()) {
				this.domNode.removeChild(this.domNode.firstChild);
			}
			this.execute(this.parents,this.tiddlerTitle);
			for(t=0; t<this.content.length; t++) {
				this.content[t].renderInDom(this.domNode);
			}
		} else {
			// Refresh any children
			for(t=0; t<this.content.length; t++) {
				this.content[t].refreshInDom(changes);
			}
		}
	}
};

MacroNode.prototype.handleEvent = function(event) {
	return this.macro.events[event.type].call(this,event);
};

MacroNode.prototype.broadcastEvent = function(event) {
	if(this.macro.events && this.macro.events.hasOwnProperty(event.type)) {
		if(!this.handleEvent(event)) {
			return false;
		}
	}
	if(this.content) {
		for(var t=0; t<this.content.length; t++) {
			if(!this.content[t].broadcastEvent(event)) {
				return false;
			}
		}
	}
	return true;
};

var ElementNode = function(type,attributes,children) {
	if(this instanceof ElementNode) {
		this.type = type;
		this.attributes = attributes;
		this.children = children;
	} else {
		return new ElementNode(type,attributes,children);
	}
};

ElementNode.prototype = new Node();
ElementNode.prototype.constructor = ElementNode;

ElementNode.prototype.clone = function() {
	var childClones;
	if(this.children) {
		childClones = [];
		for(var t=0; t<this.children.length; t++) {
			childClones.push(this.children[t].clone());
		}
	}
	return new ElementNode(this.type,this.attributes,childClones);
};

ElementNode.prototype.execute = function(parents,tiddlerTitle) {
	if(this.children) {
		for(var t=0; t<this.children.length; t++) {
			this.children[t].execute(parents,tiddlerTitle);
		}
	}
};

ElementNode.prototype.render = function(type) {
	var isHtml = type === "text/html",
		output = [];
	if(isHtml) {
		output.push("<",this.type);
		if(this.attributes) {
			for(var a in this.attributes) {
				var v = this.attributes[a];
				if(v !== undefined) {
					if(v instanceof Array) {
						v = v.join(" ");
					} else if(typeof v === "object") {
						var s = [];
						for(var p in v) {
							s.push(p + ":" + v[p] + ";");
						}
						v = s.join("");
					}
					output.push(" ",a,"='",utils.htmlEncode(v),"'");
				}
			}
		}
		output.push(">");
	}
	if(this.children) {
		for(var t=0; t<this.children.length; t++) {
			output.push(this.children[t].render(type));
		}
		if(isHtml) {
			output.push("</",this.type,">");
		}
	}
	return output.join("");
};

ElementNode.prototype.renderInDom = function(parentDomNode,insertBefore) {
	var element = document.createElement(this.type);
	if(this.attributes) {
		for(var a in this.attributes) {
			var v = this.attributes[a];
			if(v !== undefined) {
				if(v instanceof Array) { // Ahem, could there be arrays other than className?
					element.className = v.join(" "); 
				} else if (typeof v === "object") { // ...or objects other than style?
					for(var p in v) {
						element.style[p] = v[p];
					}
				} else {
					element.setAttribute(a,v);
				}
			}
		}
	}
	if(insertBefore) {
		parentDomNode.insertBefore(element,insertBefore);
	} else {
		parentDomNode.appendChild(element);
	}
	this.domNode = element;
	if(this.children) {
		for(var t=0; t<this.children.length; t++) {
			this.children[t].renderInDom(element);
		}
	}
};

ElementNode.prototype.refresh = function(changes) {
	if(this.children) {
		for(var t=0; t<this.children.length; t++) {
			this.children[t].refresh(changes);
		}
	}
};

ElementNode.prototype.refreshInDom = function(changes) {
	if(this.children) {
		for(var t=0; t<this.children.length; t++) {
			this.children[t].refreshInDom(changes);
		}
	}
};

ElementNode.prototype.broadcastEvent = function(event) {
	if(this.children) {
		for(var t=0; t<this.children.length; t++) {
			if(!this.children[t].broadcastEvent(event)) {
				return false;
			}
		}
	}
	return true;
};

var TextNode = function(text) {
	if(this instanceof TextNode) {
		this.text = text;
	} else {
		return new TextNode(text);
	}
};

TextNode.prototype = new Node();
TextNode.prototype.constructor = TextNode;

TextNode.prototype.render = function(type) {
	return type === "text/html" ? utils.htmlEncode(this.text) : this.text;
};

TextNode.prototype.renderInDom = function(domNode) {
	this.domNode = document.createTextNode(this.text);
	domNode.appendChild(this.domNode);
};

var EntityNode = function(entity) {
	if(this instanceof EntityNode) {
		this.entity = entity;
	} else {
		return new EntityNode(entity);	
	}
};

EntityNode.prototype = new Node();
EntityNode.prototype.constructor = EntityNode;

EntityNode.prototype.render = function(type) {
	return type === "text/html" ? this.entity : utils.entityDecode(this.entity);
};

EntityNode.prototype.renderInDom = function(domNode) {
	this.domNode = document.createTextNode(utils.entityDecode(this.entity));
	domNode.appendChild(this.domNode);
};

var RawNode = function(html) {
	if(this instanceof RawNode) {
		this.html = html;
	} else {
		return new RawNode(html);	
	}
};

RawNode.prototype = new Node();
RawNode.prototype.constructor = RawNode;

RawNode.prototype.render = function(type) {
	return this.html;
};

RawNode.prototype.renderInDom = function(domNode) {
	this.domNode = document.createElement("div");
	this.domNode.innerHTML = this.html;
	domNode.appendChild(this.domNode);	
};

/*
Static method to construct an error message
*/
var ErrorNode = function(text) {
	return new ElementNode("span",{
		"class": ["label","label-important"]
	},[
		new TextNode(text)
	]);
};

/*
Static method to construct a label
*/
var LabelNode = function(type,value,classes) {
	classes = (classes || []).slice(0);
	classes.push("label");
	return new ElementNode("span",{
		"class": classes,
		"data-tw-label-type": type
	},value);
};

/*
Static method to construct a split label
*/
var SplitLabelNode = function(type,left,right,classes) {
	classes = (classes || []).slice(0);
	classes.push("splitLabel");
	return new ElementNode("span",{
		"class": classes
	},[
		new ElementNode("span",{
			"class": ["splitLabelLeft"],
			"data-tw-label-type": type
		},left),
		new ElementNode("span",{
			"class": ["splitLabelRight"]
		},right)
	]);
};

Renderer.MacroNode = MacroNode;
Renderer.ElementNode = ElementNode;
Renderer.TextNode = TextNode;
Renderer.EntityNode = EntityNode;
Renderer.RawNode = RawNode;
Renderer.ErrorNode = ErrorNode;
Renderer.LabelNode = LabelNode;
Renderer.SplitLabelNode = SplitLabelNode;

exports.Renderer = Renderer;

})();