/**
 * EGroupware eTemplate2
 *
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
 * @package etemplate
 * @subpackage dataview
 * @link https://www.egroupware.org
 * @author Andreas Stöckel
 * @copyright EGroupware GmbH 2011-2021
 */

/*egw:uses
	et2_core_common;
	et2_core_inheritance;

	et2_dataview_interfaces;
	et2_dataview_controller_selection;
	et2_dataview_view_row;
	et2_dataview_view_tile;

	egw_action.egw_action;
*/

import {et2_IDataProvider} from "./et2_dataview_interfaces";
import {et2_dataview_selectionManager} from "./et2_dataview_controller_selection";
import {et2_dataview_row} from "./et2_dataview_view_row";
import {et2_dataview_grid} from "./et2_dataview_view_grid";
import {et2_arrayIntKeys, et2_bounds} from "./et2_core_common";
import {egw} from "../jsapi/egw_global";
import {egwBitIsSet} from "../egw_action/egw_action_common";
import {EGW_AO_STATE_NORMAL, EGW_AO_STATE_SELECTED} from "../egw_action/egw_action_constants";

/**
 * The fetch timeout specifies the time during which the controller tries to
 * consolidate requests for rows.
 */
export const ET2_DATAVIEW_FETCH_TIMEOUT = 50;

export const ET2_DATAVIEW_STEPSIZE = 50;

/**
 * The et2_dataview_controller class is the intermediate layer between a grid
 * instance and the corresponding data source. It manages updating the grid,
 * as well as inserting and deleting rows.
 */
export class et2_dataview_controller
{
	// Maximum concurrent data requests.  Additional ones are held in the queue.
	public static readonly CONCURRENT_REQUESTS = 5;

	protected _parentController: any;
	protected _grid: et2_dataview_grid;
	protected dataStorePrefix: any;
	private _dataProvider: any;
	protected _rowCallback: any;
	protected _linkCallback: any;
	private _context: any;

	protected _children: any[];
	protected _indexMap: any = {};

	private _queueTimer: number;
	private _lastModification: number;
	private _queue: {};
	private _request_queue: any[];
	protected _selectionMgr: et2_dataview_selectionManager;

	protected _objectManager: any;
	private hasData: boolean;

	/**
	 * Constructor of the et2_dataview_controller, connects to the grid
	 * callback.
	 *
	 * @param _grid is the grid the controller should controll.
	 * @param _rowCallback is the callback function that gets called when a row
	 * is requested.
	 * @param _linkCallback is the callback function that gets called for
	 * requesting action links for a row. The row data, the index of the row and
	 * the uid are passed as parameters to the function.
	 * uid is passed to the function.
	 * @param _actionObjectManager is the object that manages the action
	 * objects.
	 */
	constructor (_parentController, _grid)
	{
		// Copy the given arguments
		this._parentController = _parentController;
		this._grid = _grid;

		// Initialize list of child controllers
		this._children = [];

		// Initialize the "index map" which contains all currently displayed
		// containers hashed by the "index"
		this._indexMap = {};

		// Timer used for queing fetch requests
		this._queueTimer = null;

		// Array which contains all currently queued row indices in the form of
		// an associative array
		this._queue = {};

		// Current concurrent requests we have
		this._request_queue = [];

		// Register the dataFetch callback
		this._grid.setDataCallback(this._gridCallback, this);


		// Record the child
		if(this._parentController != null)
		{
			this._parentController._children.push(this);
		}
	}

	destroy( )
	{

		// Destroy the selection manager
		this._selectionMgr.destroy();

		// Clear the selection timeout
		this._clearTimer();

		// Remove the child from the child list
		if(this._parentController != null)
		{
			var idx = this._parentController._children.indexOf(this);

			if (idx >= 0)
			{
				// This element is no longer parent of the child
				this._parentController._children.splice(idx, 1);
				this._parentController = null;
			}
		}

		this._grid = null;
	}

	/**
	 * @param value is an object implementing the et2_IDataProvider
	 * interface
	 */
	setDataProvider(value: et2_IDataProvider)
	{
		this._dataProvider = value;
	}

	setRowCallback(value: any) {
		this._rowCallback = value;
	}

	setLinkCallback(value: any) {
		this._linkCallback = value;
	}

	/**
	 * @param value is the context in which the _rowCallback and the
	 * _linkCallback are called.
	 */
	setContext(value: any) {
		this._context = value;
	}

