require ('mootools');

Vn = module.exports = {
	 Locale         : require ('./locale')
	,Enum           : function () {}
	,Object         : require ('./object')
	,Browser        : require ('./browser')
	,Cookie         : require ('./cookie')
	,Date           : require ('./date')
	,Value          : require ('./value')
	,Url            : require ('./url')
	,Mutators       : require ('./mutators')
	,Param          : require ('./param')
	,HashListener   : require ('./hash-listener')
	,Hash           : require ('./hash')
	,HashParam      : require ('./hash-param')
	,Node           : require ('./node')
	,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: function ()
	{
		if (this._version === undefined)
		{
			var re = /[; ]vnVersion=([^\\s;]*)/;
			var sMatch = (' '+ document.cookie).match (re);
			this._version = (sMatch) ? '?'+ unescape (sMatch[1]) : '';
		}
		
		return this._version;
	}

	/**
	 * 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: function (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: function (fileName)
	{
		var cssData = this.cssIncludes[fileName];
	
		if (cssData && cssData.included)
		{
			cssData.link.disabled = true;
			cssData.included = false;
		}		
	}
	
	,_createIncludeData: function (path)
	{
		var includeData = {
			 depCount: 0
			,success: false
			,loaded: false
			,callbacks: []
			,dependants: []
		};

		this.includes[path] = includeData;
		return includeData;
	}
	
	,_handleCallback: function (includeData, callback)
	{
		if (!callback)
			return;
	
		if (includeData.success)
			callback (includeData.loaded);
		else
			includeData.callbacks.push (callback);
	}
	
	,_resolveDeps: function (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: function (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: function ()
	{
		this.mainDepsLoaded = true;
		this._callMain ();
	}
	
	,_onWindowLoad: function ()
	{
		this.windowReady = true;
		this._callMain ();
	}
	
	,_callMain: function ()
	{
		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: function ()
	{
		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: function ()
	{
		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: function (callback)
	{
		this.currentCallback = callback;
	}
	
	/**
	 * Includes an entire Javascript library including it's localized file.
	 *
	 * @param {string} libName The folder of the library
	 * @param {Array<string>} files Array with every library file name
	 **/
	,includeLib: function (libName, files)
	{
		Vn.Locale.loadScript ('js/'+ libName +'.js');

		for (var i = 0; i < files.length; i++)
			this.include ('js/'+ libName +'/'+ files[i]);
	}

	/**
	 * 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
	 * @param {Function} callback The function to call when script is
	 * downloaded and included
	 **/
	,includeJs: function (fileName, callback, skipVersion)
	{
		var includeData = this._realIncludeJs (fileName, skipVersion);
		this._handleCallback (includeData, callback);
	}
	 
	,_realIncludeJs: function (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.bind (this, includeData, true);
			script.onerror =
				this._onScriptLoad.bind (this, includeData, false);
			script.onreadystatechange =
				this._onScriptStateChange.bind (this, includeData, script);

			this.head.appendChild (script);
		}

		return includeData;
	}

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

	,_onScriptLoad: function (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
	 * @param {Function} callback The function to call when file is downloaded
	 **/
	,loadXml: function (path, callback)
	{
		var includeData = this._realLoadXml (path);
		this._handleCallback (includeData, callback);
	}

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

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

		return includeData;
	}
	
	,_onXmlReady: function (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: function (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: function ()
	{
		if (this.isMobileCached === null)
		{
			var regExp = /(Android|webOS|iPhone|iPad|iPod|BlackBerry|Windows Phone)/i;
			this.isMobileCached =  navigator.userAgent.match (regExp);
		}

		return this.isMobileCached;
	}
};