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

const CompilerObject = require('./compiler-object');
const CompilerElement = require('./compiler-element');
const CompilerText = require('./compiler-text');

const regCompilers = [
	CompilerText,
	CompilerObject,
	CompilerElement
];

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

	/**
	 * Compiles an XML file.
	 *
	 * @param {String} path The XML path
	 * @return {Boolean} %true on success, %false othersise
	 */
	,compileFile(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(xmlString) {
		const parser = new DOMParser();
		const 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(doc) {
		if (!doc)
		return false;

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

		const childs = docElement.childNodes;
		if (childs)
		for (let i = 0; i < childs.length; i++)
			this._compile(childs[i]);

		this._postCompile();
		return true;
	}
	
	/**
	 * Compiles a single DOM node.
	 *
	 * @path Node The DOM node
	 * @return %true on success, %false othersise
	 */
	,compileNode(node) {
		this._preCompile();
		this._mainContext = this._compile(node).id;
		this._postCompile();
		return true;
	}

	/**
	 * Called before starting to compile nodes.
	 */
	,_preCompile() {
		this._path = null;
		this._tags = {};
		this._contexts = [];
		this._exprContexts = [];
		this._contextMap = {};
		this._links = [];
		this._mainContext = null;

		this._compilers = [];
		for (regCompiler of regCompilers)
			this._compilers.push(new regCompiler(this));
	}
	
	/**
	 * Called after all nodes have been compiled.
	 */
	,_postCompile() {
		for (const compiler of this._compilers)
			compiler.postCompile(this._contextMap);
	}

	/**
	 * Compiles a node.
	 */
	,_compile(node) {
		let context = null;
		let tagName = null;
		const 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;

		let i;
		const compilers = this._compilers;
		for (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) {
			const nodeId = node.getAttribute('id');
	
			if (nodeId) {
				this._contextMap[kebabToCamel(nodeId)] = context.id;
				context.nodeId = nodeId;
			}

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

		this._contexts.push(context);
		return context;
	}
	
	,load(dstDocument, thisArg, parentScope) {
		if (!this._contexts) return null;
		const doc = dstDocument ? dstDocument : document;
		const objects = new Array(this._contexts.length);
		const exprValues = new Array(this._exprContexts.length);
		return new Scope(this, doc, objects, exprValues, thisArg, parentScope);
	}

	,showError(error) {
		const path = this._path ? this._path : 'Node';		
		const logArgs = ['Vn.Builder: %s: '+ error, path];

		for (let i = 1; i < arguments.length; i++)
			logArgs.push(arguments[i]);
	
		console.warn.apply(null, logArgs);
	}
});