	setActionObjectManager(_actionObjectManager: any)
	{
		if(this._selectionMgr)
		{
			this._selectionMgr.destroy();
		}

		// Create the selection manager
		this._selectionMgr = new et2_dataview_selectionManager(
				this._parentController ? this._parentController._selectionMgr : null,
				this._indexMap,
				_actionObjectManager,
				this._selectionFetchRange,
				this._makeIndexVisible,
				this
		);
	}

	/**
	 * The update function queries the server for changes in the currently
	 * managed index range -- those changes are then merged into the current
	 * view without a complete rebuild of every row.
	 *
	 * @param {boolean} clear Skip the fancy stuff, dump everything and start again.
	 *		Completely clears the grid and selection.
	 */
	update( clear? : boolean)
	{
		// Avoid update after destroy
		// Happens sometimes if AJAX response comes after etemplate unload
		if(!this._grid) return;

		// ---------

		// TODO: Actually stuff here should be done if the server responds that
		// there at all were some changes (needs implementation of "refresh")

		// Tell the grid not to try and update itself while we do this
		this._grid.doInvalidate = false;

		if(clear)
		{
			// Scroll to top
			this._grid.makeIndexVisible(0);
			this._grid.clear();

			// Free selection manager
			this._selectionMgr.clear();

			// Clear object manager
			this._objectManager.clear();

			// Clear the map
			this._indexMap = {};
			// Update selection manager, it uses this by reference
			this._selectionMgr.setIndexMap(this._indexMap);

			// Clear the queue
			this._queue = {};

			// Invalidate the change detection, re-fetches any known rows
			this._lastModification = 0;
		}
		// Remove all rows which are outside the view range
		this._grid.cleanup();

		// Get the currently visible range from the grid
		var range = this._grid.getIndexRange();

		// Force range.top and range.bottom to contain an integer
		if (range.top === false)
		{
			range.top = range.bottom = 0;
		}
		this._request_queue = [];

		// Require that range from the server
		this._queueFetch(et2_bounds(range.top, clear ? 0 : range.bottom + 1), 0, true);
	}

	/**
	 * Rebuilds the complete grid.
	 */
	reset( )
	{
		// Throw away all internal mappings and reset the timestamp
		this._indexMap = {};
		// Update selection manager, it uses this by reference
		this._selectionMgr.setIndexMap(this._indexMap);

		// Clear the grid
		this._grid.clear();

		// Clear the row queue
		this._queue = {};

		// Reset the request queue
		this._request_queue = [];

		// Update the data
		this.update();
	}

	/**
	 * Loads the initial order. Do not call multiple times.
	 */
	loadInitialOrder( order)
	{
		for (var i = 0; i < order.length; i++)
		{
			this._getIndexEntry(i).uid = order[i];
		}
	}

	/**
	 * Load initial data
	 *
	 * @param {string} uid_key Name of the unique row identifier field
	 * @param {Object} data Key / Value mapping of initial data.
	 */
	loadInitialData( uid_prefix, uid_key, data)
	{
		var idx = 0;
		for(var key in data)
		{
			// Skip any extra keys
			if(typeof data[key] != "object" || data[key] == null || typeof data[key][uid_key] == "undefined") continue;

			// Add to row / uid map
			var entry = this._getIndexEntry(idx++);
			entry.uid = data[key][uid_key]+"";
			if(entry.uid.indexOf(uid_prefix) < 0)
			{
				entry.uid = uid_prefix + "::" + entry.uid;
			}

			// Add to data cache so grid will find it
			egw.dataStoreUID(entry.uid, data[key])

			// Don't try to insert the rows, grid will do that automatically
		}
		if(idx == 0)
		{
			// No rows, start with an empty
			this._selectionMgr.clear();
			this._emptyRow(this._grid._total == 0);
		}
	}

	/**
	 * Returns the depth of the controller instance.
	 */
	getDepth( )
	{

		if (this._parentController)
		{
			return this._parentController.getDepth() + 1;
		}

		return 0;
	}

	/**
	 * Set the data cache prefix
	 * The default is to use appname, but if you need to set it explicitly to
	 * something else to avoid conflicts.  Use the same prefix everywhere for
	 * each type of data.  eg. infolog for infolog entries, even if accessed via addressbook
	 */
	setPrefix( prefix)
	{
		this.dataStorePrefix = prefix;
	}

