/**
 * EGroupware clientside API object
 *
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
 * @package etemplate
 * @subpackage api
 * @link http://www.egroupware.org
 * @author Andreas Stöckel (as AT stylite.de)
 * @author Ralf Becker <RalfBecker@outdoor-training.de>
 * @version $Id$
 */

"use strict";

/**
 * This code setups the egw namespace and adds the "extend" function, which is
 * used by extension modules to inject their content into the egw object.
 */
(function() {

	var instanceUid = 0;

	// Some local functions for cloning and merging javascript objects
	function cloneObject(_obj) {
		var result = {};

		for (var key in _obj)
		{
			result[key] = _obj[key];
		}

		return result;
	}

	function mergeObjects(_to, _from) {
		// Extend the egw object
		for (var key in _from)
		{
			_to[key] = _from[key];
		}
	}

	function deleteWhere(_arr, _cond)
	{
		for (var i = _arr.length - 1; i >= 0; i--)
		{
			if (_cond(_arr[i]))
			{
				_arr.splice(i, 1);
			}
		}
	}

	/**
	 * The getAppModules function returns all application specific api modules
	 * for the given application. If those application specific api instances
	 * were not created yet, the functions creates them.
	 *
	 * @param _egw is a reference to the global _egw instance and is passed as
	 * 	a context to the module instance.
	 * @param _modules is the hash map which contains all module descriptors.
	 * @param _moduleInstances is the the object which contains the application
	 * 	and window specific module instances.
	 * @param _app is the application for which the module instances should get
	 * 	created.
	 */
	function getAppModules(_egw, _modules, _moduleInstances, _app)
	{
		// Check whether the application specific modules for that instance
		// already exists, if not, create it
		if (typeof _moduleInstances.app[_app] === 'undefined')
		{
			var modInsts = {};
			_moduleInstances.app[_app] = modInsts;

			// Otherwise create the application specific instances
			for (var key in _modules)
			{
				var mod = _modules[key];

				// Check whether the module is actually an application local
				// instance. As the module instance may already have been
				// created by another extension (when calling the egw.module
				// function) we're doing the second check.
				if (mod.flags === _egw.MODULE_APP_LOCAL
				    && typeof modInsts[key] === 'undefined')
				{
					modInsts[key] = mod.code.call(_egw, _app, window);
				}
			}
		}

		return _moduleInstances.app[_app];
	}

	function getExistingWndModules(_moduleInstances, _window)
	{
		// Search for the specific window instance
		for (var i = 0; i < _moduleInstances.wnd.length; i++)
		{
			if (_moduleInstances.wnd[i].window === _window)
			{
				return _moduleInstances.wnd[i].modules;
			}
		}

		return null;
	}

	/**
	 * The getWndModules function returns all window specific api modules for
	 * the given window. If those window specific api instances were not created
	 * yet, the functions creates them.
	 *
	 * @param _egw is a reference to the global _egw instance and is passed as
	 * 	a context to the module instance.
	 * @param _modules is the hash map which contains all module descriptors.
	 * @param _moduleInstances is the the object which contains the application
	 * 	and window specific module instances.
	 * @param _instances refers to all api instances.
	 * @param _window is the window for which the module instances should get
	 * 	created.
	 */
	function getWndModules(_egw, _modules, _moduleInstances, _instances, _window)
	{
		var mods = getExistingWndModules(_moduleInstances, _window);
		if (mods) {
			return mods;
		}

		// If none was found, create the slot
		mods = {};
		_moduleInstances.wnd.push({
			'window': _window,
			'modules': mods
		});

		// Add an eventlistener for the "onunload" event -- if "onunload" gets
		// called, we have to delete the module slot created above
		var fnct = function() {
			cleanupEgwInstances(_instances, _moduleInstances, function(_w) {
				return _w.window === _window;
			});
		};
		if (_window.attachEvent)
		{
			_window.attachEvent('onbeforeunload', fnct);
		}
		else
		{
			_window.addEventListener('beforeunload', fnct, false);
		}

		// Otherwise create the window specific instances
		for (var key in _modules)
		{
			var mod = _modules[key];

			// Check whether the module is actually a window local instance. As
			// the module instance may already have been created by another
			// extension (when calling the egw.module function) we're doing the
			// second check.
			if (mod.flags === _egw.MODULE_WND_LOCAL
			    && typeof mods[key] === 'undefined')
			{
				mods[key] = mod.code.call(_egw, null, _window);
			}
		}

		return mods;
	}

	/**
	 * Creates an api instance for the given application and the given window.
	 *
	 * @param {globalEgw} _egw is the global _egw instance which should be used.
	 * @param {object} _modules is the hash map which contains references to all module
	 * 	descriptors.
	 * @param {object} _moduleInstances is the the object which contains the application
	 * 	and window specific module instances.
	 * @param {array} _list is the overall instances list, to which the module should be
	 * 	added.
	 * @param {object} _instances is the overall instances list, to which the module should be
	 * 	added.
	 * @param {string} _app is the application for which the instance should be created.
	 * @param {DOMElement} _window is the window for which the instance should be created.
	 * @return {egw}
	 */
	function createEgwInstance(_egw, _modules, _moduleInstances, _list, _instances, _app, _window)
	{
		// Clone the global object
		var instance = cloneObject(_egw);

		// Let "_window" and "_app" be exactly null, if it evaluates to false
		_window = _window ? _window : null;
		_app = _app ? _app : null;

		// Set the application name and the window the API instance belongs to
		instance.appName = _app;
		instance.window = _window;

		// Push the newly created instance onto the instance list
		_list.push({
			'instance': instance,
			'window': _window,
			'app': _app
		});

		// Merge either the application specific and/or the window specific
		// module instances into the new instance
		if (_app)
		{
			var appModules = getAppModules(_egw, _modules, _moduleInstances,
				_app);

			for (var key in appModules)
			{
				mergeObjects(instance, appModules[key]);
			}
		}

		if (_window)
		{
			var wndModules = getWndModules(_egw, _modules, _moduleInstances,
				_instances, _window);

			for (var key in wndModules)
			{
				mergeObjects(instance, wndModules[key]);
			}
		}

		// Return the new api instance
		return instance;
	}

	/**
	 * Returns a egw instance for the given application and the given window. If
	 * the instance does not exist now, the instance will be created.
	 *
	 * @param {globalEgw} _egw is the global _egw instance which should be used.
	 * @param {object} _modules is the hash map which contains references to all module
	 * 	descriptors.
	 * @param {object} _moduleInstances is the the object which contains the application
	 * 	and window specific module instances.
	 * @param {object} _instances is the overall instances list, to which the module should be
	 * 	added.
	 * @param {string} _app is the application for which the instance should be created.
	 * @param {DOMElement} _window is the window for which the instance should be created.
	 * @return {egw}
	 */
	function getEgwInstance(_egw, _modules, _moduleInstances, _instances, _app, _window)
	{
		// Generate the hash key for the instance descriptor object
		var hash = _app ? _app : '~global~';

		// Let "_window" be exactly null, if it evaluates to false
		_window = _window ? _window : null;

		// Create a new entry if the calculated hash does not exist
		if (typeof _instances[hash] === 'undefined')
		{
			_instances[hash] = [];
			return createEgwInstance(_egw, _modules, _moduleInstances,
				_instances[hash], _instances, _app, _window);
		}
		else
		{
			// Otherwise search for the api instance corresponding to the given
			// window
			for (var i = 0; i < _instances[hash].length; i++)
			{
				if (_instances[hash][i].window === _window)
				{
					return _instances[hash][i].instance;
				}
			}
		}

		// If we're still here, no API instance for the given window has been
		// found -- create a new entry
		return createEgwInstance(_egw, _modules, _moduleInstances,
			_instances[hash], _instances, _app, _window);
	}

	function cleanupEgwInstances(_instances, _moduleInstances, _cond)
	{
		// Iterate over the instances
		for (var key in _instances)
		{
			// Delete all entries corresponding to closed windows
			deleteWhere(_instances[key], _cond);

			// Delete the complete instance key if the array is empty
			if (_instances[key].length === 0)
			{
				delete _instances[key];
			}
		}

		// Delete all entries corresponding to non existing elements in the
		// module instances
		deleteWhere(_moduleInstances.wnd, _cond);
	}

	function mergeGlobalModule(_module, _code, _instances, _moduleInstances)
	{
		// Generate the global extension
		var globalExtension = _code.call(egw, null, window);

		// Store the global extension module
		_moduleInstances.glo[_module] = globalExtension;

		for (var key in _instances)
		{
			for (var i = 0; i < _instances[key].length; i++)
			{
				mergeObjects(_instances[key][i].instance,
						globalExtension);
			}
		}
	}

	function mergeAppLocalModule(_module, _code, _instances, _moduleInstances)
	{
		// Generate the global extension
		var globalExtension = _code.call(egw, null, window);

		// Store the global extension module
		_moduleInstances.glo[_module] = globalExtension;

		// Merge the extension into the global instances
		for (var i = 0; i < _instances['~global~'].length; i++)
		{
			mergeObjects(_instances['~global~'][i].instance, globalExtension);
		}

		for (var key in _moduleInstances.app)
		{
			// Create the application specific instance and
			// store it in the module instances
			var appExtension = _code.call(egw, key, window);
			_moduleInstances.app[key][_module] = appExtension;

			// Merge the extension into all instances for
			// the current application
			for (var i = 0; i < _instances[key].length; i++)
			{
				mergeObjects(_instances[key][i].instance, appExtension);
			}
		}
	}

	function mergeWndLocalModule(_module, _code, _instances, _moduleInstances)
	{
		// Iterate over all existing windows
		for (var i = 0; i < _moduleInstances.wnd.length; i++)
		{
			var wnd = _moduleInstances.wnd[i].window;

			// Create the window specific instance and
			// register it.
			var wndExtension = _code.call(egw, null, wnd);
			_moduleInstances.wnd[i].modules[_module] = wndExtension;

			// Extend all existing instances which are using
			// this window.
			for (var key in _instances)
			{
				for (var j = 0; j < _instances[key].length; j++)
				{
					if (_instances[key][j].window === wnd)
					{
						mergeObjects(_instances[key][j].instance,
								wndExtension);
					}
				}
			}
		}
	}

	/**
	 * Creates the egw object --- if the egw object should be created, some data
	 * has already been set inside the object by the egw_framework::header
	 * function and the instance has been marked as "prefsOnly".
	 */
	if (typeof window.egw != "undefined" && window.egw.prefsOnly)
	{
		// Rescue the old egw object
		var prefs = window.egw;
		delete prefs['prefsOnly'];

		/**
		 * Modules contains all currently loaded egw extension modules. A module
		 * is stored as an object of the following form:
		 * 	{
		 * 		name: <NAME OF THE OBJECT>,
		 * 		code: <REFERENCE TO THE MODULE FUNCTION>,
		 * 		flags: <MODULE FLAGS (local, global, etc.)
		 * 	}
		 */
		var modules = {};

		var moduleInstances = {
			'app': {},
			'wnd': [],
			'glo': {}
		};

		/**
		 * instances contains references to all created instances.
		 */
		var instances = {};

		/**
		 * Set a interval which is used to cleanup unused API instances all 10
		 * seconds.
		 */
		window.setInterval(function() {
			cleanupEgwInstances(instances, moduleInstances, function(w) {
				try {
					return w.window && w.window.closed;
				}
				catch(e) {
					// IE(11) seems to throw a permission denied error, when accessing closed property
					return true;
				}
			});
		}, 10000);

		/**
		 * The egw function returns an instance of the client side api. If no
		 * parameter is given, an egw istance, which is not bound to a certain
		 * application is returned.
		 * You may pass either an application name (as string) to the egw
		 * function and/or a window object. If you specify both, the app name
		 * has to preceed the window object reference. If no window object is
		 * given, the root window will be used.
		 *
		 * @return {egw}
		 */
		var egw = function() {

			// Get the window/app reference
			var _app = null;
			var _window = window;

			switch (arguments.length)
			{
				case 0:
					// Return the global instance
					return egw;

				case 1:
					if (typeof arguments[0] === 'string')
					{
						_app = arguments[0];
					}
					else if (typeof arguments[0] === 'object')
					{
						_window = arguments[0];
					}
					break;

				case 2:
					_app = arguments[0];
					_window = arguments[1];
					break;

				default:
					throw "Invalid count of parameters";
			}

			// Generate an API instance
			return getEgwInstance(egw, modules, moduleInstances, instances,
					_app, _window);
		};

		var globalEgw = {

			/**
			 * The MODULE_GLOBAL flag describes a module as global. A global
			 * module always works on the same data.
			 */
			MODULE_GLOBAL: 0,

			/**
			 * The MODULE_APP_LOCAL flag is used to describe a module as local
			 * for each application. Each time an api object is requested for
			 * another application, the complete module gets recreated.
			 */
			MODULE_APP_LOCAL: 1,

			/**
			 * The MODULE_WND_LOCAL flag is used to describe a module as local
			 * for each window. Each time an api object is requested for another
			 * window, the complete module gets recreated.
			 */
			MODULE_WND_LOCAL: 2,

			/**
			 * Name of the application the egw object belongs to.
			 */
			appName: null,

			/**
			 * Reference to the window this egw object belongs to.
			 */
			window: window,

			/**
			 * Returns the current application name. The current application
			 * name equals the name, which was given when calling the egw
			 * function. If the getAppName function is called on the global
			 * instance, 'etemplate' is returned.
			 */
			getAppName: function() {
				// Return the default application name if this function is
				// called on the global egw instance.
				if (!this.appName) {
					return 'etemplate';
				}

				// Otherwise return the correct application name.
				return this.appName;
			},

			/**
			 * The extend function can be used to extend the egw object.
			 *
			 * @param _module should be a string containing the name of the new
			 * 	module.
			 * @param _flags specifies whether the extension should be treated
			 * 	as a local or a global module. May be one of egw.MODULE_GLOBAL,
			 * 	MODULE_APP_LOCAL or MODULE_WND_LOCAL.
			 * @param _code should be a function, which returns an object that
			 * 	should extend the egw object.
			 */
			extend: function(_module, _flags, _code) {

				// Check whether that module is already registered
				if (typeof modules[_module] === 'undefined')
				{
					// Create a new module entry
					modules[_module] = {
						'code': _code,
						'flags': _flags,
						'name': _module
					};

					// Create new app/module specific instances for the new
					// module and merge the new module into all created
					// instances
					switch (_flags)
					{
						// Easiest case -- simply merge the extension into all
						// instances
						case egw.MODULE_GLOBAL:
							mergeGlobalModule(_module, _code, instances,
									moduleInstances);
							break;

						// Create new application specific instances and merge
						// those into all api instances for that application
						case egw.MODULE_APP_LOCAL:
							mergeAppLocalModule(_module, _code, instances,
									moduleInstances);
							break;


						// Create new window specific instances for each window
						// and merge those into all api instances for that
						// window
						case egw.MODULE_WND_LOCAL:
							mergeWndLocalModule(_module, _code, instances,
									moduleInstances);
							break;
					}
				}
			},

			/**
			 * Very similar to the egw function itself, but the module function
			 * returns just the functions exported by a single extension -- in
			 * this way extensions themselve are capable of accessing each
			 * others functions while they are being instanciated. Yet you
			 * should be carefull not to create any cyclic dependencies.
			 *
			 * @param _module is the name of the module
			 * @param _for may either be a string describing an application,
			 * 	an object referencing to a window or evaluate to false, in which
			 * 	case the global instance will be returned.
			 */
			module: function(_module, _for) {
				if (typeof modules[_module] !== 'undefined')
				{
					// Return the global instance of the module if _for
					// evaluates to false
					if (!_for)
					{
						return moduleInstances.glo[_module];
					}

					// Assume _for is an application name if it is a string.
					// Check whether the given application instance actually
					// exists.
					if (typeof _for === 'string'
					    && typeof moduleInstances.app[_for] !== 'undefined')
					{
						var mods = moduleInstances.app[_for];

						// Otherwise just instanciate the module if it has not
						// been created yet.
						if (typeof mods[_module] === 'undefined')
						{
							var mod = modules[_module];
							mods[_module] = mod.code.call(this, _app, window);
						}

						return mods[_module];
					}

					// If _for is an object, assume it is a window.
					if (typeof _for === 'object')
					{
						var mods = getExistingWndModules(moduleInstances, _for);

						// Check whether the module container for that window
						// has been found
						if (mods != null && typeof mods[_module] != 'undefined')
						{
							return mods[_module];
						}
						// If the given module has not been instanciated for
						// this window, instanciate it
						if (mods == null) mods = {};
						if (typeof mods[_module] === 'undefined')
						{
							var mod = modules[_module];
							mods[_module] = mod.code.call(this, null, _for);
						}
						return mods[_module];

					}
				}

				return null;
			},

			/**
			 * The "constant" function can be used to update a constant in all
			 * egw instances.
			 *
			 * @param _module is the module for which the constant should be set
			 * @param _name is the name of the constant
			 * @param _value is the value to which it should be set
			 * @param _window if set, updating the constant is restricted to
			 * 	those api instances which belong to the given window, if _window
			 * 	evaluates to false, all instances will be updated.
			 */
			constant: function(_module, _name, _value, _window) {
				// Update the module instances first
				for (var i = 0; i < moduleInstances.wnd.length; i++)
				{
					if (!_window || _window === moduleInstances.wnd[i].window)
					{
						moduleInstances.wnd[i].modules[_module][_name] = _value;
					}
				}

				// Now update all already instanciated instances
				for (var key in instances)
				{
					for (var i = 0; i < instances[key].length; i++)
					{
						if (!_window || _window === instances[key][i].window)
						{
							instances[key][i].instance[_name] = _value;
						}
					}
				}
			},

			dumpModules: function() {
				return modules;
			},

			dumpInstances: function() {
				return {
					'instances': instances,
					'moduleInstances': moduleInstances
				};
			}
		};

		// Merge the globalEgw functions into the egw object.
		mergeObjects(egw, globalEgw);

		// Merge the preferences into the egw object.
		mergeObjects(egw, prefs);

		// Create the entry for the root window in the module instances
		moduleInstances.wnd.push({
			'window': window,
			'modules': []
		});

		// Create the entry for the global window in the instances and register
		// the global instance there
		instances['~global~'] = [{
			'window': window,
			'instance': egw,
			'app': null
		}];

		// Publish the egw object
		this['egw'] = egw;
	}
}).call(window);