
var Object = require ('./object');

/**
 * Creates a object from a XML specification.
 **/
module.exports = new Class
({
	 Extends: Object
	,_addedMap: {}
	,_contexts: null
	
	,add: function (id, object)
	{
		this._addedMap[id] = object;
	}
	
	,setParent: function (parentResult)
	{
		this._parentResult = parentResult;
		
		if (parentResult && !this.signalData)
			this.signalData = parentResult.builder.signalData;
	}
	
	,getMain: function (result)
	{
		return result.objects[this._mainContext];
	}
	
	,getById: function (result, objectId)
	{
		var index = this._contextMap[objectId];
		
		if (index !== undefined)
			return result.objects[index];

		var object = this._addedMap[objectId];

		if (object !== undefined)
			return object;

		if (this._parentResult)
			return this._parentResult.getById (objectId);

		return null;
	}
	
	,getByTagName: function (result, tagName)
	{	
		var tags = this._tags[tagName];
	
		if (tags)
		{
			var arr = new Array (tags.length);
			
			for (var i = 0; i < tags.length; i++)
				arr[i] = result.objects[tags[i]];

			return arr;
		}
		
		return [];
	}

	/**
	 * Compiles an XML file.
	 *
	 * @path String The XML path
	 * @dstDocument Document The document used to create the nodes
	 * @return %true on success, %false othersise
	 **/
	,loadXml: function (path, dstDocument)
	{
		this._path = path;
		return this.loadFromXmlDoc (Vn.getXml (path), dstDocument);
	}
	
	,loadFromString: function (xmlString, dstDocument)
	{
		var parser = new DOMParser ();
		var xmlDoc = parser.parseFromString (xmlString, 'text/xml');
		return this.loadFromXmlDoc (xmlDoc, dstDocument);
	}
	
	,loadFromXmlDoc: function (xmlDoc, dstDocument)
	{	
		if (!xmlDoc)
			return false;

		this._compileInit (dstDocument);

		var docElement = xmlDoc.documentElement;
		
		if (docElement.tagName !== 'vn')
		{
			this._showError ('Malformed XML');
			this._contexts = null;
			return false;
		}

		var childs = docElement.childNodes;

		if (childs)
		for (var i = 0; i < childs.length; i++)
			this._compileNode (childs[i]);

		this._compileEnd ();
		return true;
	}
	
	/**
	 * Compiles a single DOM node.
	 *
	 * @path Node The DOM node
	 * @dstDocument Document The document used to create the nodes
	 * @return %true on success, %false othersise
	 **/
	,loadXmlFromNode: function (node, dstDocument)
	{
		this._compileInit (dstDocument);
		this._mainContext = this._compileNode (node).id;
		this._compileEnd ();
		return true;
	}
	
	,load: function ()
	{
		if (this._contexts === null)
			return null;
	
		var contexts = this._contexts;
		var len = contexts.length;
		var objects = new Array (len);
	
		for (var i = 0; i < len; i++)
		{
			var context = contexts[i];
			
			if (context.tagName)
				objects[i] = this.elementInstantiate (context);
			else if (context.klass)
				objects[i] = this.objectInstantiate (context);
			else
				objects[i] = this.textInstantiate (context);
		}
		
		return new BuilderResult (this, objects);
	}
	
	,link: function (result)
	{
		var objects = result.objects;
	
		for (var i = this._links.length - 1; i >= 0; i--)
		{
			var l = this._links[i];
			var addedObject = this._addedMap[l.objectId];

			if (addedObject)
			{
				if (l.prop)
					objects[l.context.id][l.prop] = addedObject;
				else
					objects[l.context.id].appendChild (addedObject);
			}
			else
				this._showError ('Referenced unexistent object with id \'%s\'',
					l.objectId);
		}

		var contexts = this._contexts;
		for (var i = 0; i < contexts.length; i++)
		{
			var context = contexts[i];
			var object = objects[i];

			if (context.tagName)
				this.elementLink (context, object, objects, result);
			else if (context.klass)
				this.objectLink (context, object, objects, result);
		}
	}
	
	,_compileInit: function (dstDocument)
	{
		this._path = null;
		this._tags = {};
		this._contexts = [];
		this._contextMap = {};
		this._links = [];
		this._mainContext = null;
		this._doc = dstDocument ? dstDocument : document;
	}
	
	,_compileEnd: function ()
	{
		for (var i = this._links.length - 1; i >= 0; i--)
		{
			var l = this._links[i];
			var contextId = this._contextMap[l.objectId];
			
			if (contextId != undefined)
			{
				if (l.prop)
					l.context.objectProps[l.prop] = contextId;
				else
					l.context.childs.push (contextId);
				
				this._links.splice (i, 1);
			}
			else
			{
				var object = this._addedMap[l.objectId];
				
				if (!object && this._parentResult)
					object = this._parentResult.getById (l.objectId);
			
				if (object)
				{
					l.context.props[l.prop] = object;
					this._links.splice (i, 1);
				}
			}
		}
	}

	,_compileNode: function (node)
	{
		var context = null;
		var tagName = null;

		if (node.nodeType === Node.ELEMENT_NODE)
			tagName = node.tagName.toLowerCase ();
		else if (node.nodeType !== Node.TEXT_NODE
		|| /^[\n\r\t]*$/.test (node.textContent))
			return null;
			
		var context = 
			   this.textCompile (node, tagName)
			|| this.objectCompile (node, tagName)
			|| this.elementCompile (node, tagName);

		context.id = this._contexts.length;

		if (tagName)
		{
			var nodeId = node.getAttribute ('id');
	
			if (nodeId)
				this._contextMap[nodeId] = context.id;

			var tags = this._tags[tagName];
		
			if (!tags)
				this._tags[tagName] = tags = [];
			
			tags.push (context.id);
		}

		this._contexts.push (context);
		return context;
	}

	/**
	 * Creates a text node context.
	 **/
	,textCompile: function (node, tagName)
	{
		if (!tagName)
			var text = node.textContent;
		else if (tagName === 't')
			var text = _(node.firstChild.textContent); 
		else
			return null;

		return {text: text};
	}
	
	,textInstantiate: function (context)
	{
		return this._doc.createTextNode (context.text);
	}
	
	/**
	 * Creates a object context.
	 **/
	,objectCompile: function (node, tagName)
	{
		var klass = vnCustomTags[tagName];
		
		if (!klass)
			return null;

		var props = {};
		var objectProps = {};
		var childs = [];
		var events = {};
			
		var context = {
			klass: klass,
			props: props,
			objectProps: objectProps,
			childs: childs,
			events: events,
			custom: null
		};

		var a = node.attributes;

		for (var i = 0; i < a.length; i++)
		{
			var attribute = a[i].nodeName;
			var value = a[i].nodeValue;

			if (this._isEvent (attribute))
			{
				var handler = this._getMethod (value)
				
				if (handler)
					events[attribute.substr (3)] = handler;
			}
			else if (!/^(id|property)$/.test (attribute))
			{
				this.propCompile (context, klass, props,
					node, attribute, value);
			}
		}
		
		var childNodes = node.childNodes;

		if (childNodes)
		for (var i = 0; i < childNodes.length; i++)
		{
			var child = childNodes[i];
			var isElement = child.nodeType === Node.ELEMENT_NODE;
			var childTagName = isElement ? child.tagName.toLowerCase () : null;
			var childContext;
			
			if (childTagName === 'pointer')
			{
				this._addLink (context, null, child.getAttribute ('object'));
			}
			else if (childTagName === 'custom')
			{
				context.custom = child;
			}
			else if (childContext = this._compileNode (child))
			{
				var prop = isElement ? child.getAttribute ('property') : null;
	
				if (prop)
				{
					prop = prop.replace (/-./g, this._replaceFunc); 
					objectProps[prop] = childContext.id;
				}
				else
					childs.push (childContext.id);
			}
		}

		return context;
	}

	,propCompile: function (context, klass, props, node, attribute, value)
	{
		var isLink = false;
		var newValue = null;
		var propName = attribute.replace (/-./g, this._replaceFunc);
		var propInfo = klass.Properties[propName];

		if (!propInfo)
		{
			this._showError ('Attribute \'%s\' not valid for tag \'%s\'',
				attribute, node.tagName);
			return;
		}
		if (!value)
		{
			this._showError ('Attribute \'%s\' empty on tag \'%s\'',
				attribute, node.tagName);
			return;
		}

		switch (propInfo.type)
		{
			case Boolean:
				newValue = (/^(true|1)$/i).test (value);
				break;
			case Number:
				newValue = 0 + new Number (value);
				break;
			case String:
				newValue = this._translateValue (value);
				break;
			case Function:
				var method = this._getMethod (value);
				newValue = method ? method.bind (this.signalData) : null;
				break;
			default:
			if (propInfo.enumType)
				newValue = propInfo.enumType[value];
			else if (propInfo.type instanceof Function)
				isLink = true;
		}
		
		if (isLink)
			this._addLink (context, propName, value);
		else if (newValue !== null && newValue !== undefined)
			props[propName] = newValue;
		else
			this._showError ('Attribute \'%s\' invalid for tag \'%s\'',
				attribute, node.tagName);
	}
	
	,objectInstantiate: function (context)
	{
		return new context.klass ();
	}
	
	,objectLink: function (context, object, objects, res)
	{
		object.setProperties (context.props);
	
		var objectProps = context.objectProps;
		for (var prop in objectProps)
			object[prop] = objects[objectProps[prop]];

		var childs = context.childs;
		for (var i = 0; i < childs.length; i++)
			object.appendChild (objects[childs[i]]);
		
		var events = context.events;
		for (var event in events)
			object.on (event, events[event], this.signalData);

		if (context.custom)
			object.loadXml (res, context.custom);
	}
	
	/**
	 * Creates a HTML node context.
	 **/
	,elementCompile: function (node, tagName)
	{
		var attributes = {};
		var childs = [];
		var events = {};
		var handler;

		var a = node.attributes;

		for (var i = 0; i < a.length; i++)
		{
			var attribute = a[i].nodeName;
			var value = a[i].nodeValue;

			if (this._isEvent (attribute))
			{
				var handler = this._getMethod (value);
				
				if (handler)
					events[attribute.substr (3)] = handler;
			}
			else if (attribute !== 'id')
				attributes[attribute] = this._translateValue (value);
		}

		var childContext;
		var childNodes = node.childNodes;

		if (childNodes)
		for (var i = 0; i < childNodes.length; i++)
		if (childContext = this._compileNode (childNodes[i]))
				childs.push (childContext.id);

		return {
			tagName: tagName,
			attributes: attributes,
			childs: childs,
			events: events
		};
	}
	
	,elementInstantiate: function (context)
	{
		return this._doc.createElement (context.tagName);
	}
	
	,elementLink: function (context, object, objects)
	{
		var attributes = context.attributes;
		for (var attribute in attributes)
			object.setAttribute (attribute, attributes[attribute]);

		var childs = context.childs;
		for (var i = 0; i < childs.length; i++)
		{
			var child = objects[childs[i]];
		
			if (child instanceof Htk.Widget)
				child = child.node;
			if (child instanceof Node)
				object.appendChild (child);
		}
		
		var events = context.events;
		for (var event in events)
			object.addEventListener (event,
				events[event].bind (this.signalData));
	}
	
	,_showError: function (error)
	{
		var path = this._path ? this._path : 'Node';		
		var logArgs = ['Vn.Builder: %s: '+ error, path];

		for (var i = 1; i < arguments.length; i++)
			logArgs.push (arguments[i]);
	
		console.warn.apply (null, logArgs);
	}
	
	,_addLink: function (context, prop, objectId)
	{
		this._links.push ({
			 context: context
			,prop: prop
			,objectId: objectId
		});
	}

	,_translateValue: function (value)
	{
		var chr = value.charAt (0);

		if (chr === '_')
			return _(value.substr (1));
		else if (chr === '\\' && value.charAt (1) === '_')
			return value.substr (1);
			
		return value;		
	}
	
	,_getMethod: function (value)
	{	
		if (this.signalData)
			var method = this.signalData[value];
		else
			var method = window[value];

		if (method === undefined)
			this._showError ('Function \'%s\' not found', value);

		return method;
	}
	
	,_isEvent: function (attribute)
	{
		return /^on-\w+/.test (attribute);
	}
	
	,_replaceFunc: function (token)
	{
		return token.charAt(1).toUpperCase ();
	}
});

var BuilderResult = new Class
({
	Extends: Object

	,initialize: function (builder, objects)
	{
		this.builder = builder;
		this.objects = objects;
	}

	,getMain: function ()
	{
		return this.builder.getMain (this);
	}
	
	,$: function (objectId)
	{
		return this.builder.getById (this, objectId);
	}
	
	,getById: function (objectId)
	{
		return this.builder.getById (this, objectId);
	}
	
	,getByTagName: function (tagName)
	{
		return this.builder.getByTagName (this, tagName);
	}
	
	,link: function ()
	{
		this.builder.link (this);
	}
	
	,_destroy: function ()
	{
		var objects = this.objects;
	
		for (var i = 0; i < objects.length; i++)
		if (objects[i] instanceof Object)
			objects[i].unref ();

		this.parent ();
	}
});