	/**
	 * Returns the row information of the passed node, or null if not available
	 *
	 * @param {DOMNode} node
	 * @return {string|false} UID, or false if not found
	 */
	getRowByNode( node)
	{
		// Whatever the node, find a TR
		var row_node = jQuery(node).closest('tr');
		var row = null;

		// Check index map - simple case
		var indexed = this._getIndexEntry(row_node.index());
		if(indexed && indexed.row && indexed.row.getDOMNode() == row_node[0])
		{
			row = indexed;
		}
		else
		{
			// Check whole index map
			for(var index in this._indexMap)
			{
				indexed = this._indexMap[index];
				if( indexed && indexed.row && indexed.row.getDOMNode() == row_node[0])
				{
					row = indexed;
					break;
				}
			}
		}

		// Check children
		for(var i = 0; !row && i < this._children.length; i++)
		{
			var child_row = this._children[i].getRowByNode(node);
			if(child_row !== false) row = child_row;
		}
		if(row && !row.controller)
		{
			row.controller = this;
		}
		return row;
	}

	/**
	 * Returns the current "total" count.
	 */
	getTotalCount() : number
	{
		return this._grid.getTotalCount();
	}

	/* -- PRIVATE FUNCTIONS -- */


	_getIndexEntry( _idx)
	{
		// Create an entry in the index map if it does not exist yet
		if (typeof this._indexMap[_idx] === "undefined")
		{
			this._indexMap[_idx] = {
				"row": null,
				"uid": null
			};
		}

		// Always update the index of the entries before returning them. This is
		// neccessary, as when we remove the uid from an entry without row, its
		// index does not get updated any further
		this._indexMap[_idx]["idx"] = _idx;

		return this._indexMap[_idx];
	}

	/**
	 * Inserts a new data row into the grid. index and uid are derived from the
	 * given management entry. If the data for the given uid does not exist yet,
	 * a "loading" placeholder will be shown instead. The function will do
	 * nothing if there already is a row associated to the entry. This function
	 * will not re-insert a row if the entry already had a row.
	 *
	 * @param _entry is the management entry for the index the row will be
	 * displayed at.
	 * @param _update specifies whether the row should be updated if _entry.row
	 * already exists.
	 * @return true, if all data for the row has been available, false
	 * otherwise.
	 */
	_insertDataRow( _entry, _update)
	{
		// Abort if the entry already has a row but the _insert flag is not set
		if (_entry.row && !_update)
		{
			return true;
		}

		// Context used for the callback functions
		var ctx = {"self": this, "entry": _entry};

		// Create a new row instance, if it does not exist yet
		var createdRow = false;
		if (!_entry.row)
		{
			createdRow = true;
			_entry.row = this._createRow(ctx);
			_entry.row.setDestroyCallback(this._destroyCallback, ctx);
		}

		// Load the row data if we have a uid for the entry
		this.hasData = false; // Gets updated by the _dataCallback
		if (_entry.uid)
		{
			// Register the callback / immediately load the data
			this._dataProvider.dataRegisterUID(_entry.uid, this._dataCallback,
					ctx);
		}

		// Display the loading "row prototype" if we don't have data for the row
		if (!this.hasData)
		{
			// Get the average height, the "-5" derives from the td padding
			var avg = Math.round(this._grid.getAverageHeight() - 5) + "px";
			var prototype = this._grid.getRowProvider().getPrototype("loading");
			jQuery("div", prototype).css("height", avg);
			var node = _entry.row.getJNode();
			node.empty();
			node.append(prototype.children());
		}

		// Insert the row into the table -- the same row must never be inserted
		// twice into the grid, so this function only executes the following
		// code only if it is a newly created row.
		if (createdRow && _entry.row)
		{
			this._grid.insertRow(_entry.idx, _entry.row);
		}

		// Remove 'No matches found' row
		var row = jQuery(".egwGridView_empty",this._grid.innerTbody).remove();
		if(row.length)
		{
			this._selectionMgr.unregisterRow("", 0);
		}

		// Update index map only for push (autorefresh disabled)
		if(this._indexMap[_entry.idx] && this._indexMap[_entry.idx].uid !== _entry.uid)
		{
			let max = parseInt(Object.keys(this._indexMap).reduce((a, b) => this._indexMap[a] > this._indexMap[b] ? a : b));
			for(let idx = max; idx >= _entry.idx; idx--)
			{
				let entry = this._indexMap[idx];
				this._indexMap[idx].idx = idx+1;
				this._indexMap[this._indexMap[idx].idx] = this._indexMap[idx];
				if(this._selectionMgr && this._selectionMgr._registeredRows[entry.uid])
				{
					this._selectionMgr._registeredRows[entry.uid].idx = entry.idx;
				}
			}
		}
		this._indexMap[_entry.idx] = _entry;

		return this.hasData;
	}


