
require('mootools');

Vn = module.exports = {};

Object.assign(Vn, {
	 Locale         : require('./locale')
	,Enum           : require('./enum')
	,Type           : require('./type')
	,Object         : require('./object')
	,Mutators       : require('./mutators')
	,Browser        : require('./browser')
	,Cookie         : require('./cookie')
	,Date           : require('./date')
	,Value          : require('./value')
	,Url            : require('./url')
	,LotIface       : require('./lot-iface')
	,Lot            : require('./lot')
	,LotQuery       : require('./lot-query')
	,Hash           : require('./hash')
	,ParamIface     : require('./param-iface')
	,Param          : require('./param')
	,Spec           : require('./spec')
	,Model          : require('./model')
	,ModelIface     : require('./model-iface')
	,ModelProxy     : require('./model-proxy')
	,IteratorIface  : require('./iterator-iface')
	,Iterator       : require('./iterator')
	,Form           : require('./form')
	,Node           : require('./node')
	,NodeBuilder    : require('./node-builder')
	,Component      : require('./component')
	,Builder        : require('./builder')
	,JsonException  : require('./json-exception')
	,JsonConnection : require('./json-connection')

	,Config: {}
	,includes: {}
	,cssIncludes: {}
	,currentDeps: []
	,currentCallback: null
	,head: document.getElementsByTagName('head')[0]
	,isMobileCached: null
	
	,getVersion() {
		if (this._version === undefined) {
			var re = /[; ]vnVersion=([^\\s;]*)/;
			var sMatch = (' '+ document.cookie).match(re);
			this._version = (sMatch) ? '?'+ unescape(sMatch[1]) : '';
		}
		
		return this._version;
	}

	,setVersion(version) {
		document.cookie = `vnVersion=${version}; SameSite=Lax;`;
	}

	/**
	 * Includes a new CSS stylesheet in the current document, if the stylesheet
	 * is already included, does nothing.
	 *
	 * @param {string} fileName The stylesheet file name
	 */
	,includeCss(fileName) {	
		var cssData = this.cssIncludes[fileName];

		if (!cssData) {
			var link = document.createElement('link');
			link.rel = 'stylesheet';
			link.type = 'text/css';
			link.href = fileName + this.getVersion();
			this.head.appendChild(link);
			
			this.cssIncludes[fileName] = 
			{
				 included: true
				,link: link
			};
		} else if (!cssData.included) {
			cssData.link.disabled = false;
			cssData.included = true;
		}
	}
	
	/**
	 * Excludes a CSS stylesheet from the current document.
	 *
	 * @param {string} fileName The stylesheet file name
	 */
	,excludeCss(fileName) {
		var cssData = this.cssIncludes[fileName];
	
		if (cssData && cssData.included) {
			cssData.link.disabled = true;
			cssData.included = false;
		}		
	}
	
	,_createIncludeData(path) {
		var includeData = {
			path,
			 depCount: 0,
			success: false,
			loaded: false,
			callbacks: [],
			dependants: [],
		};

		this.includes[path] = includeData;
		return includeData;
	}
	
	,_resolveDeps(includeData) {
		includeData.success = true;

		var callbacks = includeData.callbacks;
	
		if (callbacks)		
		for (var i = 0; i < callbacks.length; i++)
			callbacks[i](includeData.loaded);

		var dependants = includeData.dependants;

		if (dependants)
		for (var i = 0; i < dependants.length; i++) {
			var dependant = dependants[i];
			dependant.depCount--;
			
			if (dependant.depCount == 0)
				this._resolveDeps(dependant);
		}

		delete includeData.callbacks;
		delete includeData.dependants;
		delete includeData.depCount;
	}

	/**
	 * Initializes the library and calls the passed function when all
	 * includes and its dependencies are resolved.
	 * Should be called on the last statically incuded script.
	 *
	 * @param {Function} callback The main function
	 */
	,main(callback) {
		if (this.mainCalled) {
			Vn.warning("Vn: main method should be called only once");
			return;
		}
	
		this.mainCalled = true;
		this.mainCallback = callback;

		var basePath = location.protocol +'//'+ location.host;
		basePath += location.port ? ':'+ location.port : '';
		basePath += location.pathname;

		var scripts = this.head.getElementsByTagName('script');
		var includes = this.currentDeps;

		for (var i = 0; i < scripts.length; i++) {
			var path = scripts[i].src.substr(basePath.length);
			path = path.substr(0, path.indexOf('.js')) +'.js';

			var includeData = this.includes[path];
			
			if (includeData === undefined) {
				this.currentDeps = includes;
				var includeData = this._createIncludeData(path);
				this._onScriptLoad(includeData, true);
			}
		}

		includeData.callbacks.push(this._onMainDepsLoad.bind(this));
		window.addEventListener('load', this._onWindowLoad.bind(this));
	}
	
	,_onMainDepsLoad() {
		this.mainDepsLoaded = true;
		this._callMain();
	}
	
	,_onWindowLoad() {
		this.windowReady = true;
		this._callMain();
	}
	
	,_callMain() {
		if (this.mainCallback && this.windowReady && this.mainDepsLoaded)
			this.mainCallback();
	}
	
	/**
	 * Includes a set of javascript files and sets it as dependecies of the
	 * current script.
	 *
	 * @param {...} The list of files as function arguments
	 */
	,include() {
		for (var i = 0; i < arguments.length; i++) {
			var includeData = this._realIncludeJs(arguments[i] +'.js');
			
			if (!includeData.success)
				this.currentDeps.push(includeData);
		}
	}

	/**
	 * Downloads a set of resources and sets it as dependecies of the
	 * current script.
	 *
	 * @param {...} The list of files as function arguments
	 */
	,resource() {
		for (var i = 0; i < arguments.length; i++) {
			var includeData = this._realLoadXml(arguments[i]);
			
			if (!includeData.success)
				this.currentDeps.push(includeData);
		}
	}
	
	/**
	 * Sets the function that will be called when current script dependencies
	 * are resolved.
	 *
	 * @param {Function} callback The callback function
	 */
	,define(callback) {
		this.currentCallback = callback;
	}

	,async _handleCallback(includeData) {
		if (includeData.success) return;

		return new Promise((resolve, reject) => {
			function callback(loaded) {
				if (!loaded)
					return reject(new Error(`Could not load resource: ${includeData.path}`));
				resolve();
			}

			includeData.callbacks.push(callback);
		});
	}

	/**
	 * Includes a new Javascript in the current document, if the script
	 * is already included, does nothing and calls the callback.
	 *
	 * @param {string} fileName The script file name
	 */
	,async includeJs(fileName, skipVersion) {
		var includeData = this._realIncludeJs(fileName, skipVersion);
		return this._handleCallback(includeData);
	}
	 
	,_realIncludeJs(fileName, skipVersion) {
		var includeData = this.includes[fileName];
	
		if (includeData === undefined) {
			includeData = this._createIncludeData(fileName);

			var src = fileName;

			if (!skipVersion)
				src = src + this.getVersion();

			var script = document.createElement('script');
			script.type = 'text/javascript';
			script.async = false;
			script.src = src;

			script.onload =
				() => this._onScriptLoad(includeData, true);
			script.onerror =
				() => this._onScriptLoad(includeData, false);
			script.onreadystatechange =
				() => this._onScriptStateChange(includeData, script);

			this.head.appendChild(script);
		}

		return includeData;
	}

	,_onScriptStateChange(includeData, script) {
		if (script.readyState == 'complete')
			this._onScriptLoad(includeData, true);
	}

	,_onScriptLoad(includeData, loaded) {
		includeData.loaded = loaded;

		if (loaded) {			
			if (this.currentCallback)
				includeData.callbacks.unshift(this.currentCallback);

			var includes = this.currentDeps;
			
			if (includes && includes.length > 0) {
				includeData.depCount = includes.length;

				for (var i = 0; i < includes.length; i++)
					includes[i].dependants.push(includeData);
			} else
				this._resolveDeps(includeData);
		} else
			this._resolveDeps(includeData);
		
		this.currentDeps = [];
		this.currentCallback = null;
	}

	/**
	 * Request an XML file.
	 *
	 * @param {string} path The file path
	 */
	,async loadXml(path) {
		var includeData = this._realLoadXml(path);
		return this._handleCallback(includeData);
	}

	,_realLoadXml(path) {
		var includeData = this.includes[path];
	
		if (includeData === undefined) {
			includeData = this._createIncludeData(path);

			var request = new XMLHttpRequest();
			request.onreadystatechange =
				() => this._onXmlReady(includeData, request);
			request.open('get', path + this.getVersion(), true);
			request.send();
		}

		return includeData;
	}
	
	,_onXmlReady(includeData, request) {
		if (request.readyState != 4)
			return;
			
		includeData.loaded = request.status == 200;

		if (includeData.loaded)
			includeData.xml = request.responseXML;
			
		this._resolveDeps(includeData);
	}
	
	/**
	 * Gets the DOM object from an included XML file.
	 *
	 * @param {string} path The file path
	 * @return {Object} The DOM object
	 */
	,getXml(path) {
		var includeData = this.includes[path];
		
		if (!(includeData && includeData.success))
			return null;
			
		return includeData.xml;
	}

	/**
	 * Checks if user is using a mobile browser.
	 *
	 * return {boolean} %true if is mobile, %false otherwise.
	 */
	,isMobile() {
		if (this.isMobileCached === null) {
			var regExp = /(Android|webOS|iPhone|iPad|iPod|BlackBerry|Windows Phone)/i;
			this.isMobileCached =  navigator.userAgent.match(regExp);
		}

		return this.isMobileCached;
	}
});
