/**
 * eGroupWare eTemplate2
 *
 * @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
 * @copyright Stylite 2012
 * @version $Id$
 */

/*egw:uses
	egw_core;
	egw_debug;
*/
import './egw.js';
import './egw_json.js';	// for egw.registerJSONPlugin

/**
 * Module storing and updating row data
 *
 * @param {string} _app application name object is instanciated for
 * @param {object} _wnd window object is instanciated for
 */
egw.extend("data", egw.MODULE_APP_LOCAL, function (_app, _wnd)
{
	"use strict";

	/**
	 * How many UIDs we'll tell the server we know about.  No need to pass the whole list around.
	 */
	var KNOWN_UID_LIMIT = 200;

	/**
	 * Cache lifetime
	 *
	 * If cached results are used, we check their timestamp.  If the timestamp
	 * is older than this, we will also ask for fresh data.  For cached data
	 * younger than this, we only return the cache
	 *
	 * 29 seconds, 1 less then the fastest nextmatch autorefresh option
	 */
	var CACHE_LIFETIME = 29; // seconds

	/**
	 * Cached fetches are differentiated from actual results by using this prefix
	 * @type String
	 */
	var CACHE_KEY_PREFIX = 'cached_fetch_';

	var lastModification = null;

	/**
	 * cacheCallback stores callbacks that determine if data is placed
	 * into cacheStorage, or simply kept temporarily.  It is indexed
	 * by prefix.
	 *
	 * @type Array
	 */
	var cacheCallback = {};

	/**
	 * The uid function generates a session-unique id for the current
	 * application by appending the application name to the given uid.
	 *
	 * @param {string} _uid
	 * @param {string} _prefix
	 */
	function UID(_uid, _prefix)
	{
		_prefix = _prefix ? _prefix : _app;

		return _prefix + "::" + _uid;
	}

	/**
	 * Looks like too much data is cached.  Forget some.
	 *
	 * Tries to free up localStorage by removing the oldest cached data for the
	 * given prefix, but if none is found it will look at all cached data.
	 *
	 * @param {string} _prefix UID / application prefix
	 * @returns {Number} Number of cached recordsets removed, normally 1.
	 */
	function _clearCache(_prefix)
	{
		// Find cached items for the prefix, we prefer to expire just within the app
		var indexes = [];
		for(var i = 0; i < window.localStorage.length; i++)
		{
			var key = window.localStorage.key(i);

			// This is a cached fetch for many rows
			if(key.indexOf(CACHE_KEY_PREFIX+_prefix) == 0)
			{
				var cached = JSON.parse(window.localStorage.getItem(key));

				if(cached.lastModification)
				{
					indexes.push({
						key: key,
						lastModification: cached.lastModification
					});
				}
				else
				{
					// No way to know how old it is, just remove it
					window.localStorage.removeItem(key);
				}
			}
			// Actual cached data
			else if (key.indexOf(_prefix) == 0)
			{
				try {
					let cached = JSON.parse(window.localStorage.getItem(key));
					if(cached.timestamp)
					{
						indexes.push({
							key: key,
							lastModification: cached.timestamp
						});
					}
					else
					{
						// No way to know how old it is, just remove it
						window.localStorage.removeItem(key);
					}
				}
				catch (e) {
					window.localStorage.removeItem(key);
				}
			}
		}
		// Nothing for that prefix?  Clear all cached data.
		if(_prefix && indexes.length == 0)
		{
			return _clearCache('');
		}
		// Found some cached for that prefix, only remove the oldest
		else if (indexes.length > 0)
		{
			indexes.sort(function(a,b) {
				if(a.lastModification < b.lastModification) return 1;
				if(a.lastModification > b.lastModification) return -1;
				return 0;
			});
			window.localStorage.removeItem(indexes.pop().key);
			return 1;
		}
		return indexes.length;
	}

	function parseServerResponse(_result, _callback, _context, _execId, _widgetId)
	{
		// Check whether the result is valid
		// This result is not for us, quietly return
		if(_result && typeof _result.type != 'undefined') return;

		// "result" has to be an object consisting of "order" and "data"
		if (!(_result && typeof _result.order !== "undefined"
		    && typeof _result.data !== "undefined"))
		{
			egw.debug("error", "Invalid result for 'dataFetch'");
		}

		if (_result.lastModification)
		{
			lastModification = _result.lastModification;
		}

		if (_result.order && _result.data)
		{
			// Assemble the correct order uids
			if(!(_result.order.length && _result.order[0] && _result.order[0].indexOf && _result.order[0].indexOf(_context.prefix) == 0))
			{
				for (var i = 0; i < _result.order.length; i++)
				{
					_result.order[i] = UID(_result.order[i], _context.prefix);
				}
			}

			// Load all data entries that have been sent or delete them
			for (var key in _result.data)
			{
				let uid = UID(key, (typeof _context == "object" && _context != null) ?_context.prefix : "");
				if (_result.data[key] === null &&
				(
					typeof _context.refresh == "undefined" || _context.refresh && !jQuery.inArray(key,_context.refresh)
				))
				{
					egw.dataDeleteUID(uid);
				}
				else
				{
					egw.dataStoreUID(uid, _result.data[key]);
				}
			}

			// Check if we tried to refresh a specific row and didn't get it, so set it to null
			// (triggers update for listeners), then remove it
			if(typeof _context == "object" && _context.refresh)
			{
				for(let i = 0; i < _context.refresh.length; i++)
				{
					let uid = UID(_context.refresh[i], _context.prefix);
					if(_result.order.indexOf(uid) >= 0)
					{
						continue;
					}
					egw.dataStoreUID(uid, null);
					egw.dataDeleteUID(uid);
				}
			}

			// Check to see if we need long-term caching of the query and its results
			if(window.localStorage && _context.prefix && cacheCallback[_context.prefix]  && !_context.no_cache)
			{
				// Ask registered callbacks if we should cache this
				for(var i = 0; i < cacheCallback[_context.prefix].length; i++)
				{
					var cc = cacheCallback[_context.prefix][i];
					var cache_key = cc.callback.call(cc.context, _context);
					if(cache_key)
					{
						cache_key = CACHE_KEY_PREFIX + _context.prefix + '::' + cache_key;
						try
						{
							for (var key in _result.data)
							{
								var uid = UID(key, (typeof _context == "object" && _context != null) ? _context.prefix : "");

								// Register a handler on each data so we can know if it is updated or removed
								egw.dataUnregisterUID(uid, null, cache_key);
								egw.dataRegisterUID(uid, function(data, _uid) {
									// If data item is removed, remove it from cached fetch too
									if(data == null)
									{
										var cached = JSON.parse(window.localStorage[this]) || false;
										if(cached && cached.order && cached.order.indexOf(_uid) >= 0)
										{
											cached.order.splice(cached.order.indexOf(_uid),1);
											if(cached.total) cached.total--;
											window.localStorage[this] = JSON.stringify(cached);
										}
										window.localStorage.removeItem(_uid);
									}
									else
									{
										// Update or store data in long-term storage
										window.localStorage[_uid] = JSON.stringify({timestamp: (new Date).getTime(), data: data});
									}
								}, cache_key, _execId, _widgetId);
							}
							// Don't keep data in long-term cache with request also
							_result.data = {};
							window.localStorage.setItem(cache_key,JSON.stringify(_result));
						}
						catch (e)
						{
							// Maybe ran out of space?  Free some up.
							if(e.name == 'QuotaExceededError'	// storage quota is exceeded, remove cached data
								|| e.name == 'NS_ERROR_DOM_QUOTA_REACHED')	// FF-name
							{
								var count = _clearCache(_context.prefix);
								egw.debug('info', 'localStorage full, removed ' + count + ' stored datasets');
							}
							// No, something worse happened
							else
							{
								egw.debug('warning', 'Tried to cache some data.  It did not work.', cache_key, e);
							}
						}
					}
				}
			}

			// Call the callback function and pass the calculated "order" array
			// as well as the "total" count and the "timestamp" to the listener.
			if (_callback)
			{
				_callback.call(_context, {
					"order": _result.order,
					"total": parseInt(_result.total),
					"readonlys": _result.readonlys,
					"rows": _result.rows,
					"lastModification": lastModification
				});
			}
		}
	}

	return {

		/**
		 * The dataFetch function provides an abstraction layer for the
		 * corresponding "EGroupware\Api\Etemplate\Widget\Nextmatch::ajax_get_rows" function.
		 * The server returns the following structure:
		 * 	{
		 * 		order: [uid, ...],
		 * 		data:
		 * 			{
		 * 				uid0: data,
		 * 				...
		 * 				uidN: data
		 * 			},
		 * 		total: <TOTAL COUNT>,
		 * 		lastModification: <LAST MODIFICATION TIMESTAMP>,
		 * 		readonlys: <READONLYS>
		 * 	}
		 * If a uid got deleted on the server above data is null.
		 * If a uid is omitted from data, is has not changed since lastModification.
		 *
		 * If order/data is null, this means that nothing has changed for the
		 * given range.
		 * The dataFetch function stores new data for the uid's inside the
		 * local data storage, the grid views are then capable of querying the
		 * data for those uids from the local storage using the
		 * "dataRegisterUID" function.
		 *
		 * @param _execId is the execution context of the etemplate instance
		 * 	you're querying the data for.
		 * @param _queriedRange is an object of the following form:
		 * 	{
		 * 		start: <START INDEX>,
		 * 		num_rows: <COUNT OF ENTRIES>
		 * 	}
		 * The range always corresponds to the given filter settings.
		 * @param _filters contains the filter settings. The filter settings are
		 * 	those which are crucial for the mapping between index and uid.
		 * @param _widgetId id with full namespace of widget
		 * @param _callback is the function that should get called, once the data
		 * 	is available. The data passed to the callback function has the
		 * 	following form:
		 * 	{
		 * 		order: [uid, ...],
		 * 		total: <TOTAL COUNT>,
		 * 		lastModification: <LAST MODIFICATION TIMESTAMP>,
		 * 		readonlys: <READONLYS>
		 * 	}
		 * 	Please note that the "uids" comming from the server and the ones
		 * 	being parsed to the callback function differ. While the uids
		 * 	which are returned from the server are only unique inside the
		 * 	application, the uids which are used on the client are "globally"
		 * 	unique.
		 * @param _context is the context in which the callback function will get
		 * 	called.
		 * @param _knownUids is an array of uids already known to the client.
		 *  This parameter may be null in order to indicate that the client
		 *  currently has no data for the given filter settings.
		 */
		dataFetch: function (_execId, _queriedRange, _filters, _widgetId,
				_callback, _context, _knownUids)
		{
			var lm = lastModification;
			if(typeof _context.lastModification != "undefined") lm = _context.lastModification;

			if (_queriedRange["no_data"])
			{
				lm = 0xFFFFFFFFFFFF;
			}
			else if (_queriedRange["only_data"])
			{
				lm = 0;
			}

			// Store refresh in context to not delete the other entries when server only returns these
			if (typeof _queriedRange.refresh != "undefined")
			{
				if(typeof _queriedRange.refresh == "string")
				{
					_context.refresh = [_queriedRange.refresh];
				}
				else
				{
					_context.refresh = _queriedRange.refresh;
				}
			}

			// Limit the amount of UIDs we say we know about to a sensible number, in case user is enjoying auto-pagination
			var knownUids = _knownUids ? _knownUids : egw.dataKnownUIDs(_context.prefix ? _context.prefix : _app);
			if(knownUids > KNOWN_UID_LIMIT)
			{
				knownUids.slice(typeof _queriedRange.start != "undefined" ? _queriedRange.start:0,KNOWN_UID_LIMIT);
			}

			// Check to see if we have long-term caching of the query and its results
			if(window.localStorage && _context.prefix && cacheCallback[_context.prefix])
			{
				// Ask registered callbacks if we should cache this
				for(var i = 0; i < cacheCallback[_context.prefix].length; i++)
				{
					var cc = cacheCallback[_context.prefix][i];
					var cache_key = cc.callback.call(cc.context, _context);
					if(cache_key)
					{
						cache_key = CACHE_KEY_PREFIX + _context.prefix + '::' + cache_key;

						var cached = window.localStorage.getItem(cache_key);
						if(cached)
						{
							cached = JSON.parse(cached);
							var needs_update = true;

							// Check timestamp
							if(cached.lastModification && ((Date.now()/1000) - cached.lastModification) < CACHE_LIFETIME)
							{
								needs_update = false;
							}

							egw.debug('log', 'Data cached query from ' + new Date(cached.lastModification*1000)+': ' + cache_key + '('+
								(needs_update ? 'will be' : 'will not be')+" updated)\nprocessing...");

							// Call right away with cached data, but set no_cache flag
							// to avoid re-caching this data with a new timestamp.
							// We may still ask the server though.
							var no_cache = _context.no_cache;
							_context.no_cache = true;
							parseServerResponse(cached, _callback, _context, _execId, _widgetId);
							_context.no_cache = no_cache;


							// If cache registrant wants notification of cache useage,
							// let it know
							if(cc.notification)
							{
								cc.notification.call(cc.context, needs_update);
							}

							if(!needs_update)
							{
								// Cached data is new enough, skip the server call
								return;
							}
						}
					}
				}
			}
			// create a clone of filters, which can be used in parseServerResponse and cache callbacks
			// independent of changes happening while waiting for the response
			_context.filters = jQuery.extend({}, _filters);
			var request = egw.json(
				"EGroupware\\Api\\Etemplate\\Widget\\Nextmatch::ajax_get_rows",
				[
					_execId,
					_queriedRange,
					_filters,
					_widgetId,
					knownUids,
					lm
				],
				function(result) {
					parseServerResponse(result, _callback, _context, _execId, _widgetId);
				},
				this,
				true
			);
			request.sendRequest();
		},

		/**
		 * Turn on long-term client side cache of a particular request
		 * (cache the nextmatch query results) for fast, immediate response
		 * with old data.
		 *
		 * The request is still sent to the server, and the cache is updated
		 * with fresh data, and any needed callbacks are called again with
		 * the fresh data.
		 *
		 * @param {string} prefix UID / Application prefix should match the
		 *	individual record prefix
		 * @param {function} callback_function A function that will analize the provided fetch
		 *	parameters and return a reproducable cache key, or false to not cache
		 *	the request.
		 * @param {function} notice_function A function that will be called whenever
		 *	cached data is used.  It is passed one parameter, a boolean that indicates
		 *	if the server is or will be queried to refresh the cache.  Do not fetch additional data
		 *	inside this callback, and return quickly.
		 * @param {object} context Context for callback function.
		 */
		dataCacheRegister: function(prefix, callback_function, notice_function, context)
		{
			if(typeof cacheCallback[prefix] == 'undefined')
			{
				cacheCallback[prefix] = [];
			}
			cacheCallback[prefix].push({
				callback: callback_function,
				notification: notice_function || false,
				context: context || null
			});
		},

		/**
		 * Unregister a previously registered cache callback
		 * @param {string} prefix UID / Application prefix should match the
		 *	individual record prefix
		 * @param {function} [callback] Callback function to un-register.  If
		 *	omitted, all functions for the prefix will be removed.
		 */
		dataCacheUnregister: function(prefix, callback)
		{
			if(typeof callback != 'undefined')
			{
				for(var i = 0; i < cacheCallback[prefix].length; i++)
				{
					if(cacheCallback[prefix][i].callback == callback)
					{
						cacheCallback[prefix].splice(i,1);
						return;
					}
				}
			}
			// Callback not provided or not found, reset by prefix
			cacheCallback[prefix] = [];
		}
	};

});