	/**
	 * Create a new row.
	 *
	 * @param {type} ctx
	 * @returns {et2_dataview_container}
	 */
	_createRow( ctx)
	{
		return new et2_dataview_row(this._grid);
	}

	/**
	 * Function which gets called by the grid when data is requested.
	 *
	 * @param _idxStart is the index of the first row for which data is
	 * requested.
	 * @param _idxEnd is the index of the last requested row.
	 */
	_gridCallback( _idxStart, _idxEnd)
	{

		var needsData = false;

		// Iterate over all elements the dataview requested and create a row
		// which indicates that we are currently loading data
		for (var i = _idxStart; i <= _idxEnd; i++)
		{
			var entry = this._getIndexEntry(i);

			// Insert the row for the entry -- do not update rows which are
			// already existing, as we do not have new data for those.
			if (!this._insertDataRow(entry, false) && needsData === false)
			{
				needsData = i;
			}
		}

		// Queue fetching that data range
		if (needsData !== false)
		{
			this._queueFetch(et2_bounds(needsData, _idxEnd + 1), needsData == _idxStart ? 0 : needsData > _idxStart ? 1 : -1, false);
		}
	}

	/**
	 * The _queueFetch function is used to queue a fetch request.
	 * TODO: Refresh is currently not used
	 */
	_queueFetch( _range, _direction, _isUpdate)
	{

		// Force immediate to be false
		_isUpdate = _isUpdate ? _isUpdate : false;

		// Push the requests onto the request queue
		var start = Math.max(0, _range.top);
		var end = Math.min(this._grid.getTotalCount(), _range.bottom);
		for (var i = start; i < end; i++)
		{
			if (typeof this._queue[i] === "undefined")
			{
				this._queue[i] = _direction; // Stage 1 - queue for after current, -1 -- queue for before current
			}
		}

		// Start the queue timer, if this has not already been done
		if (this._queueTimer === null && !_isUpdate)
		{
			var self = this;
			egw.debug('log', 'Dataview queue: ', _range);
			this._queueTimer = window.setTimeout(function () {
				self._flushQueue(false);
			}, ET2_DATAVIEW_FETCH_TIMEOUT);
		}

		if (_isUpdate)
		{
			this._flushQueue(true);
		}
	}

	/**
	 * Flushes the queue.
	 */
	_flushQueue( _isUpdate)
	{

		// Clear any still existing timer
		this._clearTimer();

		// Mark all elements in a radius of ET2_DATAVIEW_STEPSIZE
		var marked = {};
		var r = _isUpdate ? 0 : Math.floor(ET2_DATAVIEW_STEPSIZE / 2);
		var total = this._grid.getTotalCount();
		for (var key in this._queue)
		{
			if (this._queue[key] > 1)
				continue;

			key = parseInt(key);

			var b = Math.max(0, key - r + (r * this._queue[key]));
			var t = Math.min(key + r + (r * this._queue[key]), total - 1);
			var c = 0;
			for (var i = b; i <= t && c < ET2_DATAVIEW_STEPSIZE; i ++)
			{
				if (typeof this._queue[i] == "undefined"
						|| this._queue[i] <= 1)
				{
					this._queue[i] = 2; // Stage 2 -- pending or available
					marked[i] = true;
					c++;
				}
			}
		}

		// Create a list with start indices and counts
		var fetchList = [];
		var entry = null;
		var last = 0;

		// Get the int keys and sort the array numeric
		var arr = et2_arrayIntKeys(marked).sort(
				function(a,b){return a > b ? 1 : (a == b ? 0 : -1)});

		for (var i = 0; i < arr.length; i++)
		{
			if (i == 0 || arr[i] - last > 1)
			{
				if (entry)
				{
					fetchList.push(entry);
				}
				entry = {
					"start": arr[i],
					"count": 1
				};
			}
			else
			{
				entry.count++;
			}

			last = arr[i];
		}

		if (entry)
		{
			fetchList.push(entry);
		}

		// Special case: If there are no entries in the fetch list and this is
		// an update, create an dummy entry, so that we'll get the current count
		if (fetchList.length === 0 && _isUpdate)
		{
			fetchList.push({
				"start": 0, "count": 0
			});

			// Disable grid invalidate, or it might request again before we're done
			this._grid.doInvalidate = false;
		}

		egw.debug("log", "Dataview flush", fetchList);
		// Execute all queries
		for (var i = 0; i < fetchList.length; i++)
		{
			// Build the query
			var query = {
					"start": fetchList[i].start,
					"num_rows": fetchList[i].count,
					"refresh": false
			};

			// Context used in the callback function
			var ctx = {
					"self": this,
					"start": query.start,
					"count": query.num_rows,
					"lastModification": this._lastModification,
				prefix: undefined
			};
			if(this.dataStorePrefix)
			{
				ctx.prefix = this.dataStorePrefix;
			}

			this._queueRequest(query, ctx);
		}
	}

