var VnObject = require ('./object');
var Scope = require ('./scope');
var kebabToCamel = require ('./string-util').kebabToCamel;

var CompilerObject = require ('./compiler-object');
var CompilerElement = require ('./compiler-element');
var CompilerText = require ('./compiler-text');
var CompilerString = require ('./compiler-string');
var CompilerInterpolable = require ('./compiler-interpolable');

var regCompilers = [
	CompilerString,
	CompilerObject,
	CompilerElement,
	CompilerText
];

/**
 * Creates a object from a XML specification.
 */
module.exports = new Class
({
	 Extends: VnObject

	,_contexts: null

	/**
	 * Compiles an XML file.
	 *
	 * @param {String} path The XML path
	 * @return {Boolean} %true on success, %false othersise
	 */
	,compileFile: function (path)
	{
		this._path = path;
		return this.compileDocument (Vn.getXml (path));
	}
	
	/**
	 * Compiles an XML string.
	 *
	 * @param {String} xmlString The XML string
	 * @return {Boolean} %true on success, %false othersise
	 */
	,compileString: function (xmlString)
	{
		var parser = new DOMParser ();
		var doc = parser.parseFromString (xmlString, 'text/xml');
		return this.compileDocument (doc);
	}
	
	/**
	 * Compiles a XML document.
	 *
	 * @param {Document} doc The DOM document
	 * @return {Boolean} %true on success, %false othersise
	 */
	,compileDocument: function (doc)
	{	
		if (!doc)
			return false;

		this._preCompile ();

		var docElement = doc.documentElement;
		
		if (docElement.tagName !== 'vn')
		{
			this.showError ('The toplevel tag should be named \'vn\'');
			this._contexts = null;
			return false;
		}

		var childs = docElement.childNodes;

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

		this._postCompile ();
		return true;
	}
	
	/**
	 * Compiles a single DOM node.
	 *
	 * @param {Node} path The DOM node
	 * @return {Boolean} %true on success, %false othersise
	 */
	,compileNode: function (node)
	{
		this._preCompile ();
		this._mainContext = this._compile (node).id;
		this._postCompile ();
		return true;
	}
	
	/**
	 * Called before starting to compile nodes.
	 */
	,_preCompile: function ()
	{
		this._path = null;
		this._tags = {};
		this._contexts = [];
		this._contextMap = {};
		this._mainContext = null;
		this._interpoler = new CompilerInterpolable (this);

		var compilers = [];
		this._compilers = compilers;

		for (var i = 0; i < regCompilers.length; i++)
			compilers.push (new regCompilers[i] (this));
	}
	
	/**
	 * Called after all nodes have been compiled.
	 */
	,_postCompile: function ()
	{
		var compilers = this._compilers;
		for (var i = 0; i < compilers.length; i++)
			compilers[i].postCompile (this._contextMap);
	}

	/**
	 * Compiles a node.
	 */
	,_compile: function (node)
	{
		var context = null;
		var tagName = null;
		var isElement = node.nodeType === Node.ELEMENT_NODE;

		if (isElement)
			tagName = node.tagName.toLowerCase ();
		else if (node.nodeType !== Node.TEXT_NODE
		|| /^[\n\r\t]*$/.test (node.textContent))
			return null;

		var compilers = this._compilers;
		for (var i = 0; i < compilers.length && context === null; i++)
			context = compilers[i].compile (this, node, tagName);
	
		context.id = this._contexts.length;
		context.compiler = compilers[i - 1];

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

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

		this._contexts.push (context);
		return context;
	}
	
	/**
	 * Creates a new scope from a compiled XML tree.
	 * 
	 * @param {Document} dstDocument The document used to create the nodes
	 * @param {Object} thisArg The object where to bind methods
	 * @param {Scope} parentScope The parent scope or %null for no parent
	 * @param {Lot} lot The default lot
	 * @return {Scope} The created scope
	 */
	,load: function (dstDocument, thisArg, parentScope, extraObjects, lot)
	{
		if (this._contexts === null)
			return null;
	
		var doc = dstDocument ? dstDocument : document;
		var contexts = this._contexts;
		var len = contexts.length;
		var objects = new Array (len);
		var scope = new Scope (this, objects, thisArg, parentScope, lot)
	
		for (var i = 0; i < len; i++)
		{
			var context = contexts[i];
			objects[i] = context.compiler.instantiate (doc, context, scope);
		}
		
		scope.init (extraObjects);
		return scope;
	}
	
	/**
	 * Links all scope objects and connects it's events.
	 */
	,link: function (scope)
	{
		var contexts = this._contexts;
		var objects = scope.objects;
		var compilers = this._compilers;

		this._interpoler.preLink (scope);

		for (var i = 0; i < compilers.length; i++)
			compilers[i].preLink (scope);

		for (var i = 0; i < contexts.length; i++)
		{
			var context = contexts[i];
			context.compiler.link (context, objects[i], objects, scope);
		}

		for (var i = 0; i < contexts.length; i++)
		{
			var context = contexts[i];
			context.compiler.connect (context, objects[i], objects, scope);
		}

		this._interpoler.postLink (scope);
	
		for (var i = 0; i < compilers.length; i++)
			compilers[i].postLink (scope);
	}

	/**
	 * Frees all resources allocated by a scope.
	 */
	,freeScope: function (scope)
	{
		this._interpoler.free (scope);

		var compilers = this._compilers;
		for (var i = 0; i < compilers.length; i++)
			compilers[i].free (scope);
	}

	/**
	 * Logs an error parsing the node.
	 * 
	 * @param {String} error The error message template
	 * @param {...} varArgs The message template arguments
	 */
	,showError: function (error)
	{
		var path = this._path ? this._path : '<unknown template>';		
		var logArgs = ['%s: '+ error, path];

		for (var i = 1; i < arguments.length; i++)
			logArgs.push (arguments[i]);
	
		console.warn.apply (console, logArgs);
	}
	
	,getMain: function (result)
	{
		return result.objects[this._mainContext];
	}

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

			return arr;
		}
		
		return [];
	}
});