/*!
 * OpenUI5
 * (c) Copyright 2009-2026 SAP SE or an SAP affiliate company.
 * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
 */

// Provides class sap.m.GrowingEnablement
sap.ui.define([
	'sap/ui/base/Object',
	'sap/ui/core/format/NumberFormat',
	'sap/m/library',
	'sap/ui/model/ChangeReason',
	'sap/ui/base/ManagedObjectMetadata',
	'sap/ui/base/ManagedObjectObserver',
	'sap/ui/core/HTML',
	'sap/m/CustomListItem',
	'sap/base/security/encodeXML',
	'sap/ui/core/Core',
	"sap/ui/thirdparty/jquery"
],
	function(
		BaseObject,
		NumberFormat,
		library,
		ChangeReason,
		ManagedObjectMetadata,
		ManagedObjectObserver,
		HTML,
		CustomListItem,
		encodeXML,
		Core,
		jQuery
	) {
	"use strict";


	// shortcut for sap.m.ListType
	var ListType = library.ListType;

	// shortcut for sap.m.ListGrowingDirection
	var ListGrowingDirection = library.ListGrowingDirection;


	/**
	 * Creates a GrowingEnablement delegate that can be attached to ListBase Controls requiring capabilities for growing
	 *
	 * @extends sap.ui.base.Object
	 * @alias sap.m.GrowingEnablement
	 * @experimental Since 1.16. This class is experimental and provides only limited functionality. Also the API might be changed in future.
	 *
	 * @param {sap.m.ListBase} oControl the ListBase control of which this Growing is the delegate
	 *
	 * @constructor
	 * @protected
	 */
	var GrowingEnablement = BaseObject.extend("sap.m.GrowingEnablement", /** @lends sap.m.GrowingEnablement.prototype */ {

		constructor : function(oControl) {
			BaseObject.apply(this);
			this._oControl = oControl;
			this._oControl.bUseExtendedChangeDetection = true;
			this._oControl.addDelegate(this);

			/* init growing list */
			var iRenderedItemsLength = this._oControl.getItems(true).length;
			this._iRenderedDataItems = iRenderedItemsLength;
			this._iLimit = iRenderedItemsLength;
			this._bLoading = false;
			this._sGroupingPath = "";
			this._bDataRequested = false;
			this._bSkippedItemsUpdateUntilDataReceived = false;
			this._iLastItemsCount = 0;
			this._iTriggerTimer = 0;
			this._aChunk = [];
			this._oRM = null;
			this._aItemsPool = [];
		},

		/**
		 * Destroys this GrowingEnablement delegate.
		 * This function must be called by the control which uses this delegate in the <code>exit</code> function.
		 */
		destroy : function() {
			if (this._oTrigger) {
				this._oTrigger.destroy();
				this._oTrigger = null;
			}
			if (this._oScrollDelegate) {
				this._oScrollDelegate.setGrowingList(null);
				this._oScrollDelegate = null;
			}
			if (this._oRM) {
				this._oRM.destroy();
				this._oRM = null;
			}
			if (this._oObserver) {
				this._oObserver.disconnect();
				this._oObserver = null;
			}

			this.clearItemsPool();
			this._oControl.$("triggerList").remove();
			this._oControl.bUseExtendedChangeDetection = false;
			this._oControl.removeDelegate(this);
			this._oControl = null;
		},

		// renders load more trigger
		render : function(oRm) {
			oRm.openStart("div", this._oControl.getId() + "-triggerList");
			oRm.class("sapMListUl").class("sapMGrowingList");
			oRm.style("display", "none");
			oRm.openEnd();
			oRm.renderControl(this._getTrigger());
			oRm.close("div");
		},

		onAfterRendering : function() {
			var oControl = this._oControl;
			if (oControl.getGrowingScrollToLoad()) {
				var oScrollDelegate = library.getScrollDelegate(oControl);
				if (oScrollDelegate) {
					this._oScrollDelegate = oScrollDelegate;
					oScrollDelegate.setGrowingList(this.onScrollToLoad.bind(this), oControl.getGrowingDirection(), this._updateTrigger.bind(this, false));
				}
			} else if (this._oScrollDelegate) {
				this._oScrollDelegate.setGrowingList(null);
				this._oScrollDelegate = null;
			}

			if (!this._bLoading) {
				this._updateTriggerDelayed(false);
			}
		},

		onsapdown: function(oEvent) {
			// Navigate from last item to growing trigger and vice versa via arrow keys
			var oControl = this._oControl;
			if (oControl._oItemNavigation && !oEvent.isMarked()) {
				var aItemDomRefs = oControl._oItemNavigation.getItemDomRefs();
				var sDir = oControl.getGrowingDirection();
				if ((sDir != ListGrowingDirection.Upwards && oEvent.type == "sapdown" && oEvent.target === aItemDomRefs[aItemDomRefs.length - 1])
						|| (sDir == ListGrowingDirection.Upwards && oEvent.type == "sapup" && oEvent.target === aItemDomRefs[0])) {
					var $Trigger = oControl.$("trigger");
					$Trigger.trigger("focus");
					oEvent.setMarked();
					oEvent.stopImmediatePropagation(); // to prevent ItemNavigation
				} else if ((sDir == ListGrowingDirection.Upwards && oEvent.type == "sapdown")
						|| (sDir != ListGrowingDirection.Upwards && oEvent.type == "sapup")
						&& oEvent.target === oControl.getDomRef("trigger")) {
					jQuery(aItemDomRefs[oEvent.type == "sapdown" ? 0 : aItemDomRefs.length - 1]).trigger("focus");
					oEvent.setMarked();
				}
			}
		},

		onsapup: function(oEvent) {
			return this.onsapdown(oEvent);
		},

		setTriggerText : function(sText) {
			this._oControl.$("triggerText").text(sText);
		},

		// reset paging on rebind
		reset : function(bPageSizeOnly) {
			this._iLimit = 0;

			if (bPageSizeOnly) {
				return;
			}

			// destroy the observer on rebind
			if (this._oObserver) {
				this._oObserver.disconnect();
				this._oObserver = null;
			}

			// if the template invalidates, then also clear the itemsPool
			this.clearItemsPool();

			// if factory function is used we do not activate the replace option of the extended change detection
			var oBindingInfo = this._oControl.getBindingInfo("items");
			this._oControl.oExtendedChangeDetectionConfig = (!oBindingInfo || !oBindingInfo.template) ? null : {replace: true};
		},

		clearItemsPool: function() {
			this._aItemsPool.forEach(function(oItem) {
				oItem.destroy();
			});
			this._aItemsPool = [];
		},

		// determines growing reset with binding change reason
		// according to UX sort/filter/context should reset the growing
		shouldReset : function(sChangeReason) {
			var mChangeReason = ChangeReason;

			return 	sChangeReason == mChangeReason.Sort ||
					sChangeReason == mChangeReason.Filter ||
					sChangeReason == mChangeReason.Context;
		},

		// get actual and total info
		getInfo : function() {
			return {
				total : this._oControl.getMaxItemsCount(),
				actual : this._iRenderedDataItems
			};
		},

		onScrollToLoad: function() {
			if (this._bLoading) {
				return;
			}

			if (this._oControl.getGrowingDirection() == ListGrowingDirection.Upwards) {
				var oScrollDelegate = this._oScrollDelegate;
				this._oScrollPosition = {
					left : oScrollDelegate.getScrollLeft(),
					top : oScrollDelegate.getScrollHeight()
				};
			}

			this.requestNewPage();
		},

		// call to request new page
		requestNewPage : function() {
			if (!this._oControl || this._bLoading) {
				return;
			}

			// if max item count not reached or if we do not know the count
			var oBinding = this._oControl.getBinding("items");
			if (oBinding && !oBinding.isLengthFinal() || this._iLimit < this._oControl.getMaxItemsCount()) {
				// block busy indicator animation from the ListBase
				this._oControl._bBusy = true;
				this._iLimit += this._oControl.getGrowingThreshold();
				this._updateTriggerDelayed(true);
				this.updateItems("Growing");
			}
		},

		// called before new page loaded
		_onBeforePageLoaded : function(sChangeReason) {
			this._bLoading = true;
			this._oControl.onBeforePageLoaded(this.getInfo(), sChangeReason);
		},

		// called after new page loaded
		_onAfterPageLoaded : function(sChangeReason) {
			if (!this._oControl) {
				return;
			}

			this._bLoading = false;
			this._updateTriggerDelayed(false);
			this._oControl.onAfterPageLoaded(this.getInfo(), sChangeReason);
		},

		// created and returns load more trigger
		_getTrigger : function() {
			var sTriggerID = this._oControl.getId() + "-trigger",
				sTriggerText = this._oControl.getGrowingTriggerText();

			sTriggerText = sTriggerText || Core.getLibraryResourceBundle("sap.m").getText("LOAD_MORE_DATA");
			this._oControl.addNavSection(sTriggerID);

			if (this._oTrigger) {
				this.setTriggerText(sTriggerText);
				return this._oTrigger;
			}

			// The growing button is changed to span tag as h1 tag was semantically incorrect.
			this._oTrigger = new CustomListItem({
				id: sTriggerID,
				busyIndicatorDelay: 0,
				type: ListType.Active,
				content: new HTML({
					content:	'<div class="sapMGrowingListTrigger">' +
									'<div class="sapMSLIDiv sapMGrowingListTriggerText">' +
										'<span class="sapMSLITitle" id="' + sTriggerID + 'Text">' + encodeXML(sTriggerText) + '</span>' +
									'</div>' +
									'<div class="sapMGrowingListDescription sapMSLIDescription" id="' + sTriggerID + 'Info"></div>' +
									'<div class="sapUiInvisibleText" id="' + sTriggerID + 'Message"></div>' +
								'</div>'
				})
			});

			// stop the eventing between item and the list
			this._oTrigger.getList = function() {};
			// defines the tag name
			this._oTrigger.TagName = "div";
			// trigger should not be mapped to groupHeader when setParent is called, hence overwrite this method
			this._oTrigger.setGroupedItem = function() {};

			this._oTrigger.setParent(this._oControl, null, true).attachPress(this.requestNewPage, this).addDelegate({
				onsapenter : function(oEvent) {
					this.requestNewPage();
					oEvent.preventDefault();
				},
				onsapspace : function(oEvent) {
					this._bSpaceKeyPressed = true;
					this._oTrigger.setActive(true);
					oEvent.preventDefault();
				},
				onkeydown : function(oEvent) {
					this._bSpaceKeyCancelled = this._bSpaceKeyCancelled || (oEvent.shiftKey || oEvent.which == 27 /** KeyCodes.ESCAPE */);
				},
				onkeyup: function(oEvent) {
					this._bSpaceKeyPressed && !this._bSpaceKeyCancelled && this.requestNewPage();
					this._bSpaceKeyPressed = this._bSpaceKeyCancelled = false;
					this._oTrigger.setActive(false);
				},
				onAfterRendering : function(oEvent) {
					var $oTrigger = this._oTrigger.$();
					// aria-selected is added as the CustomListItem type="Active"
					// aria-selected should be removed as it is not allowed with role="button"
					$oTrigger.removeAttr("aria-selected");
					// aria-roledescription not required for growing trigger
					$oTrigger.removeAttr("aria-roledescription");
					// aria-posinset & aria-setsize removed as it is not allowed with role="button"
					$oTrigger.removeAttr("aria-posinset").removeAttr("aria-setsize");
					$oTrigger.attr({
						"tabindex": 0,
						"role": "button",
						"aria-labelledby": sTriggerID + "Text",
						"aria-describedby": sTriggerID + "Message"
					});
				}
			}, this);

			return this._oTrigger;
		},

		// returns the growing information to be shown at the growing button
		_getListItemInfo : function() {
			var aCounts = this._getItemCounts();
			var oFormat = NumberFormat.getFloatInstance();
			return "[ " + oFormat.format(aCounts[0]) + " / " + oFormat.format(aCounts[1]) + " ]";
		},

		// returns the item counts for the growing information (current vs. total)
		_getItemCounts : function() {
			return [this._iRenderedDataItems, this._oControl.getMaxItemsCount()];
		},

		// returns the first sorters grouping path when available
		_getGroupingPath : function(oBinding) {
			var aSorters = oBinding.aSorters || [];
			var oSorter = aSorters[0] || {};
			return (oSorter.fnGroup) ? oSorter.sPath || "" : "";
		},

		// if table has pop-in then we have two rows for one item
		_getDomIndex : function(vIndex) {
			if (typeof vIndex != "number") {
				return vIndex;
			}

			if (this._oControl.hasPopin && this._oControl.hasPopin()) {
				return (vIndex * 2);
			}

			return vIndex;
		},

		// determines if the scroll container of the list has enough scrollable area to hide the growing button
		_getHasScrollbars : function() {
			if (!this._oScrollDelegate) {
				return false;
			}

			// 32 is the minimum height of the item
			if (this._getDomIndex(this._iRenderedDataItems) > (window.innerHeight / 32)) {
				return true;
			}

			// after growing-button gets hidden scroll container should still be scrollable
			return this._oScrollDelegate.getMaxScrollTop() > this._oControl.getDomRef("triggerList").clientHeight;
		},

		// destroy all items in the list and cleanup
		destroyListItems : function(bSuppressInvalidate) {
			this._oControl.destroyItems(bSuppressInvalidate);
			this._iRenderedDataItems = 0;
			this._aChunk = [];
		},

		// appends single list item to the list
		addListItem : function(oContext, oBindingInfo, bSuppressInvalidate) {

			var oControl = this._oControl,
				oBinding = oBindingInfo.binding,
				oItem = this.createListItem(oContext, oBindingInfo);

			if (oBinding.isGrouped()) {
				// creates group header if need
				var aItems = oControl.getItems(true),
					oLastItem = aItems[aItems.length - 1],
					sModelName = oBindingInfo.model,
					oGroupInfo = oBinding.getGroup(oItem.getBindingContext(sModelName));

				if (oLastItem && oLastItem.isGroupHeader()) {
					oControl.removeAggregation("items", oLastItem, true);
					oControl.setLastGroupHeader(oLastItem);
					this._fnAppendGroupItem = this.appendGroupItem.bind(this, oGroupInfo, oLastItem, bSuppressInvalidate);
					oLastItem = aItems[aItems.length - 1];
				}

				if (!oLastItem || oGroupInfo.key !== oBinding.getGroup(oLastItem.getBindingContext(sModelName)).key) {
					// get the groupHeader control groupHeaderFactory or create an sap.m.GroupHeaderListItem instance with the oGroupInfo
					// required for setLastGroupHeader to correctly set the _oLastGroupHeader. The created groupHeader is later reused
					var oGroupHeader = oBindingInfo.groupHeaderFactory ? oBindingInfo.groupHeaderFactory(oGroupInfo) : oControl.getGroupHeaderTemplate(oGroupInfo);

					if (oControl.getGrowingDirection() == ListGrowingDirection.Upwards) {
						this.applyPendingGroupItem();
						oControl.setLastGroupHeader(oGroupHeader);
						this._fnAppendGroupItem = this.appendGroupItem.bind(this, oGroupInfo, oGroupHeader, bSuppressInvalidate);
					} else {
						this.appendGroupItem(oGroupInfo, oGroupHeader, bSuppressInvalidate);
					}
				}

				var oLastGroupHeader = oControl.getLastGroupHeader();
				if (oLastGroupHeader) {
					// required to update the group header aria-owns attribute
					oLastGroupHeader.invalidate();
				}
			}

			oControl.addAggregation("items", oItem, bSuppressInvalidate);
			if (bSuppressInvalidate) {
				this._aChunk.push(oItem);
			}
		},

		applyPendingGroupItem: function() {
			if (this._fnAppendGroupItem) {
				this._fnAppendGroupItem();
				this._fnAppendGroupItem = undefined;
			}
		},

		appendGroupItem: function(oGroupInfo, oGroupHeader, bSuppressInvalidate) {
			oGroupHeader = this._oControl.addItemGroup(oGroupInfo, oGroupHeader, bSuppressInvalidate);
			if (bSuppressInvalidate) {
				this._aChunk.push(oGroupHeader);
			}
		},

		fillItemsPool: function() {
			if (!this._oControl || !this._iLimit || this._iRenderedDataItems || this._aItemsPool.length) {
				return;
			}

			var oBindingInfo = this._oControl.getBindingInfo("items");
			var oTemplate = oBindingInfo.template;
			if (!oTemplate) {
				return;
			}

			// limit the number of items in the pool to 100, since have too many items in the pool is also not performant
			for (var i = 0, iLimit = Math.min(this._iLimit, 100); i < iLimit; i++) {
				this._aItemsPool.push(oBindingInfo.factory());
			}

			if (oTemplate.getCells) {
				// items pool is not usable when the template is changed e.g. p13n removes/insert cells instead of rebind
				this._oObserver = new ManagedObjectObserver(this.clearItemsPool.bind(this));
				this._oObserver.observe(oTemplate, { aggregations: ["cells"] });
			}
		},

		// creates list item from the factory
		createListItem : function(oContext, oBindingInfo) {
			this._iRenderedDataItems++;

			if (this._aItemsPool.length) {
				return this._aItemsPool.shift().setBindingContext(oContext, oBindingInfo.model);
			}

			return GrowingEnablement.createItem(oContext, oBindingInfo);
		},

		// update context on all items except group headers
		updateItemsBindingContext :  function(aContexts, oModel) {
			if (!aContexts.length) {
				return;
			}

			var aItems = this._oControl.getItems(true);
			for (var i = 0, c = 0, oItem; i < aItems.length; i++) {
				oItem = aItems[i];

				// group headers are not in binding context
				if (!oItem.isGroupHeader()) {
					oItem.setBindingContext(aContexts[c++], oModel);
				}
			}
		},

		// render all the collected items in the chunk and flush them into the DOM
		// vInsert whether to append (true) or replace (falsy) or to insert at a certain position (int)
		applyChunk : function(vInsert, bAsync) {
			if (!this._oControl) {
				return;
			}

			this.applyPendingGroupItem();

			var iTimer = this._iChunkTimer;
			var iLength = this._aChunk.length;
			var oDomRef = this._oControl.getItemsContainerDomRef();

			if (iTimer) {
				this._iChunkTimer = clearTimeout(iTimer);
			}

			if (!iLength || !oDomRef || !this._oControl.shouldRenderItems()) {
				this._aChunk = [];
				return;
			}

			if (iTimer && !bAsync) {
				this._oControl.invalidate();
				this._aChunk = [];
				return;
			}

			if (this._oControl.getGrowingDirection() == ListGrowingDirection.Upwards) {
				this._aChunk.reverse();
				if (vInsert === true) {
					vInsert = 0;
				} else if (typeof vInsert == "number") {
					vInsert = this._iRenderedDataItems - iLength - vInsert;
				}
			}

			this._oRM = this._oRM || Core.createRenderManager();
			for (var i = 0; i < iLength; i++) {
				this._oRM.renderControl(this._aChunk[i]);
			}

			var bHasFocus = (vInsert == false) && oDomRef.contains(document.activeElement);
			this._oRM.flush(oDomRef, false, this._getDomIndex(vInsert));
			bHasFocus && this._oControl.focus();
			this._aChunk = [];
		},

		// async version of applyChunk
		applyChunkAsync : function(vInsert) {
			if (this._bApplyChunkAsync) {
				this._iChunkTimer = setTimeout(this.applyChunk.bind(this, vInsert, true));
			} else {
				this.applyChunk(vInsert);
			}
		},

		// add multiple items to the list via BindingContext
		addListItems : function(aContexts, oBindingInfo, bSuppressInvalidate) {
			for (var i = 0; i < aContexts.length; i++) {
				this.addListItem(aContexts[i], oBindingInfo, bSuppressInvalidate);
			}
		},

		// destroy all the items and create from scratch
		rebuildListItems : function(aContexts, oBindingInfo, bSuppressInvalidate) {
			this.destroyListItems(bSuppressInvalidate);
			this.addListItems(aContexts, oBindingInfo, bSuppressInvalidate);
			if (bSuppressInvalidate) {
				this.applyChunkAsync(false);
			} else {
				this.applyPendingGroupItem();
			}
		},

		// inserts a single list item
		insertListItem : function(oContext, oBindingInfo, iIndex) {
			var oItem = this.createListItem(oContext, oBindingInfo);
			this._oControl.insertAggregation("items", oItem, iIndex, true);
			this._aChunk.push(oItem);
		},

		// destroy a single list item
		deleteListItem : function(iIndex) {
			var oItem = this._oControl.getItems(true)[iIndex];
			if (oItem) {
				this._oControl.getItems(true)[iIndex].destroy(true);
				this._iRenderedDataItems--;
			}
		},

		/**
		 * refresh items only for OData model.
		 */
		refreshItems : function(sChangeReason) {
			var oControl = this._oControl;
			var oBinding = oControl.getBinding("items");

			// the value of the ODataV4PropertyBinding created during the template clone gets resolved asynchronously
			this._bApplyChunkAsync = oBinding.isA("sap.ui.model.odata.v4.ODataListBinding") && oControl.checkGrowingFromScratch();

			// if the data is not already requested then let the updateStarted event to be fired
			if (!this._bDataRequested) {
				this._bDataRequested = true;
				this._onBeforePageLoaded(sChangeReason);
			}

			// set iItemCount to initial value if not set or no items at the control yet
			if (!this._iLimit || this.shouldReset(sChangeReason) || !oControl.getItems(true).length) {
				this._iLimit = oControl.getGrowingThreshold();
			}

			// pre-initialize items during the request is ongoing (but not for v1 ODataModel, since it is synchronous)
			if (!oBinding.isA("sap.ui.model.odata.ODataListBinding")) {
				if (oControl._bBusy) {
					setTimeout(this.fillItemsPool.bind(this));
				} else {
					oBinding.attachEventOnce("dataRequested", function() {
						setTimeout(this.fillItemsPool.bind(this));
					}, this);
				}
			}

			// send the request to get the context
			oBinding.getContexts(0, this._iLimit);
		},

		/**
		 * update control aggregation if contexts are already available
		 * or send a request to get the contexts in case of ODATA model.
		 */
		updateItems : function(sChangeReason) {
			var oControl = this._oControl,
				oBinding = oControl.getBinding("items"),
				oBindingInfo = oControl.getBindingInfo("items"),
				aItems = oControl.getItems(true),
				sGroupingPath = this._sGroupingPath;

			// set limit to initial value if not set yet or no items at the control yet
			if (!this._iLimit || this.shouldReset(sChangeReason) || !aItems.length) {
				this._iLimit = oControl.getGrowingThreshold();
			}

			this._bSkippedItemsUpdateUntilDataReceived = false;

			// fire growing started event if data was requested this is a followup call of updateItems
			if (this._bDataRequested) {
				this._bDataRequested = false;
			} else {
				this._onBeforePageLoaded(sChangeReason);
			}

			// get the context from the binding or request will be sent
			var aContexts = oBinding.getContexts(0, this._iLimit) || [];

			// if getContexts did cause a request to be sent, set the internal flag so growing started event is not fired again
			if (aContexts.dataRequested) {
				this._bDataRequested = true;

				// a partial response may already be contained, so only return here without updating the list when diff is empty
				if (aContexts.diff && !aContexts.diff.length) {
					if (sChangeReason === ChangeReason.Context) {
						this._bSkippedItemsUpdateUntilDataReceived = true;
					}
					return;
				}
			}

			// let the grouping path to be stored
			this._sGroupingPath = this._getGroupingPath(oBinding);

			// aContexts.diff ==> undefined : New data we should build from scratch
			// aContexts.diff ==> [] : There is no diff, means data did not changed at all
			// aContexts.diff ==> [{index: 0, type: "delete"}, {index: 1, type: "insert"},...] : Run the diff logic
			var aDiff = aContexts.diff;

			// process the diff
			if (!aContexts.length) {
				// no context, destroy list items
				this.destroyListItems();
			} else if (!aItems.length && !oControl.getItemsContainerDomRef()) {
				// no dom ref for compatibility reason start from scratch
				this.rebuildListItems(aContexts, oBindingInfo);
			} else if (!aDiff || !aItems.length && aDiff.length) {
				// new records need to be applied from scratch
				this.rebuildListItems(aContexts, oBindingInfo, oControl.shouldGrowingSuppressInvalidation());
			} else {
				// diff handling case for grouping and merging
				var bFromScratch = false, vInsertIndex = true;
				if (oBinding.isGrouped() || oControl.checkGrowingFromScratch()) {

					if (sGroupingPath != this._sGroupingPath) {
						// grouping is changed so we need to rebuild the list for the group headers
						bFromScratch = true;
					} else {
						// append items if possible
						for (var i = 0; i < aDiff.length; i++) {
							var oDiff = aDiff[i],
								oContext = aContexts[oDiff.index];

							if (oDiff.type == "delete" || oDiff.type == "replace") {
								// group header may need to be deleted as well
								bFromScratch = true;
								break;
							} else if (oDiff.index != this._iRenderedDataItems) {
								// this item is not appended
								bFromScratch = true;
								break;
							} else {
								// the item is appended
								this.addListItem(oContext, oBindingInfo, true);
							}
						}
					}

				} else {

					if (sGroupingPath && !this._sGroupingPath) {
						// if it was already grouped then we need to remove group headers first
						oControl.removeGroupHeaders(true);
					}

					vInsertIndex = -1;
					var iLastInsertIndex = -1;
					for (var i = 0; i < aDiff.length; i++) {
						var oDiff = aDiff[i],
							iDiffIndex = oDiff.index,
							oContext = aContexts[iDiffIndex];

						if (oDiff.type == "delete") {
							if (vInsertIndex != -1) {
								// this record is deleted while the chunk is getting build
								this.applyChunk(vInsertIndex);
								iLastInsertIndex = -1;
								vInsertIndex = -1;
							}

							this.deleteListItem(iDiffIndex);
						} else if (oDiff.type == "insert") {
							if (vInsertIndex == -1) {
								// the subsequent of items needs to be inserted at this position
								vInsertIndex = iDiffIndex;
							} else if (iLastInsertIndex > -1 && iDiffIndex != iLastInsertIndex + 1) {
								// this item is not simply appended to the last one but has been inserted
								this.applyChunk(vInsertIndex);
								vInsertIndex = iDiffIndex;
							}

							this.insertListItem(oContext, oBindingInfo, iDiffIndex);
							iLastInsertIndex = iDiffIndex;
						}
					}
				}

				if (bFromScratch) {
					this.rebuildListItems(aContexts, oBindingInfo, true);
				} else {
					// set the binding context of items inserting/deleting entries shifts the index of all following items
					this.updateItemsBindingContext(aContexts, oBindingInfo.model);
					this.applyChunkAsync(vInsertIndex);
				}
			}

			if (!this._bDataRequested) {
				this._onAfterPageLoaded(sChangeReason);
			}
		},

		_onBindingDataReceivedListener: function(oEvent) {
			if (this._bSkippedItemsUpdateUntilDataReceived && !oEvent.getParameter("data") /* request failed */) {
				this._bSkippedItemsUpdateUntilDataReceived = false;
				this.destroyListItems();
				this._onAfterPageLoaded();
			}
		},

		_updateTriggerDelayed: function(bLoading) {
			if (this._oControl.getGrowingScrollToLoad()) {
				this._iTriggerTimer && window.cancelAnimationFrame(this._iTriggerTimer);
				this._iTriggerTimer = window.requestAnimationFrame(this._updateTrigger.bind(this, bLoading));
			} else {
				this._updateTrigger(bLoading);
			}
		},

		// updates the trigger state
		_updateTrigger : function(bLoading) {
			var oTrigger = this._oTrigger,
				oControl = this._oControl,
				bVisibleItems = oControl && oControl.getVisibleItems().length > 0;

			// If there are no visible columns or items then also hide the trigger.
			if (!oTrigger || !oControl || !bVisibleItems || !oControl.shouldRenderItems() || !oControl.getDomRef()) {
				return;
			}

			var oBinding = oControl.getBinding("items");
			if (!oBinding) {
				return;
			}

			// update busy state
			oTrigger.setBusy(bLoading);
			oTrigger.$().toggleClass("sapMGrowingListBusyIndicatorVisible", bLoading);

			if (bLoading) {
				oTrigger.setActive(false);
				oControl.$("triggerList").css("display", "");
			} else {
				var aItems = oControl.getItems(true),
					iItemsLength = aItems.length,
					iBindingLength = oBinding.getLength() || 0,
					bLengthFinal = oBinding.isLengthFinal(),
					bHasScrollToLoad = oControl.getGrowingScrollToLoad(),
					oTriggerDomRef = oTrigger.getDomRef();

				// put the focus to the newly added item if growing button is pressed
				if (oTriggerDomRef && oTriggerDomRef.contains(document.activeElement)) {
					(aItems[this._iLastItemsCount] || aItems[iItemsLength - 1] || oControl).focus();
				}

				// show, update or hide the growing button
				if (!iItemsLength || !this._iLimit || !iBindingLength ||
					(bLengthFinal && this._iLimit >= iBindingLength) ||
					(bHasScrollToLoad && this._getHasScrollbars())) {
					oControl.$("triggerList").css("display", "none");
					oControl.$("listUl").removeClass("sapMListHasGrowing");
				} else {
					var oBundle = Core.getLibraryResourceBundle("sap.m");
					if (bLengthFinal) {
						oControl.$("triggerInfo").css("display", "block").text(this._getListItemInfo());
						var aCounts = this._getItemCounts();
						oControl.$("triggerMessage").text(oBundle.getText(oControl.isA("sap.m.Table") ? "LOAD_MORE_ROWS_ACC_WITH_COUNT" : "LOAD_MORE_DATA_ACC_WITH_COUNT", aCounts));
					} else {
						oControl.$("triggerMessage").text(oBundle.getText("LOAD_MORE_DATA_ACC"));
					}

					oControl.$("triggerList").css("display", "");
					oControl.$("listUl").addClass("sapMListHasGrowing");
					oTrigger.$().removeClass("sapMGrowingListBusyIndicatorVisible");

					if (oControl.isA("sap.m.Table") && !oControl.hasPopin()) {
						this.adaptTriggerButtonWidth(oControl, oTriggerDomRef);
					}
				}

				// store the last item count to be able to focus to the newly added item when the growing button is pressed
				this._iLastItemsCount = this._oControl.getItems(true).length;

				// at the beginning we should scroll to last item
				if (bHasScrollToLoad && this._oScrollPosition === undefined && oControl.getGrowingDirection() == ListGrowingDirection.Upwards) {
					this._oScrollPosition = {
						left : 0,
						top : 0
					};
				}

				// scroll to last position
				if (iItemsLength > 0 && this._oScrollPosition) {
					var oScrollDelegate = this._oScrollDelegate,
						oScrollPosition = this._oScrollPosition;

					oScrollDelegate.scrollTo(oScrollPosition.left, oScrollDelegate.getScrollHeight() - oScrollPosition.top);
					this._oScrollPosition = null;
				}
			}
		},

		adaptTriggerButtonWidth: function(oControl, oTriggerDomRef) {
			// adapt trigger button width if dummy col is rendered
			if (oControl.shouldRenderDummyColumn() && oControl.$("listUl").hasClass("sapMListHasGrowing")) {
				if (!oTriggerDomRef) {
					oTriggerDomRef = this._oTrigger.getDomRef();
				}

				window.requestAnimationFrame(function() {
					if (oControl.bIsDestroyed) {
						return;
					}

					var sCalWidth = Array.from(oControl.getDomRef("tblHeader").childNodes).slice(0, -1).map(function(oDomRef) {
						var sWidth = oDomRef.getAttribute("data-sap-width");
						if (!sWidth || !sWidth.includes("%")) {
							return oDomRef.getBoundingClientRect().width + "px";
						} else {
							return sWidth;
						}
					}).join(" + ");
					// 1px is borderLeft of the dummyCell
					oTriggerDomRef.style.width = "calc(" + sCalWidth + " + 1px)";
					oTriggerDomRef.classList.add("sapMGrowingListDummyColumn");
				});
			}
		}
	});

	GrowingEnablement.createItem = function(oContext, oBindingInfo, sIdSuffix) {
		var oItem = oBindingInfo.factory(ManagedObjectMetadata.uid(sIdSuffix ? sIdSuffix : "clone"), oContext);
		return oItem.setBindingContext(oContext, oBindingInfo.model);
	};

	return GrowingEnablement;

});