	/**
	 * Queue a request for data
	 * @param {Object} query
	 * @param {Object} ctx
	 */
	_queueRequest(query, ctx)
	{
		this._request_queue.push({
			query: query,
			context: ctx,
			// Start pending, set to 1 when request sent
			status: 0
		});

		this._fetchQueuedRequest();
	}

	/**
	 * Fetch data for a queued request, subject to rate limit
	 */
	_fetchQueuedRequest()
	{
		// Check to see if there's room
		var count = 0;
		for (var i = 0; i < this._request_queue.length; i++)
		{
			if(this._request_queue[i].status > 0) count++;
		}
		// Too many requests, will try again after response is received
		if(count >= et2_dataview_controller.CONCURRENT_REQUESTS || this._request_queue.length === 0)
		{
			return;
		}

		// Keep at least 1 previous pending
		var keep = 1;

		// The most recent is the one the user's most interested in
		var request = null;
		for(var i = this._request_queue.length - 1; i >= 0; i--)
		{
			// Only interested in pending requests (status 0)
			if(this._request_queue[i].status != 0)
			{
				continue;
			}
			if(request == null)
			{
				request = this._request_queue[i];
			}
			else if (keep > 0)
			{
				keep--;
			}
			else if (keep <= 0)
			{
				// Cancel pending, they've probably scrolled past.
				this._request_queue.splice(i,1);
			}
		}
		if(request == null) return;

		// Request being sent
		request.status = 1;

		// Call the callback
		this._dataProvider.dataFetch(request.query, this._fetchCallback, request.context);
	}

	_clearTimer( )
	{

		// Reset the queue timer upon destruction
		if (this._queueTimer)
		{
			window.clearTimeout(this._queueTimer);
			this._queueTimer = null;
		}

	}

	/**
	 * Called by the data source when the data changes
	 *
	 * @param _data Object|null New data, or null.  Null will remove the row.
	 */
	_dataCallback( _data)
	{
		// Set the "hasData" flag
		this.self.hasData = true;

		// Call the row callback with the new data -- the row callback then
		// generates the row DOM nodes that will be inserted into the grid
		if (this.self._rowCallback)
		{
			// Remove everything from the current row
			this.entry.row.clear();

			// If there's no data, stop
			if(typeof _data == "undefined" || _data == null)
			{
				this.self._destroyCallback.call(
					this,
					this.entry.row
				);
				return;
			}

			// Fill the row DOM Node with data
			this.self._rowCallback.call(
				this.self._context,
				_data,
				this.entry.row,
				this.entry.idx,
				this.entry
			);

			// Attach the "subgrid" tag to the row, if the depth of this
			// controller is larger than zero
			var tr = this.entry.row.getDOMNode();
			var d = this.self.getDepth();
			if (d > 0)
			{
				jQuery(tr).addClass("subentry");
				jQuery("td:first",tr).children("div").last().addClass("level_" + d + " indentation");

				if(this.entry.idx == 0)
				{
					// Set the CSS for the level - required so columns line up
					var indent = jQuery("<span class='indentation'/>").appendTo('body');
					egw.css(".subentry td div.innerContainer.level_"+d,
						"margin-right:" + (parseInt(indent.css("margin-right")) * d) + "px"
					);
					indent.remove();
				}
			}

			var links = null;

			// Look for a flag in the row to avoid actions.  Use for sums or extra header rows.
			if(!_data.no_actions)
			{
				// Get the action links if the links callback is set
				if (this.self._linkCallback)
				{
					links = this.self._linkCallback.call(
							this.self._context,
							_data,
							this.entry.idx,
							this.entry.uid
					);
				}

				// Register the row in the selection manager
				this.self._selectionMgr.registerRow(this.entry.uid, this.entry.idx,
						tr, links);
			}
			else
			{
				// Remember that
				this.entry.no_actions = true;
			}

			// Invalidate the current row entry
			this.entry.row.invalidate();
		}
	}

