sap.ui.define([
	"sap/ui/demo/iconexplorer/controller/BaseController",
	"sap/ui/core/IconPool",
	"sap/ui/model/json/JSONModel",
	"sap/ui/demo/iconexplorer/model/formatter",
	"sap/ui/model/Filter",
	"sap/ui/model/FilterOperator",
	"sap/ui/Device",
	"sap/m/MessageToast",
	"sap/m/Label",
	"sap/m/ToggleButton",
	"sap/m/library",
	"sap/ui/core/theming/Parameters",
	"sap/ui/core/Fragment",
	"sap/ui/core/Core"
], function(
	BaseController,
	IconPool,
	JSONModel,
	formatter,
	Filter,
	FilterOperator,
	Device,
	MessageToast,
	Label,
	ToggleButton,
	mobileLibrary,
	Parameters,
	Fragment,
	oCore
) {
	"use strict";

	var TYPING_DELAY = 200; // ms

	return BaseController.extend("sap.ui.demo.iconexplorer.controller.Overview", {

		formatter: formatter,

		/* =========================================================== */
		/* lifecycle methods                                           */
		/* =========================================================== */

		/**
		 * Called when the overview controller is instantiated.
		 * @public
		 */
		onInit : function () {
			var oViewModel,
				oTagModel;

			this._oPreviousQueryContext = {};
			this._oCurrentQueryContext = null;

			// model used to manipulate control states
			oViewModel = new JSONModel({
				growingThreshold : 200,
				iconFilterCount: this.getResourceBundle().getText("overviewTabAllInitial"),
				overviewNoDataText : this.getResourceBundle().getText("overviewNoDataText"),
				fontName: "",
				iconPath : "",
				busy : true
			});
			this.setModel(oViewModel, "view");

			// helper model for managing the tag selection
			oTagModel = new JSONModel();
			this.setModel(oTagModel, "tags");

			// register to both new and legacy pattern to not break bookmarked URLs
			this.getRouter().getRoute("legacy").attachPatternMatched(this._updateUI, this);
			this.getRouter().getRoute("overview").attachPatternMatched(this._updateUI, this);
		},

		/**
		 * Focus search field after rendering for immediate searchability
		 */
		onAfterRendering: function () {
			setTimeout(function () {
				this.byId("searchField").focus();
			}.bind(this),0);
		},

		/* =========================================================== */
		/* event handlers                                              */
		/* =========================================================== */

		/**
		 * Event handler for navigating back.
		 * We navigate back in the browser history
		 * @public
		 */
		onNavBack : function() {
			this.getRouter().navTo("home");
		},

		/**
		 * Event handler for the navigation button, opens a Popover with all fonts
		 * @public
		 * @param oEvent
		 */
		onSelectFont : function(oEvent) {
			var sFontName = this.getModel("view").getProperty("/fontName"),
				aListItems = this.byId("selectFontList").getItems(),
				oSelectedItem = aListItems.filter(function (oListItem) {
					return oListItem.getCustomData()[0].getValue() === sFontName;
				}).pop();

			this.byId("selectFont").openBy(oEvent.getSource());
			this.byId("selectFontList").setSelectedItem(oSelectedItem);
		},

		/**
		 * Event handler for pressing a list item in the navigation popover
		 * @public
		 * @param oEvent
		 */
		onChangeFont : function(oEvent) {
			var oListItem = oEvent.getParameter("listItem"),
				sSelectedFont = oListItem.getCustomData()[0].getValue();

			this.getModel("view").setProperty("/busy", true, null, true);
			this.getRouter().navTo("overview", {
				query: {
					tab: this._oCurrentQueryContext.tab
				},
				fontName: sSelectedFont
			});
			this.byId("selectFont").close();
		},

		/**
		 * Triggered by the table's 'updateFinished' event and by the other tabs change: after new table
		 * data is available, this handler method updates the icon counter.
		 * @public
		 */
		onUpdateFinished : function () {
			function getRootControl(oEvent) {
				if (oEvent.srcControl.getMetadata().getName().search("CustomListItem") >= 0) {
					// keyboard event, get child
					return oEvent.srcControl.getContent()[0];
				} else if (oEvent.srcControl.getMetadata().getName().search("VerticalLayout") >= 0) {
					// layout clicked, just return it
					return oEvent.srcControl;
				} else {
					// inner control clicked, return parent
					return oEvent.srcControl.getParent();
				}
			}
			// show total count of items
			this.getModel("view").setProperty("/iconFilterCount", this.byId("results").getBinding(this._sAggregationName).getLength(), null, true);
			// register press callback for grid and visual mode
			if (this._oCurrentQueryContext.tab === "grid" || this._oCurrentQueryContext.tab === "visual") {
				if (!this._oPressLayoutCellDelegate) {
					this._oPressLayoutCellDelegate = {
						// tap: set selected and hoverable class
						ontap: function (oEvent) {
							var oBindingContext = oEvent.srcControl.getBindingContext();
							var oRoot = getRootControl(oEvent);

							// prevent setting styles on the favorite button
							if (oRoot.getMetadata().getName().search("ToggleButton") >= 0 || oEvent.srcControl.getMetadata().getName().search("ToggleButton") >= 0) {
								return;
							}

							// select the icon
							this._updateHash("icon", oBindingContext.getProperty("name"));
						}.bind(this),
						// touchstart: set item active and remove hoverable class, invert icon color
						ontouchstart: function (oEvent) {
							var oRoot = getRootControl(oEvent);

							oRoot.addStyleClass("sapMLIBActive");
							oRoot.removeStyleClass("sapMLIBHoverable");
							if (!this._sNormalIconColor) {
								this._sNormalIconColor = oRoot.$().find(".sapUiIcon").control(0).getColor();
							}
							oRoot.$().find(".sapUiVltCell > .sapUiIcon").control().forEach(function (oIcon) {
								oIcon.setColor(Parameters.get("sapUiTextInverted"));
							});
						}.bind(this),
						// touchend: remove active class, reset icon color
						ontouchend: function (oEvent) {
							var oRoot = getRootControl(oEvent);

							oRoot.removeStyleClass("sapMLIBActive");
							oRoot.$().find(".sapUiVltCell > .sapUiIcon").control().forEach(function (oIcon) {
								oIcon.setColor(this._sNormalIconColor);
							}.bind(this));
						}.bind(this)
					};
					// enter + space key: same as tab
					this._oPressLayoutCellDelegate.onsapenter = this._oPressLayoutCellDelegate.ontap;
				}

				// there is no addEventDelegateOnce so we remove and add it for all items
				var aItems = this.byId("results").getAggregation(this._sAggregationName);
				if (aItems) {
					aItems.forEach(function (oItem) {
						oItem.removeEventDelegate(this._oPressLayoutCellDelegate);
						oItem.addEventDelegate(this._oPressLayoutCellDelegate);
					}.bind(this));
				}
			}
		},

		/**
		 * Event handler when a table item gets pressed
		 * @param {sap.ui.base.Event} oEvent the table selectionChange event
		 * @public
		 */
		onSelectionChange : function (oEvent) {
			var sModelName = (this._oCurrentQueryContext.tab === "favorites" ? "fav" : undefined),
				oItem = oEvent.getParameter("listItem");

			this._updateHash("icon", oItem.getBindingContext(sModelName).getProperty("name"));
		},

		/**
		 * Event handler for changing the icon browse mode.
		 * We update the hash in case a new tab is selected.
		 * @param {sap.ui.base.Event} oEvent the tabSelect event of the IconTabBar
		 */
		onTabSelect : function (oEvent){
			if (oEvent.getParameter("selectedKey") === "all") {
				this._updateHash("reset", "all");
			} else {
				this._updateHash("tab", oEvent.getParameter("selectedKey"));
			}
		},

		/**
		 * Searches the icons and filters the bindings accordingly
		 * @param {sap.ui.base.Event} oEvent the liveChange event of the SearchField
		 * @public
		 */
		onSearch: function (oEvent) {
			this._updateHash("search", oEvent.getParameter("newValue"));
		},

		/**
		 * Event handler for the category selection
		 * @param {sap.ui.base.Event} oEvent the selectionChange event
		 */
		onSelectCategory : function (oEvent) {
			this._updateHash("cat", (oEvent.getParameter("selectedItem") ? oEvent.getParameter("selectedItem").getKey() : undefined));
		},

		/**
		 * Searches the icons for a single tag only and filters the bindings accordingly
		 * @param {sap.ui.base.Event} oEvent the liveChange event of the SearchField
		 * @public
		 */
		onTagSelect: function (oEvent) {
			this._updateHash("tag", oEvent.getParameter("pressed") === false ? "" : oEvent.getSource().getText());
		},

		/**
		 * Toggles the favorite state of an icon when the user presses on the favorite button
		 * @param {sap.ui.base.Event} oEvent the press event of the ToggleButton
		 * @public
		 */
		onToggleFavorite: function (oEvent) {
			var sModelName = (this._oCurrentQueryContext.tab === "favorites" ? "fav" : undefined),
				oBindingContext = oEvent.getSource().getBindingContext(sModelName),
				sName = oBindingContext.getProperty("name"),
				oResourceBundle = this.getResourceBundle(),
				bFavorite = this.getModel("fav").toggleFavorite(oBindingContext);

			if (bFavorite) {
				MessageToast.show(oResourceBundle.getText("overviewFavoriteAdd", [sName]));
			} else {
				MessageToast.show(oResourceBundle.getText("overviewFavoriteRemove", [sName]));
			}
		},

		/**
		 * Switches between code and icon copy mode
		 * @param {sap.ui.base.Event} oEvent the select event of the RadioButtonGroup
		 */
		onCopySelect: function (oEvent) {
			var iIndex = oEvent.getParameter("selectedIndex");

			if (iIndex === 0) {
				this.byId("previewCopy").getContent()[0].setVisible(true);
				this.byId("previewCopy").getContent()[1].setVisible(false);
			} else {
				this.byId("previewCopy").getContent()[1].setVisible(true);
				this.byId("previewCopy").getContent()[0].setVisible(false);
			}
		},

		/**
		 * Copies the value of the code input field to the clipboard and displays a message
		 * @public
		 */
		onCopyCodeToClipboard: function () {
			var sString = this.byId("previewCopyCode").getValue(),
				oResourceBundle = this.getResourceBundle(),
				sSuccessText, sExceptionText;

			sSuccessText = oResourceBundle.getText("previewCopyToClipboardSuccess", [sString]);
			sExceptionText = oResourceBundle.getText("previewCopyToClipboardFail", [sString]);
			this._copyStringToClipboard(sString, sSuccessText, sExceptionText);
		},

		/**
		 * Copies the unicode part from the input field to the clipboard and displays a message
		 * @public
		 */
		onCopyUnicodeToClipboard: function () {
			var oResourceBundle = this.getResourceBundle(),
				sSuccessText, sExceptionText,
				sIconName = this.byId("preview").getBindingContext().getObject().name,
				sString = this.getModel().getUnicodeHTML(sIconName);
			sString = sString.substring(2, sString.length - 1);
			sSuccessText = oResourceBundle.getText("previewCopyUnicodeToClipboardSuccess", [sString]);
			sExceptionText = oResourceBundle.getText("previewCopyUnicodeToClipboardFail", [sString]);
			this._copyStringToClipboard(sString, sSuccessText, sExceptionText);
		},

		/**
		 * Copies the icon to the clipboard and displays a message
		 * @public
		 */
		onCopyIconToClipboard: function () {
			var sString = this.byId("previewCopyCode").getValue(),
				oResourceBundle = this.getResourceBundle(),
				sSuccessText, sExceptionText,
				sIcon = this.getModel().getUnicode(sString);

			sSuccessText = oResourceBundle.getText("previewCopyToClipboardSuccess", [sString]);
			sExceptionText = oResourceBundle.getText("previewCopyToClipboardFail", [sString]);
			this._copyStringToClipboard(sIcon, sSuccessText, sExceptionText);
		},

		/**
		 * Shows a random icon in the preview pane
		 * @public
		 */
		onSurpriseMe: function () {
			var sFontName = this.getModel("view").getProperty("/fontName"),
				aIcons = this.getModel().getProperty("/" + sFontName + "/groups/0/icons"),
				oRandomItem = aIcons[Math.floor(Math.random() * aIcons.length)];

			this._updateHash("icon", oRandomItem.name);
		},

		/**
		 * Downloads the icon font relatively from the UI5 delivery
		 * @public
		 */
		onDownload: function () {
			var sFontName = this.getModel("view").getProperty("/fontName");
			var oConfigs = this.getOwnerComponent()._oFontConfigs;
			var sDownloadURI = oConfigs[sFontName].downloadURI || oConfigs[sFontName].fontURI;

			if (oCore.getConfiguration().getTheme() === "sap_horizon") {
				sDownloadURI = oConfigs[sFontName].downloadURIV5 || sDownloadURI;
			}

			mobileLibrary.URLHelper.redirect(sDownloadURI + sFontName + ".ttf");
		},

		/* =========================================================== */
		/* internal method                                             */
		/* =========================================================== */

		/**
		 * Copies the string to the clipboard and displays a message
		 * @param {string} copyText the text string that has to be copied to the clipboard
		 * @private
		 */
		_copyStringToClipboard: function (copyText, successText, exceptionText) {
			var oTemp = document.createElement("input");

			try {
				document.body.append(oTemp);
				oTemp.value = copyText;
				oTemp.select();
				document.execCommand("copy");
				oTemp.remove();

				MessageToast.show(successText);
			} catch (oException) {
				MessageToast.show(exceptionText);
			}
		},

		/**
		 * Shows the selected item on the object page
		 * On phones an additional history entry is created
		 * @param {string} sIcon the icon name to be previewed
		 * @private
		 */
		_previewIcon : function (sIcon) {
			this.getModel().iconsLoaded().then(function () {
				var sPath = this.getModel().getIconPath(sIcon);

				if (sPath) {
					// bind the preview to the item path
					this.byId("preview").bindElement({
						path: sPath
					});

					// update the group information with a timeout as this task takes some time to calculate
					setTimeout(function () {
						var aGroups = this.getModel().getIconGroups(sIcon);
						this.byId("categoryInfo").setText(aGroups.join(", "));
					}.bind(this), 0);

					// update unicode info
					this.byId("unicodeInfo").setText(this.getModel().getUnicodeHTML(sIcon));
				}
			}.bind(this));
		},

		/**
		 * Updates the UI according to the hash
		 * @param {sap.ui.base.Event} oEvent the routing event
		 * @private
		 */
		_updateUI: function (oEvent) {
			var oArguments = oEvent.getParameter("arguments"),
				sFontName = oArguments.fontName || "SAP-icons",
				oQuery = oArguments["?query"],
				bInitial = false,
				oViewModel = this.getModel("view");

			// set a default query object in case no hash is defined
			if (!oQuery) {
				oQuery = {
					tab: "grid"
				};
			}

			// keep the previous item if all tab (remove filters) has been pressed
			if (oQuery.tab === "all") {
				oQuery.tab = this._oPreviousQueryContext.tab;
			}

			// check tab value against an allowlist
			var aValidKeys = ["details", "grid", "visual", "favorites"];
			if (aValidKeys.indexOf(oQuery.tab) < 0) {
				oQuery.tab = "grid";
			}

			// store current context
			if (!this._oCurrentQueryContext) {
				bInitial = true;
			}
			this._oCurrentQueryContext = oQuery;

			// helper variables for updating the UI pieces
			var bFontChanged = sFontName !== oViewModel.getProperty("/fontName");
			var bTabChanged = this._oPreviousQueryContext.tab !== oQuery.tab;
			var bCategoryChanged = this._oPreviousQueryContext.cat !== oQuery.cat;
			var bSearchChanged = this._oPreviousQueryContext.search !== oQuery.search;
			var bTagChanged = this._oPreviousQueryContext.tag !== oQuery.tag;
			var bIconChanged = this._oPreviousQueryContext.icon !== oQuery.icon;

			this._sAggregationName = "items";

			this.getOwnerComponent().iconsLoaded().then(function () {
				// bind the view if the displayed icon font changes or is not set yet
				if (bFontChanged) {
					// avoid refresh of preview area when setting new properties since we hide it anyway
					this.byId("preview") && this.byId("preview").unbindElement();

					// store the current font name in the view model and set the path to the new font (async)
					oViewModel.setProperty("/fontName", sFontName, null, true);
					oViewModel.setProperty("/iconPath", (sFontName === "SAP-icons" ? "" : sFontName + "/"), null, true);

					// update the view to the new path
					this.getView().bindElement({
						path: "/" + sFontName,
						suspend: true
					});

					// set the font on the icon model
					this.getModel().setFont(sFontName);
					oViewModel.setProperty("/busy", false, null, true);
				}

				// tab
				if (!this.byId("iconTabBar")) {
					return;
				}
				this.byId("iconTabBar").setSelectedKey(oQuery.tab);
				if (bTabChanged) {
					var oContent = this.byId("resultContainer").getContent();

					// destroy content item if available
					if (oContent.length === 1) {
						oContent.pop().destroy();
					}
					// uppercase first letter
					var sFragmentName = formatter.uppercaseFirstLetter(oQuery.tab);

					// add new content to the end of result container
					this._resultsLoaded = Fragment.load({
						id: this.getView().getId(),
						name: "sap.ui.demo.iconexplorer.view.browse." + sFragmentName,
						controller: this
					}).then(function(oFragmentContent){
						this.byId("resultContainer").addContent(oFragmentContent);
					}.bind(this));

					var bCategoriesVisible = !(Device.system.phone || oQuery.tab == "favorites");
					this.byId("categorySelection").setVisible(bCategoriesVisible);
				}

				this._resultsLoaded.then(function() {
					// icon
					if (oQuery.icon && bIconChanged) {
						this._previewIcon(oQuery.icon);
						this.byId("preview").setVisible(true);
						if (this.byId("preview").getLayoutData().getSize() === "0px") {
							this.byId("preview").getLayoutData().setSize("350px");
						}
					} else if (!oQuery.icon) {
						if (bInitial) {
							this._previewIcon("sap-ui5");
						}
						this.byId("preview").setVisible(false);
						this.byId("preview").getLayoutData().setSize("0px");
					}

					// category
					this.byId("categorySelection").setSelectedKey(oQuery.cat || "all");
					if ((oQuery.cat || bCategoryChanged || bFontChanged) && oQuery.tab !== "favorites") {
						if (bInitial || bFontChanged || bTabChanged) {
							this._selectCategory(oQuery);
						} else {
							clearTimeout(this._iCategorySelectionTimeout);
							this._iCategorySelectionTimeout = setTimeout(function () {
								this._selectCategory(oQuery);
							}.bind(this), TYPING_DELAY);
						}
					}

					// search & tags
					this.byId("searchField").setValue(oQuery.search);
					if (bInitial || bFontChanged || bSearchChanged || bTagChanged || bTabChanged) {
						// search
						if (bInitial || bFontChanged || bTabChanged) {
							this._searchIcons(oQuery.search, oQuery.tag);
						} else {
							clearTimeout(this._iSearchTimeout);
							this._iSearchTimeout = setTimeout(function () {
								this._searchIcons(oQuery.search, oQuery.tag);
							}.bind(this), TYPING_DELAY);
						}

						// tags
						if (bInitial || bFontChanged || bTabChanged) {
							if (oQuery.tab === "favorites") {
								this._aCategoryTags = undefined;
							}
							this._updateTags(oQuery);
						} else {
							clearTimeout(this._iTagTimeout);
							this._iTagTimeout = setTimeout(function () {
								this._updateTags(oQuery);
							}.bind(this), TYPING_DELAY);
						}
					}
				}.bind(this));

			}.bind(this));
		},

		/**
		 * Updates the hash with the current UI state
		 * @param {string} [sKey] the key of the query to be updated
		 * @param {string} [sValue] the value of the query to be updated
		 * @private
		 */
		_updateHash: function (sKey, sValue) {
			var oQuery = {};

			// deep copy of the context
			if (this._oCurrentQueryContext.tab) {
				oQuery.tab = this._oCurrentQueryContext.tab;
			}
			if (this._oCurrentQueryContext.icon) {
				oQuery.icon = this._oCurrentQueryContext.icon;
			}
			if (this._oCurrentQueryContext.search) {
				oQuery.search = this._oCurrentQueryContext.search;
			}
			if (this._oCurrentQueryContext.cat) {
				oQuery.cat = this._oCurrentQueryContext.cat;
			}
			if (this._oCurrentQueryContext.tag) {
				oQuery.tag = this._oCurrentQueryContext.tag;
			}

			// explicit reset for tags and search when the all item was pressed
			if (sKey === "reset") {
				delete oQuery.tag;
				delete oQuery.search;
				delete oQuery.cat;
			} else {
				// override the key value pair passed in as parameters
				if (sKey && sValue) {
					oQuery[sKey] = sValue;
				}

				// reset tags under the following conditions
				// - navigating from or to favorite tab
				// - category was changed
				// - emtpy tag value
				if (this._oCurrentQueryContext.tab !== oQuery.tab  && (this._oCurrentQueryContext.tab === "favorites" || oQuery.tab === "favorites") ||
					this._oCurrentQueryContext.cat !== oQuery.cat ||
					sKey === "tag" && !sValue) {
					delete oQuery.tag;
				}

				// reset search if no value has been passed
				if (sKey === "search" && !sValue) {
					delete oQuery.search;
				}

				// reset icon if no value has been passed
				if (sKey === "icon" && !sValue) {
					delete oQuery.icon;
				}
			}

			// call route with query parameter
			this.getRouter().navTo("overview", {
				fontName: this.getModel("view").getProperty("/fontName"),
				query: oQuery
			});

			// store previous context
			this._oPreviousQueryContext = this._oCurrentQueryContext;
			// store the new context
			this._oCurrentQueryContext = oQuery;
		},

		/**
		 * Does the real search after a short delay to improve the perceived performance of the app
		 * The following search modes can apply depending on the parameter values
		 * - search, no tag: reset the filters
		 * - search, no tag: search for the string in the name OR the tags of the icons
		 * - no search, tag: search for the tag in the tags of the icons
		 * - search, tag: search for the tag in the tags of the icons AND search for the string in the name OR the tags of the icons
		 * @param {string} sSearchValue the search string
		 * @param {string} sTagValue the tag string
		 * @private
		 */
		_searchIcons : function (sSearchValue, sTagValue) {
			if (sSearchValue || sTagValue) {
				// only initialize the filters if needed to save some time
				var aFilters = [],
					oFilterTags = (sTagValue ? new Filter("tagString", FilterOperator.Contains, sTagValue) : undefined),
					oFilterSearchName = (sSearchValue ? new Filter("name", FilterOperator.Contains, sSearchValue) : undefined),
					fnUnicodeCustomFilter = (sSearchValue ? this._unicodeFilterFactory(sSearchValue) : undefined),
					oFilterSearchUnicode = (sSearchValue ? new Filter("name", fnUnicodeCustomFilter) : undefined),
					oFilterSearchTags = (sSearchValue ? new Filter("tagString", FilterOperator.Contains, sSearchValue) : undefined),
					oFilterSearchNameTags = (sSearchValue ? new Filter({
						filters: [oFilterSearchTags, oFilterSearchName, oFilterSearchUnicode],
						and: false
					}) : undefined);

				// search for name
				if (sSearchValue) {
					aFilters.push(oFilterSearchNameTags);
				}
				// search for tags
				if (sTagValue) {
					aFilters.push(oFilterTags);
				}
				if (aFilters.length <= 1) {
					// search or tag: just take the filter
					this._vFilterSearch = aFilters;
				} else {
					// search and tag: tags contain the tag value and search for name or tags
					this._vFilterSearch = [new Filter({
						filters: aFilters,
						and: true
					})];
				}
			} else {
				// reset search
				this._vFilterSearch = [];
			}

			// filter icon list
			this._resultsLoaded.then(function () {
				var oResultBinding = this.byId("results").getBinding(this._sAggregationName);
				if (oResultBinding !== undefined) {
					oResultBinding.filter(this._vFilterSearch);
					this.getModel("view").setProperty("/overviewNoDataText", this.getResourceBundle().getText("overviewNoDataWithSearchText"), null, true);
				}
			}.bind(this));
		},

		/**
		 * Factory that produces the custom filter for the given unicode query
		 * @param {string} query the query text that has been entered in the search field and contains the unicode character
		 * @return {function} the custom filter function that takes the name of the icon and returns true if the icon's unicode contains the query string
		 * @private
		 */
		_unicodeFilterFactory: function(query) {
			return function (name) {
				var sUnicode = this.getModel().getUnicodeHTML(name.toLowerCase());
				return sUnicode.indexOf(query) !== -1;
			}.bind(this);
		},

		/**
		 * Event handler for the category selection
		 * @param {object} oQuery the query object from the routing event
		 * @private
		 */
		_selectCategory: function (oQuery) {
			var sGroupPath = this.getModel().getGroupPath(oQuery.cat);

			// rebind the result set to the current group
			this.byId("results").bindAggregation(this._sAggregationName, {
				path: sGroupPath + "/icons",
				length: this.getModel("view").getProperty("/growingThreshold"),
				template: this.byId("results").getBindingInfo(this._sAggregationName).template.clone(),
				templateShareable: true,
				events: {
					change: this.onUpdateFinished.bind(this)
				},
				suspended: true
			});
			// apply filters
			this._resultsLoaded.then(function () {
				this.byId("results").getBinding(this._sAggregationName).filter(this._vFilterSearch);
			}.bind(this));

			// update tags
			this._aCategoryTags = this.getModel().getProperty(sGroupPath + "/tags");
			// update tag bar directly with all tags of this category when no search or tag is selected
			if (!oQuery.tag && !oQuery.search) {
				this._updateTagSelectionBar(this._aCategoryTags);
			}
		},

		/**
		 * updates the tags to the currently available binding contexts
		 * @param {Object} oQuery the current query state
		 * @private
		 */
		_updateTags: function (oQuery) {
			// caution: it is really important to use getCurrentContexts and not getContexts here as the later modifies the binding
			this._resultsLoaded.then(function () {
				var aContexts = this.byId("results").getBinding(this._sAggregationName).getCurrentContexts(),
					aAllTags = [],
					aCurrentTags = [],
					bTagVisible = false,
					sFontName = this.getModel("view").getProperty("/fontName"),
					i;

				// collect all current tags from the result list
				for (i = 0; i < aContexts.length; i++) {
					aAllTags = aAllTags.concat(aContexts[i].getProperty("tags").map(function(oItem) { return oItem.name; }));
				}

				// no category selected yet: use all tags
				if (!this._aCategoryTags) {
					this._aCategoryTags = this.getModel().getProperty("/" + sFontName + "/groups/0/tags");
				}

				// filter tags to the currently visible
				for (i = 0; i < this._aCategoryTags.length; i++) {
					if (aAllTags.indexOf(this._aCategoryTags[i].name) >= 0) {
						this._aCategoryTags[i].pressed = (this._aCategoryTags[i].name === oQuery.tag);
						if (this._aCategoryTags[i].pressed) {
							bTagVisible = true;
						}
						aCurrentTags.push(this._aCategoryTags[i]);
					}
				}

				// add current tag if it is not visible yet (tag bar only contains the top [x] tags)
				if (oQuery.tag && !bTagVisible) {
					aCurrentTags.push({
						pressed : true,
						name : oQuery.tag
					});
				}

				// update model data and bind the tags
				this._updateTagSelectionBar(aCurrentTags);
			}.bind(this));
		},

		/**
		 * Binds tags to the tag selection bar and appends a label
		 * @param {object[]} aTags the tags to be bound
		 * @private
		 */
		_updateTagSelectionBar: function (aTags) {
			this.getModel("tags").setData([{"name" : ""}].concat(aTags));
			this.byId("tagSelection").bindAggregation("content", {
				path: "tags>/",
				length: 51,
				factory: this._tagSelectionFactory.bind(this)
			});
		},

		/**
		 * Factory function for filling the tag bar.
		 * First item is a label, then the tags are listed
		 * @param {string} sId the id for the control to be created
		 * @param {sap.ui.model.Context} oContext the binding context for the control to be created
		 * @return {sap.m.Label|sap.m.ToggleButton} the control for the toolbar
		 * @private
		 */
		_tagSelectionFactory: function (sId, oContext) {
			if (oContext.getProperty("name") === "") {
				return new Label(sId, {
					text: "{i18n>overviewTagSelectionLabel}"
				});
			} else {
				return new ToggleButton(sId, {
					text: "{tags>name}",
					pressed: "{tags>pressed}",
					press: [this.onTagSelect, this],
					ariaLabelledBy: this.byId("labelTags")
				});
			}
		}

	});
});
