
/**
 * @class Ext.ux.util.Dependencies
 * <p>[introduction here]
 * Example usage:</p>
 * <pre><code>
// [explanation here]
Ext.define('My.random.Class', {
	extend: ['Ext.panel.Panel'],
	mixins: {
		dependencies: 'Ext.ux.mixins.Dependencies'
    },
    initComponent: function() {

		this.callParent();

		this.initDependencies();
    }
});

// [explanation here]
var p = Ext.create('My.random.Class', {
	border: false,
	dependencies: [
		[
			'scripts/jquery.js',
			'styles/mystyle.css'
		],
		[
			'plugins/jquery.plugin.js'
		]
	],
	items: [{
		title: 'Some sub panel',
		width: 300,
		autoHeight: true,
		html: 'Some content'
	}],
	listeners: {
		'dependenciesinit': {
			fn: function(self) {
				console.log($, jQuery);
			}
		}
	}
});
</code></pre>
 */

Ext.define('Ext.ux.util.Dependencies', {

	statics: {
		_loaded: {}
	},

	/**
	 * @private
	 */
	_dependenciesStack: null,

	/**
	 * @private
	 */
	_dependenciesLoaded: null,

	/**
	 * @private
	 */
	_dependenciesRqsCnt: 0,

	/**
	 * @private
	 */
	_dependenciesPrevious: null,

	_dependenciesInitialized: false,

	/**
	 * Performs initialization of this mixin. Component classes using this mixin should call this method
	 * during their own initialization.
	 */
	initDependencies: function(scope, callback) {

		scope = scope || this;
		callback = callback || Ext.emptyFn;

		if (scope._dependenciesInitialized)
			return false;

		var me = scope,
			dependencies = me.dependencies || null,
			autoLoad = me.autoLoadDependencies || true;

		/**
		 * @private
		 */
		scope._dependenciesStack = [];

		/**
		 * @private
		 */
		scope._dependenciesLoaded = [];

		me._dependenciesInitialized = true;

		if (dependencies && autoLoad) {

			me.loadDependencies(dependencies, function() {

				me.fireEvent('dependenciesinit', me, me, me._dependenciesLoaded); // deprecate this event call

				me.fireEvent('dependencyinit', me, me._dependenciesLoaded);

				if (Ext.isFunction(callback)) {
					callback.call(me, me);
				}
			});
		}
	},

	/**
	     * [explanation here]
	     * @param {Array/String} src The dependencies to load, can be an array of strings, a string (one source file), an array of arrays (for nested dependencies)
	     * @param {Function} value2 Callback function
	     * @chainable
	     */
	loadDependencies: function(src, callback) {

		callback = callback || Ext.emptyFn;

		var me = this,
			stackEntry,
			i = 0;

		src = me._filterDependencies(src);

		stackEntry = {
			inProcess: 0,
			callback: callback,
			previous: me._dependenciesPrevious
		};

		if (src instanceof Array) {
			for (; i < src.length; i++) {
				if (!src[i]) src.splice(i, 1);
			}

			if (src[0] instanceof Array) {

				var s1 = src[0],
					s2 = src.slice(1);

				me.loadDependencies(s1, function () {

					if (s2.length === 0) {
						if (callback) callback.call();
					}

					else {
						me.loadDependencies(s2, function () {
							if (callback) callback.call();
						});
					}
				});

				return;
			}

			if (src.length) {

				me._dependenciesPushStack(stackEntry, src.length);

				for (i = 0; i < src.length; i++) {

					if (src[i]) {
						if (!me.isSourceLoaded(src[i])) {
							me.addLoadedSource(src[i]);
						}
						me._dependenciesLoadResource(src[i], stackEntry);
					}
				}
			}
			else {

				callback.call();
			}
		}

		else if (src) {

			// TODO: Check out all changes in this else if statement

			// START OF ADDED (force loading into the array loader part for easier check if already loaded)
			var a = [];
			a.push(src);
			me.loadDependencies(a);

			// END OF ADDED

			/* START OF REMOVED

				me._dependenciesPushStack(stackEntry, 1);
				me._dependenciesLoadResource(src, stackEntry);

			END OF REMOVED */
		}

		else {

			me.fireEvent('dependenciesloaded', me); // deprecate this call

			me._dependenciesReady = true;

			if (callback) callback.call();
		}

		return me;
	},

	/**
	 * @private
	 */
	_filterDependencies: function(m_dependencies) {

		var i_this = this,
			a_proxy = [],
			a_dependencies;

		if (!Ext.isArray(m_dependencies)) {
			a_proxy.push(m_dependencies);
			a_dependencies = i_this._filterDependencies(a_proxy);
		}
		else {
			a_dependencies = m_dependencies;
		}

		Ext.Array.each(a_dependencies, function(m_dependency, n_index) {

			if (Ext.isArray(m_dependency)) {
				m_dependency = i_this._filterDependencies(m_dependency);
			}
			else {

				// only allow js and css files to be cached like this!
				var a_split = m_dependency.split('.'),
					s_type = a_split[a_split.length - 1];

				if (s_type == 'js' || s_type == 'css') {

					if (i_this.isSourceLoaded(m_dependency)) {
						a_dependencies.splice(n_index, 1);
					}
				}
			}
		});

		return a_dependencies;
	},

	/**
	 * @private
	 */
	_dependenciesDoCallback: function(req) {

		var me = this;

		req.done = true;

		me._dependenciesRqsCnt--;

		// Handle memory leak in IE
		req.elm.onload = req.elm.onreadystatechange = null;

		if (req.head && req.elm.parentNode) {
			// You can't go and remove the item from the DOM structure if it's not loaded
			// You can have unexpected behaivor.
			// req.head.removeChild(req.elm);
		}

		// decement all previous requirements levels, This need to be seperate since callback can start increment
		for (var se = req.stackEntry; se; se = se.previous) {
			se.inProcess--;
		}

		for (se = req.stackEntry; se; se = se.previous) {

			if (se.callback	&& se.inProcess === 0) {

				var cb = se.callback;
				se.callback = null;
				cb.call();

				if (me._dependenciesRqsCnt === 0) {

					me._dependenciesStack = [];
					me.previous = null;
				}
			}
		}
	},

	/**
	 * @private
	 */
	_dependenciesLoadResource: function(src, stackEntry) {

		var me = this,
			req;

		req = {
			src: src,
			stackEntry: stackEntry,
			elm: {},
			head: document.getElementsByTagName("head")[0] || document.documentElement,
			done: false
		};

		me._dependenciesRqsCnt++;

		if (me._dependenciesLoaded[src]) {
			me._dependenciesDoCallback(req);
			return;
		}

		me._dependenciesLoaded[src] = true;

		var a_extension = src.split('.'),
			s_extension = a_extension[a_extension.length - 1]

		if (s_extension == 'css') {

			req.elm = document.createElement('link');
			req.elm.rel = 'stylesheet';
			req.elm.type = 'text/css';
			req.elm.href = src;

			(function() {
				// Load css file, scope is make for not population parent score with
				// img
				var img = document.createElement("img");

				img.style.position = "absolute";
				img.style.top = '-1000px';
				img.style.left = '-1000px';

				img.onerror = img.onload = function() {

					img.parentNode.removeChild(img);
					me._dependenciesDoCallback(req);
				};

				document.body.appendChild(img);

				img.src = src;
			})();
		}

		else if (s_extension == 'js') {

			req.elm = document.createElement('script');
			req.elm.type = 'text/javascript';
			req.elm.src = src;

			// This is for IE
			req.elm.onreadystatechange = function () {
				if ((this.readyState !== "loaded" && this.readyState !== "complete") || (req.done)) {
					return;
				}
				me._dependenciesDoCallback(req);
			};

			req.elm.onload = function () {
				me._dependenciesDoCallback(req);
			};
		}
		else {

			Ext.require(src, function () {
				me._dependenciesDoCallback(req);
			});

			return;
		}

		// Use insertBefore instead of appendChild  to circumvent an IE6 bug.
		// This arises when a base node is used (#2709 and #4378).
		//req.head.insertBefore( req.elm, req.head.firstChild );

		// AND: The new item needs to be the last item in the DOM structure.
		// Else you may cause unespected problems in browser.
		req.head.appendChild(req.elm);

		return;
	},

	/**
	 * @private
	 */
	_dependenciesPushStack: function(stackEntry, len) {

		var me = this;

		for (var se = stackEntry; se; se = se.previous) {

			se.inProcess += len;
		}

		me._dependenciesStack.push(stackEntry);
		me._dependenciesPrevious = stackEntry;
	},

	addLoadedSource: function(s_src) {

		Ext.ux.util.Dependencies._loaded[s_src.toLowerCase()] = true;
	},

	isSourceLoaded: function(s_src) {

		return Ext.ux.util.Dependencies._loaded[s_src.toLowerCase()] ? true : false;
	},

	isDependencyReady: function() {

		var me = this,
			isReady;

		isReady = this._dependenciesRqsCnt === 0;

		return isReady;
	},

	isDependencyInit: function() {

		return this._dependenciesInitialized;
	}
});