	/**
	 *
	 */
	_destroyCallback( _row)
	{

		// Unregister the row from the selection manager, if not selected
		// If it is selected, leave it there - allows selecting rows and scrolling
		var selection = this.self._selectionMgr._getRegisteredRowsEntry(this.entry.uid);
		if (this.entry.row && selection && !egwBitIsSet(selection.state, EGW_AO_STATE_SELECTED))
		{
			var tr = this.entry.row.getDOMNode();
			this.self._selectionMgr._updateState(this.entry.uid, EGW_AO_STATE_NORMAL);
			this.self._selectionMgr.unregisterRow(this.entry.uid, tr);
		}

		// There is no further row connected to the entry
		this.entry.row = null;

		// Unregister the data callback
		this.self._dataProvider.dataUnregisterUID(this.entry.uid,
				this.self._dataCallback, this);
	}

	/**
	 * Returns an array containing "_count" index mapping entries starting from
	 * the index given in "_start".
	 */
	_getIndexMapping( _start, _count)
	{
		var result = [];

		for (var i = _start; i < _start + _count; i++)
		{
			result.push(this._getIndexEntry(i));
		}

		return result;
	}

	/**
	 * Updates the grid according to the new order. The function simply does the
	 * following: It iterates along the new order (given in _order) and the old
	 * order given in _idxMap. Iteration variables used are
	 *     a) i -- points to the current entry in _order
	 *     b) idx -- points to the current grid row that will be effected by
	 *        this operation.
	 *     c) mapIdx -- points to the current entry in _indexMap
	 * The following cases may occur:
	 *     a) The current entry in the old order has no uid or no row -- in that
	 *        case the row at the current position is simply updated,
	 *        the old pointer will be incremented.
	 *     b) The two uids differ -- insert a new row with the new uid, do not
	 *        increment the old pointer.
	 *     c) The two uids are the same -- increment the old pointer.
	 * In a last step all rows that are left in the old order are deleted. All
	 * newly created index entries are returned. This function does not update
	 * the internal mapping in _idxMap.
	 */
	_updateOrder( _start, _count, _idxMap, _order)
	{
		// The result contains the newly created index map entries which have to
		// be merged with the result
		var result = [];

		// Iterate over the new order
		var mapIdx = 0;
		var idx = _start;
		for (var i = 0; i < _order.length; i++, idx++)
		{
			var current = _idxMap[mapIdx];

			if (!current.row || !current.uid)
			{
				// If there is no row yet at the current position or the uid
				// of that entry is unknown, simply update the entry.
				current.uid = _order[i];
				current.idx = idx;

				// Only update the row, if it is displayed (e.g. has a "loading"
				// row displayed) -- this is needed for prefetching
				if (current.row)
				{
					this._insertDataRow(current, true);
				}

				mapIdx++;
			}
			else if (current.uid !== _order[i])
			{
				// Insert a new row at the new position
				var entry = {
					"idx": idx,
					"uid": _order[i],
					"row": null
				};

				this._insertDataRow(entry, true);

				// Remember the new entry
				result.push(entry);
			}
			else
			{
				// Do nothing, the uids do not differ, just update the index of
				// the element
				current.idx = idx;
				mapIdx++;
			}
		}

		// Delete as many rows as we have left, invalidate the corresponding
		// index entry
		for (var i = mapIdx; i < _idxMap.length; i++)
		{
			if(typeof _idxMap[i] != 'undefined')
			{
				_idxMap[i].uid = null;
			}
		}

		return result;
	}