egw.extend("data_storage", egw.MODULE_GLOBAL, function (_app, _wnd) {

	/**
	 * The localStorage object is used to store the data for certain uids. An
	 * entry inside the localStorage object looks like the following:
	 * 	{
	 * 		timestamp: <CREATION TIMESTAMP (local)>,
	 * 		data: <DATA>
	 * 	}
	 */
	var localStorage = {};

	/**
	 * The registeredCallbacks map is used to store all callbacks registerd for
	 * a certain uid.
	 */
	var registeredCallbacks = {};



	/**
	 * Register the "data" plugin globally for single uids
	 * Multiple UIDs such as nextmatch results are still handled by egw.data
	 * using dataFetch() && parseServerResponse(), above.  Both update the
	 * GLOBAL data cache though this one is registered globally, and the above
	 * is registered app local.
	 *
	 * @param {string} type
	 * @param {object} res
	 * @param {object} req
	 * @returns {Boolean}
	 */
	egw.registerJSONPlugin(function(type, res, req) {
		if ((typeof res.data.uid != 'undefined') &&
			(typeof res.data.data != 'undefined'))
		{
			// Store it, which will call all registered listeners
			this.dataStoreUID(res.data.uid, res.data.data);
			return true;
		}
	}, egw, 'data',true);

	/**
	 * Uids and timers used for querying data uids, hashed by the first few
	 * bytes of the _execId, stored as an object of the form
	 * {
	 *     "timer": <QUEUE TIMER>,
	 *     "uids": <ARRAY OF UIDS>
	 * }
	 */
	var queue = {};

	/**
	 * Contains the queue timeout in milliseconds.
	 */
	var QUEUE_TIMEOUT = 10;

	/**
	 * This constant specifies the maximum age of entries in the local storrage
	 * in milliseconds
	 */
	var MAX_AGE = 5 * 60 * 1000; // 5 mins

	/**
	 * This constant specifies the interval in which the local storage gets
	 * cleaned up.
	 */
	var CLEANUP_INTERVAL = 30 * 1000; // 30 sec

	/**
	 * Register a cleanup function, which throws away all data entries which are
	 * older than the given age.
	 */
	_wnd.setInterval(function() {
		// Get the current timestamp
		var time = (new Date).getTime();

		// Iterate over the local storage
		for (var uid in localStorage)
		{
			// Expire old data, if there are no callbacks
			if (time - localStorage[uid].timestamp > MAX_AGE && typeof registeredCallbacks[uid] == "undefined")
			{
				// Unregister all registered callbacks for that uid
				egw.dataUnregisterUID(uid);

				// Delete the data from the localStorage
				delete localStorage[uid];

				// We don't clean long-term storage because of age until it runs
				// out of space
			}
		}
	}, CLEANUP_INTERVAL);

	return {

		/**
		 * Registers the intrest in a certain uid for a callback function. If
		 * the data for that uid changes or gets loaded, the given callback
		 * function is called. If the data for the given uid is available at the
		 * time of registering the callback, the callback is called immediately.
		 *
		 * @param _uid is the uid for which the callback should be registered.
		 * @param _callback is the callback which should get called.
		 * @param _context is the optional context in which the callback will be
		 * executed
		 * @param _execId is the exec id which will be used in case the data is
		 * not available
		 * @param _widgetId is the widget id which will be used in case the uid
		 * has to be fetched.
		 */
		dataRegisterUID: function (_uid, _callback, _context, _execId, _widgetId) {
			// Create the slot for the uid if it does not exist now
			if (typeof registeredCallbacks[_uid] === "undefined")
			{
				registeredCallbacks[_uid] = [];
			}

			// Store the given callback
			registeredCallbacks[_uid].push({
				"callback": _callback,
				"context": _context ? _context : null,
				"execId": _execId,
				"widgetId" : _widgetId
			});

			// Check whether the data is available -- if yes, immediately call
			// back the callback function
			if (typeof localStorage[_uid] !== "undefined")
			{
				// Update the timestamp and call the given callback function
				localStorage[_uid].timestamp = (new Date).getTime();
				_callback.call(_context, localStorage[_uid].data, _uid);
			}
			// Check long-term storage
			else if(window.localStorage && window.localStorage[_uid])
			{
				localStorage[_uid] = JSON.parse(window.localStorage[_uid]);
				_callback.call(_context, localStorage[_uid].data, _uid);
			}
			else if (_execId && _widgetId)
			{
				// Get the first 50 bytes of the exex id
				var hash = _execId.substring(0, 50);

				// Create a new queue if it does not exist yet
				if (typeof queue[hash] === "undefined")
				{
					var self = this;
					queue[hash] = {"uids": [], "timer": null};
					queue[hash].timer = window.setTimeout(function () {
						// Fetch the data
						self.dataFetch(_execId, {
								"start": 0,
								"num_rows": 0,
								"only_data": true,
								"refresh": queue[hash].uids
							},
							[], _widgetId, null, _context, null);

						// Delete the queue entry
						delete queue[hash];
					}, 100);
				}

				// Push the uid onto the queue, removing the prefix
				var parts = _uid.split("::");
				parts.shift();
				if (queue[hash].uids.indexOf(parts.join("::")) === -1)
				{
					queue[hash].uids.push(parts.join('::'));
				}
			}
			else
			{
				this.debug("log", "Data for uid " + _uid + " not available.");
			}
		},

		/**
		 * Unregisters the intrest of updates for a certain data uid.
		 *
		 * @param _uid is the data uid for which the callbacks should be
		 * 	unregistered.
		 * @param _callback specifies the specific callback that should be
		 * 	unregistered. If it evaluates to false, all callbacks (or those
		 * 	matching the optionally given context) are removed.
		 * @param _context specifies the callback context that should be
		 * 	unregistered. If it evaluates to false, all callbacks (or those
		 * 	matching the optionally given callback function) are removed.
		 */
		dataUnregisterUID: function (_uid, _callback, _context) {

			// Force the optional parameters to be exactly null
			_callback = _callback ? _callback : null;
			_context = _context ? _context : null;

			if (typeof registeredCallbacks[_uid] !== "undefined")
			{

				// Iterate over the registered callbacks for that uid and delete
				// all callbacks pointing to the given callback and context
				for (var i = registeredCallbacks[_uid].length - 1; i >= 0; i--)
				{
					if ((!_callback || registeredCallbacks[_uid][i].callback === _callback)
					    && (!_context || registeredCallbacks[_uid][i].context === _context))
					{
						registeredCallbacks[_uid].splice(i, 1);
					}
				}

				// Delete the slot if no callback is left for the uid
				if (registeredCallbacks[_uid].length === 0)
				{
					delete registeredCallbacks[_uid];
				}
			}
		},

		/**
		 * Returns whether data is available for the given uid.
		 *
		 * @param _uid is the uid for which should be checked whether it has some
		 * 	data.
		 */
		dataHasUID: function (_uid) {
			return typeof localStorage[_uid] !== "undefined";
		},

		/**
		 * Returns data of a given uid.
		 *
		 * @param _uid is the uid for which should be checked whether it has some
		 * 	data.
		 */
		dataGetUIDdata: function (_uid) {
			return localStorage[_uid];
		},

		/**
		 * Returns all uids that have the given prefix
		 *
		 * @param {string} _prefix
		 * @return {array}
		 * TODO: Improve this
		 */
		dataKnownUIDs: function (_prefix) {

			var result = [];

			for (var key in localStorage)
			{
				var parts = key.split("::");
				if (parts.shift() === _prefix && localStorage[key].data)
				{

					result.push(parts.join('::'));
				}
			}

			return result;
		},

		/**
		 * Stores data for the uid and calls all callback functions registered
		 * for that uid.
		 *
		 * @param _uid is the uid for which the data should be saved.
		 * @param _data is the data which should be saved.
		 */
		dataStoreUID: function (_uid, _data) {
			// Get the current unix timestamp
			var timestamp = (new Date).getTime();

			// Store the data in the local storage
			localStorage[_uid] = {
				"timestamp": timestamp,
				"data": _data
			};

			// Inform all registered callback functions and pass the data to
			// those.
			if (typeof registeredCallbacks[_uid] != "undefined")
			{
				for (var i = registeredCallbacks[_uid].length - 1; i >= 0; i--)
				{
					try {
						registeredCallbacks[_uid][i].callback.call(
							registeredCallbacks[_uid][i].context,
							_data,
							_uid
						);
					} catch (e) {
						// Remove this callback from the list
						if(typeof registeredCallbacks[_uid] != "undefined")
						{
							registeredCallbacks[_uid].splice(i, 1);
						}
					}
				}
			}
		},

		/**
		 * Deletes the data for a certain uid from the local storage and
		 * unregisters all callback functions associated to it.
		 *
		 * This does NOT update nextmatch!
		 * Application code should use: egw(window).refresh(msg, app, id, "delete");
		 *
		 * @param _uid is the uid which should be deleted.
		 */
		dataDeleteUID: function (_uid) {
			if (typeof localStorage[_uid] !== "undefined")
			{
				// Delete the element from the local storage
				delete localStorage[_uid];

				// Unregister all callbacks for that uid
				this.dataUnregisterUID(_uid);
			}
		},

		/**
		 * Force a refreash of the given uid from the server if known, and
		 * calls all associated callbacks.
		 *
		 * If the UID does not have any registered callbacks, it cannot be refreshed because the required
		 * execID and context are missing.
		 *
		 * @param {string} _uid is the uid which should be refreshed.
		 * @return {boolean} True if the uid is known and can be refreshed, false if unknown and will not be refreshed
		 */
		dataRefreshUID: function (_uid) {
			if (typeof localStorage[_uid] === "undefined") return false;

			if(typeof registeredCallbacks[_uid] !== "undefined" && registeredCallbacks[_uid].length > 0)
			{
				var _execId = registeredCallbacks[_uid][0].execId;
				// This widget ID MUST be a nextmatch, because the data call is to Etemplate\Widget\Nexmatch
				var nextmatchId = registeredCallbacks[_uid][0].widgetId;
				var uid = _uid.split("::");
				var context = {
					"prefix":uid.shift()
				};
				uid = uid.join("::");

				// find filters, even if context is not always from nextmatch, eg. caching uses it's a string context
				var filters = {};
				for(var i=0; i < registeredCallbacks[_uid].length; i++)
				{
					var callback = registeredCallbacks[_uid][i];
					if (typeof callback.context == 'object' &&
						typeof callback.context.self == 'object' &&
						typeof callback.context.self._filters == 'object')
					{
						filters = callback.context.self._filters;
						break;
					}
				}

				// need to send nextmatch filters too, as server-side will merge old version from request otherwise
				this.dataFetch(_execId, {'refresh':uid}, filters, nextmatchId, false, context, [uid]);

				return true;
			}
			return false;
		},

		/**
		 * Search for exact UID string or regular expression and return widgets using it
		 *
		 * @param {string|RegExp} _uid is the uid which should be refreshed.
		 * @return {object} UID: array of (nextmatch-)wigetIds
		 */
		dataSearchUIDs: function(_uid)
		{
			var matches = {};
			var f = function(_uid)
			{
				if (typeof matches[_uid] == "undefined")
				{
					matches[_uid] = [];
				}
				if (typeof registeredCallbacks[_uid] !== "undefined")
				{
					for(var n=0; n < registeredCallbacks[_uid].length; ++n)
					{
						var callback = registeredCallbacks[_uid][n];
						if (typeof callback.context != "undefined" &&
							typeof callback.context.self != "undefined" &&
							typeof callback.context.self._widget != "undefined")
						{
							matches[_uid].push(callback.context.self._widget);
						}
					}
				}
			};
			if (typeof _uid == "object" && _uid.constructor.name == "RegExp")
			{
				for(var uid in localStorage)
				{
					if (_uid.test(uid))
					{
						f(uid);
					}
				}
			}
			else if (typeof localStorage[_uid] != "undefined")
			{
				f(_uid);
			}
			return matches;
		},

		/**
		 * Search for exact UID string or regular expression and call registered (nextmatch-)widgets refresh function with given _type
		 *
		 * This method is preferable over dataRefreshUID for app code, as it takes care of things like counters too.
		 *
		 * It does not do anything for _type="add"!
		 *
		 * @param {string|RegExp) _uid is the uid which should be refreshed.
		 * @param {string} _type "delete", "edit", "update", not useful for "add"!
		 * @return {array} (nextmatch-)wigets refreshed
		 */
		dataRefreshUIDs: function(_uid, _type)
		{
			var uids = this.dataSearchUIDs(_uid);
			var widgets = [];
			var uids4widget = [];
			for(var uid in uids)
			{
				for(var n=0; n < uids[uid].length; ++n)
				{
					var widget = uids[uid][n];
					var idx = widgets.indexOf(widget);
					if (idx == -1)
					{
						widgets.push(widget);
						idx = widgets.length-1;
					}
					// uids for nextmatch.refesh do NOT contain the prefix
					var nm_uid = uid.replace(RegExp('^'+widget.controller.dataStorePrefix+'::'), '');
					if (typeof uids4widget[idx] == "undefined")
					{
						uids4widget[idx] = [nm_uid];
					}
					else
					{
						uids4widget[idx].push(nm_uid);
					}
				}
			}
			for(var w=0; w < widgets.length; ++w)
			{
				widgets[w].refresh(uids4widget[w], _type);
			}
			return widgets;
		}
	};
});