	_mergeResult( _newEntries, _invalidStartIdx, _diff, _total)
	{

		if (_newEntries.length > 0 || _diff > 0)
		{
			// Create a new index map
			var newMap = {};

			// Insert all new entries into the new index map
			for (var i = 0; i < _newEntries.length; i++)
			{
				newMap[_newEntries[i].idx] = _newEntries[i];
			}

			// Merge the old map with all old entries
			for (var key in this._indexMap)
			{
				// Get the corresponding index entry
				var entry = this._indexMap[key];

				// Calculate the new index -- if rows were deleted, we'll
				// have to adjust the index
				var newIdx = entry.idx >= _invalidStartIdx
						? entry.idx - _diff : entry.idx;
				if (newIdx >= 0 && newIdx < _total
				    && typeof newMap[newIdx] === "undefined")
				{
					entry.idx = newIdx;
					newMap[newIdx] = entry;
				}
				else if(newMap[newIdx]!==this._indexMap[key])
				{
					// Make sure the old entry gets invalidated
					entry.idx = null;
					entry.row = null;
				}
			}

			// Make the new index map the current index map
			this._indexMap = newMap;
			this._selectionMgr.setIndexMap(newMap);
		}

	}

	_fetchCallback( _response)
	{
		// Remove answered request from queue
		var request = null;
		for(var i = 0; i < this.self._request_queue.length; i++)
		{
			if(this.self._request_queue[i].context == this)
			{
				request = this.self._request_queue[i];
				this.self._request_queue.splice(i,1);
				break;
			}
		}

		this.self._lastModification = _response.lastModification;

		// Do nothing if _response.order evaluates to false
		if (!_response.order)
		{
			return;
		}

		// Make sure _response.order.length is not longer than the requested
		// count, if a specific count was requested
		var order = this.count != 0 ? _response.order.splice(0, this.count) : _response.order;

		// Remove from queue, or it will not be fetched again
		if(_response.total < this.count)
		{
			// Less rows than we expected
			// Clear the queue, or the remnants will never be loaded again
			this.self._queue = {};
		}
		else
		{
			for(var i = this.start; i < this.start + order.length; i++)
				delete this.self._queue[i];
		}

		// Get the current index map for the updated region
		var idxMap = this.self._getIndexMapping(this.start, order.length);

		// Update the grid using the new order. The _updateOrder function does
		// not update the internal mapping while inserting and deleting rows, as
		// this would move us to another asymptotic runtime level.
		var res = this.self._updateOrder(this.start, this.count, idxMap, order);

		// Merge the new indices, update all indices with rows that were not
		// affected and invalidate all indices if there were changes
		this.self._mergeResult(res, this.start + order.length,
				idxMap.length - order.length, _response.total);

		if(_response.total == 0)
		{
			this.self._emptyRow(true);
		}
		else
		{
			var row = jQuery(".egwGridView_empty",this.self._grid.innerTbody).remove();
			this.self._selectionMgr.unregisterRow("",0,row.get(0));
		}

		// Now it's OK to invalidate, if it wasn't before
		this.self._grid.doInvalidate = true;

		// Update the total element count in the grid
		this.self._grid.setTotalCount(_response.total);
		this.self._selectionMgr.setTotalCount(_response.total);

		// Schedule an invalidate, in case total is the same
		this.self._grid.invalidate();

		// Check if requests are waiting
		this.self._fetchQueuedRequest();
	}

	/**
	 * Insert an empty / placeholder row when there is no data to display
	 */
	_emptyRow(_noRows)
	{
		var noRows = !_noRows ? false : true;
		jQuery(".egwGridView_empty",this._grid.innerTbody).remove();
		if(typeof this._grid._rowProvider != "undefined" && this._grid._rowProvider.getPrototype("empty"))
		{
			var placeholder = this._grid._rowProvider.getPrototype("empty");
			if(jQuery("td",placeholder).length == 1)
			{
				jQuery("td",placeholder).css("width",this._grid.outerCell.width() + "px")
			}
			placeholder.appendTo(this._grid.innerTbody);

			// Register placeholder action only if no rows
			if (noRows)
			{
				// Get the action links if the links callback is set
				var links = null;
				if (this._linkCallback)
				{
					links = this._linkCallback.call(
						this._context,
						{},
						0,
						""
					);
				}
				this._selectionMgr.registerRow("",0,placeholder.get(0), links);
			}
		}
	}

	/**
	 * Callback function used by the selection manager to translate the selected
	 * range to uids.
	 */
	_selectionFetchRange( _range, _callback, _context)
	{
		this._dataProvider.dataFetch(
				{ "start": _range.top, "num_rows": _range.bottom - _range.top + 1,
				  "no_data": true },
				function (_response) {
					_callback.call(_context, _response.order);
				},
				_context
		);
	}

	/**
	 * Tells the grid to make the given index visible.
	 */
	_makeIndexVisible( _idx)
	{
		this._grid.makeIndexVisible(_idx);
	}

}

