/* eslint-disable no-trailing-spaces */
/*!
 * SAPUI5
 * (c) Copyright 2009-2025 SAP SE. All rights reserved.
 */

sap.ui.define([
	"sap/ui/thirdparty/jquery",
	"./library",
	"sap/ui/core/Control",
	"sap/ui/core/ResizeHandler",
	"sap/ui/core/format/DateFormat",
	"sap/ui/model/ClientListBinding",
	"sap/ui/model/FilterType",
	"sap/suite/ui/commons/TimelineNavigator",
	"sap/suite/ui/commons/util/DateUtils",
	"sap/suite/ui/commons/util/ManagedObjectRegister",
	"sap/ui/model/json/JSONModel",
	"sap/ui/model/Sorter",
	"sap/ui/model/Filter",
	"sap/ui/model/FilterOperator",
	"sap/ui/base/ManagedObject",
	"sap/suite/ui/commons/TimelineItem",
	"sap/suite/ui/commons/TimelineRenderManager",
	"sap/ui/core/delegate/ScrollEnablement",
	"sap/ui/base/Object",
	"sap/base/assert",
	"sap/base/Log",
	"./TimelineRenderer"
], function (jQuery, library, Control, ResizeHandler, DateFormat, ClientListBinding, FilterType, TimelineNavigator, DateUtils, ManagedObjectRegister,
			 JSONModel, Sorter, Filter, FilterOperator, ManagedObject, TimelineItem, TimelineRenderManager, ScrollEnablement, BaseObject, assert, Log, TimelineRenderer) {
	"use strict";

	var ScrollingFadeout = library.TimelineScrollingFadeout,
		TimelineAlignment = library.TimelineAlignment,
		TimelineGroupType = library.TimelineGroupType,
		TimelineFilterType = library.TimelineFilterType,
		TimelineAxisOrientation = library.TimelineAxisOrientation;

	/**
	 * Constructor for a new Timeline.
	 *
	 * @param {string} [sId] ID for the new control, generated automatically if no id is given
	 * @param {object} [mSettings] Initial settings for the new control
	 *
	 * @class
	 * The timeline control shows entries (such as objects, events, or posts) in chronological order.
	 * <br>A common use case is to provide information about changes to an object, or events related to an
	 * object.
	 * These entries can be generated by the system (for example, value XY changed from A to B), or added manually.
	 * <br>There are two distinct variants of the timeline: basic and social. The basic timeline is read-only,
	 * while the social timeline offers a high level of interaction and collaboration.
	 * @extends sap.ui.core.Control
	 *
	 * @constructor
	 * @public
	 * @alias sap.suite.ui.commons.Timeline
	 * @ui5-metamodel This control/element also will be described in the UI5 (legacy) designtime metamodel
	 */
	var Timeline = Control.extend("sap.suite.ui.commons.Timeline", /** @lends sap.suite.ui.commons.Timeline.prototype */ {
		metadata: {
			library: "sap.suite.ui.commons",
			properties: {

				/**
				 * Defines the alignment of timeline posts relative to the timeline axis. This option can be used for single-sided timelines only. If the axisOrientation property is set to <code> Horizontal</code>, the <code>Right</code> value is interpreted as bottom alignment and the <code>Left</code> value as top alignment.
				 */
				alignment: {type: "sap.suite.ui.commons.TimelineAlignment", group: "Misc", defaultValue: "Right"},

				/**
				 * Defines the orientation of the timeline. Can be set to <code>Vertical</code> or <code>Horizontal</code>.
				 */
				axisOrientation: {
					type: "sap.suite.ui.commons.TimelineAxisOrientation",
					group: "Misc",
					defaultValue: "Vertical"
				},

				/**
				 * Data for the Timeline control.
				 * @deprecated Since version 1.46.0.
				 * As of version 1.46, this property was replaced by JSONModel context binding.
				 */
				data: {type: "object", group: "Misc", defaultValue: null, deprecated: true},

				/**
				 * Enables to add 'All' at the beginning of the filter list.
				 * @deprecated Since version 1.46.0.
				 * The filter list now includes a Select All check box, so the All radio button is
				 * no longer required. This property is ignored.
				 */
				enableAllInFilterItem: {type: "boolean", group: "Behavior", defaultValue: true, deprecated: true},

				/**
				 * Enables fetching data from backend instead of deriving filter values from the frontend values (displayed list).
				 * As of version 1.46, replaced by {@link sap.suite.ui.commons.Timeline#getEnableModelFilter}.
				 * @deprecated Since version 1.46.0.
				 * Use the enableModelFilter property instead.
				 */
				enableBackendFilter: {type: "boolean", group: "Misc", defaultValue: true, deprecated: true},

				/**
				 * When this property is set to <code>true</code>, the timeline displays a busy indicator when loading data. This busy indicator blocks the interaction with the items until the data loading is complete. It is strongly recommended to use this option. Loading data without a busy indicator may confuse the users.
				 */
				enableBusyIndicator: {type: "boolean", group: "Misc", defaultValue: true},

				/**
				 * If set to <code>true</code>, timeline posts are displayed on both sides of the timeline axis.
				 */
				enableDoubleSided: {type: "boolean", group: "Misc", defaultValue: false},

				/**
				 * Enables filtering directly on the binding level.
				 * If set to <code>true</code>, all filters are translated into the filters in the model binding. When the OData model is used, the filtering is performed on the backend side. This option is strongly recommended.
				 * If set to <code>false</code>, all entries from the model need to be fetched before they can be filtered on the frontend side. Please be aware that the <code>growingThreshold</code> property determines how many entries can be rendered. Timeline requests all entries from the model before it performs the filtering. Some models may have internal limits for the number of entries that can be used in bindings. Such limits can be set using {@link sap.ui.model.Model#setSizeLimit}.
				 * @since 1.46.0
				 */
				enableModelFilter: {type: "boolean", group: "Misc", defaultValue: true},

				/**
				 * If set to <code>true</code>, the Timeline control has its own scroll bar, with the scrolling taking place within the Timeline control itself. Scrolling is required if you want to enable the lazyLoading property.
				 */
				enableScroll: {type: "boolean", group: "Misc", defaultValue: true},

				/**
				 * Adds a <i>Reply</i> link to the posts on a social timeline that allows users to reply to posts. When a user adds a reply, an event is fired. This event should be handled by external code.
				 */
				enableSocial: {type: "boolean", group: "Misc", defaultValue: false},

				/**
				 * Title for the data filter. When a filter is applied, this title is displayed in the message strip along with the filter name.
				 */
				filterTitle: {type: "string", group: "Misc", defaultValue: null},

				/**
				 * If set to <code>true</code>, the <i>Show More</i> button is displayed when the user scrolls along the timeline axis.
				 */
				forceGrowing: {type: "boolean", group: "Misc", defaultValue: false},

				/**
				 * Groups the timeline posts by year.
				 * As of version 1.46, replaced by {@link sap.suite.ui.commons.Timeline#getGroupByType}.
				 * @deprecated Since version 1.46.0.
				 * Use the groupByType property instead.
				 */
				group: {type: "boolean", group: "Misc", defaultValue: false, deprecated: true},

				/**
				 * Groups the timeline posts by a field. Only fields that contain date values are supported.
				 */
				groupBy: {type: "string", group: "Misc", defaultValue: null},

				/**
				 * Groups the timeline posts by a time period, including year, quarter, month, week, and day. If set to <code>None</code>, no grouping is applied.
				 * If you specify a custom grouping function using the {@link sap.suite.ui.commons.Timeline#setCustomGrouping} method, this function overrides the groupByType property settings.
				 */
				groupByType: {type: "sap.suite.ui.commons.TimelineGroupType", group: "Misc", defaultValue: "None"},

				/**
				 * If set to <code>true</code>, the timeline displays a limited number of posts with a button to show more. The displayed posts limit can be set using the growingThreshold property.
				 * As of version 1.46, replaced by {@link sap.suite.ui.commons.Timeline#getGrowingThreshold}.
				 * @deprecated Since version 1.46.0.
				 * Use the growingThreshold property instead, setting it to 0 to disable growing.
				 */
				growing: {type: "boolean", group: "Misc", defaultValue: true, deprecated: true},

				/**
				 * Number of posts requested from the server at a time. Each time when the timeline needs to load more posts, it will request exactly this amount. The number of posts displayed in the timeline is increased by this number. If set to <code>0</code>, all posts are fetched and displayed on the initial load.
				 */
				growingThreshold: {type: "int", group: "Misc", defaultValue: 5},

				/**
				 * Sets the height of the Timeline.
				 * @since 1.46.0
				 */
				height: {type: "sap.ui.core.CSSSize", group: "Misc", defaultValue: ''},

				/**
				 * Enables the Lazy Loading feature that automatically loads more posts as the user scrolls along the timeline axis. This feature works only when the enableScroll property is set to <code>true</code>. See also {@link sap.suite.ui.commons.Timeline#getEnableScroll}.
				 * @since 1.46.0
				 */
				lazyLoading: {type: "boolean", group: "Dimension", defaultValue: false},

				/**
				 * This text is displayed when the control has no data. The default value is loaded from the component resource bundle.
				 */
				noDataText: {type: "string", group: "Misc", defaultValue: null},

				/**
				 * If set to <code>Area</code>, the timeline fades into the visible area margin.
				 * If set to <code>AreaWithButtons</code>, the timeline fades into the visible area margin and scroll buttons are displayed.
				 * If set to <code>None</code>, the fading effect is not applied.
				 * This feature works only when the enableScroll property is set to <code>true</code>. See also {@link sap.suite.ui.commons.Timeline#getEnableScroll}.
				 * @since 1.46.0
				 * @deprecated Since version 1.54.0. Not Fiori.
				 */
				scrollingFadeout: {
					type: "sap.suite.ui.commons.TimelineScrollingFadeout",
					group: "Misc",
					defaultValue: "None"
				},

				/**
				 * Sets the visibility of the filter in the timeline toolbar.<br>
				 * As of version 1.46, replaced by {@link sap.suite.ui.commons.Timeline#getShowHeaderBar}.
				 * @deprecated Since version 1.46.0.
				 * Use the showHeaderBar property instead.
				 */
				showFilterBar: {type: "boolean", group: "Misc", defaultValue: true, deprecated: true},

				/**
				 * Shows the timeline toolbar with search and filter options.
				 */
				showHeaderBar: {type: "boolean", group: "Misc", defaultValue: true},

				/**
				 * Shows an icon on the timeline axis as an anchor for each entry node. If set to <code>false</code>, all icons are replaced by dots.
				 */
				showIcons: {type: "boolean", group: "Misc", defaultValue: true},

				/**
				 * Show item filter in filter area.
				 */
				showItemFilter: {type: "boolean", group: "Misc", defaultValue: true},

				/**
				 * Sets the search field visibility on the timeline toolbar.
				 */
				showSearch: {type: "boolean", group: "Misc", defaultValue: true},

				/**
				 * Show sort icon in the header bar. For displaying this icon, sorting have to be allowed.
				 */
				showSort: {type: "boolean", group: "Misc", defaultValue: true},

				/**
				 * If set to <code>true</code>, a suggest event is fired when the user enters text into the search field. Changing the suggestionItems aggregation in the suggest event listener will display suggestions inside a popup.
				 * As of version 1.46, replaced by {@link sap.collaboration.components.feed.Component}.
				 * @since 1.26.1
				 * @deprecated Since version 1.46.0.
				 * Use the Group Feed Component instead.
				 */
				showSuggestion: {type: "boolean", group: "Behavior", defaultValue: true, deprecated: true},

				/**
				 * Shows the time range filter in the filter menu.
				 */
				showTimeFilter: {type: "boolean", group: "Misc", defaultValue: true},

				/**
				 * Indicates whether the data should be sorted. If set to <code>false</code> the data model's default sorting is applied.
				 */
				sort: {type: "boolean", group: "Misc", defaultValue: true},

				/**
				 * Path for dateTime in mode binding. Use this property when you use factory function for <code>TimeLineItem</code> creation.
				 * When every item has different path or the <code>datetime</code> is calculated in factory sorting, filtering and searching has to
				 * be done manually using events. If not set for factory binding filter and sorting won't work.
				 */
				dateTimePath: {type: "string", group: "Misc", defaultValue: ""},

				/**
				 * Whether the oldest item will be displayed first.
				 */
				sortOldestFirst: {type: "boolean", group: "Misc", defaultValue: false},

				/**
				 * Height of the timeline posts. It can be set to either of the following types of values:
				 * <ul>
				 * <li> Automatic &ndash; If set to <code>automatic</code>, the optimal post height is calculated automatically to fit the timeline height specified by the height parameter. This option works only in horizontal timelines and only when the timeline height is defined. In vertical timelines, this setting is ignored. </li>
				 * <li> Number &ndash; If set to a number, a corresponding number of text lines is displayed in every post.</li>
				 * <li> Pixels &ndash; If set to a number followed by <code>px</code>, such as <code>50px</code>, the closest number in lines is calculated and applied to the timeline posts, so that the post height corresponds to the specified number of pixels and the text lines are not cut off in the middle of a line.</li>
				 * <li> Any other valid {@link sap.ui.core.CSSSize CSSSize} value that is inserted into the text wrapper without being changed. </li>
				 * </ul>
				 */
				textHeight: {type: "string", group: "Misc", defaultValue: ''},

				/**
				 * Sets the width of the Timeline.
				 */
				width: {type: "sap.ui.core.CSSSize", group: "Dimension", defaultValue: '100%'}
			},
			defaultAggregation: "content",
			aggregations: {
				/**
				 * List of timeline posts.
				 */
				content: {type: "sap.suite.ui.commons.TimelineItem", multiple: true, singularName: "content"},

				/**
				 * A custom filter to be used instead of the default item filter. This control must have an <code>openBy</code> or <code>open</code> function that is used by the timeline to open the control.
				 * If the control has both functions, <code>openBy</code> is used.
				 */
				customFilter: {type: "sap.ui.core.Control", multiple: false},

				/**
				 * Custom filter criteria for the items filter.
				 */
				filterList: {
					type: "sap.suite.ui.commons.TimelineFilterListItem",
					multiple: true,
					singularName: "filterList"
				},

				/**
				 * Items for suggestions.
				 * As of version 1.46, replaced by {@link sap.collaboration.components.feed.Component}.
				 * @deprecated Since version 1.46.0.
				 * Use the Group Feed Component instead.
				 */
				suggestionItems: {
					type: "sap.m.StandardListItem",
					multiple: true,
					singularName: "suggestionItem",
					deprecated: true
				}
			},
			events: {
				/**
				 * This event is fired when a new post is added.
				 * As of version 1.46, replaced by {@link sap.collaboration.components.feed.Component}.
				 * @deprecated Since version 1.46.0.
				 * Use the Group Feed Component instead.
				 */
				addPost: {
					deprecated: true,
					parameters: {

						/**
						 * Post message text.
						 */
						value: {type: "string"}
					}
				},

				/**
				 * This event is fired when the user clicks the Close button on a message strip.
				 */
				customMessageClosed: {},

				/**
				 * This event is fired when the user clicks the filter icon in the timeline toolbar and opens the filter.
				 */
				filterOpen: {},

				/**
				 * This event is fired when filtering is applied to timeline posts or when a search term is entered into the search field. This event can be fired only if the enableModelFilter property is set to <code>true</code>.
				 */
				filterSelectionChange: {
					parameters: {

						/**
						 * Type of filter that has changed, can be an item filter or a time range filter.
						 */
						type: {type: "sap.suite.ui.commons.TimelineFilterType"},

						/**
						 * Search term entered into the search field.
						 */
						searchTerm: {type: "string"},

						/**
						 * The first criteria selected in the filter criteria list (required for backward compatibility).
						 */
						selectedItem: {type: "string"},

						/**
						 * An array with all selected filter criteria.
						 */
						selectedItems: {type: "object"},

						/**
						 * An object specifying the start date (<code>from</code>) and the end date (<code>to</code>) of the time range.
						 */
						timeKeys: {type: "object"},

						/**
						 * Is set to <code>true</code> when this event is fired by clearing the filter.
						 */
						clear: {type: "boolean"}
					}
				},

				/**
				 * This event is fired when the "More" button is clicked.
				 */
				grow: {},

				/**
				 * This event is fired for every timeline post when data filtering is being applied. It indicates whether this post is included in the filtering and why. Can be used only when the enableModelFilter property is set to <code>false</code>.
				 */
				itemFiltering: {
					parameters: {

						/**
						 * Timeline post that is currently being filtered.
						 */
						item: {type: "sap.suite.ui.commons.TimelineItem"},

						/**
						 * Reasons why the post is being filtered. Empty if the post is not being filtered. There are three properties available in the object: <code>Search</code>, <code>Time</code>, and <code>Data</code>. You can call preventDefault to the event object to prevent the result.
						 */
						reasons: {type: "object"},

						/**
						 * Array with keys used for data filtering.
						 */
						dataKeys: {type: "object"},

						/**
						 * An object specifying the start date (<code>from</code>) and the end date (<code>to</code>) of the time range.
						 */
						timeKeys: {type: "object"},

						/**
						 * The text string that has been typed into the search field.
						 */
						searchTerm: {type: "string"}
					}
				},

				/**
				 * This event is fired when a timeline post is selected.
				 */
				select: {
					parameters: {

						/**
						 * The timeline post that is selected.
						 */
						selectedItem: {type: "sap.suite.ui.commons.TimelineItem"},
						/**
						 * Indicates whether the selection was triggered by a user action or in any other way.
						 * <br>Set to <code>true</code> if the selection was triggered by a user action, for example, by clicking with a mouse or pressing the Enter key.
						 * <br>Set to <code>false</code> if the selection was triggered in a different way, for example, by shifting focus back from a Popover element or by pressing an arrow key.
						 * <br>For backward compatibility, the <code>select</code> event is fired whenever an item gets focus.
						 */
						userAction: {type: "boolean"}
					}
				},

				/**
				 * This event is fired when the user enters text into the search field, in cases when the showSuggestion property is set to <code>true</code>. Changing the suggestionItems aggregation will show the suggestions inside a popup.
				 * @since 1.26.1
				 * @deprecated Since version 1.46.0.
				 * Replaced by {@link sap.collaboration.components.feed.Component}.
				 */
				suggest: {
					deprecated: true,
					parameters: {

						/**
						 * The text string that has been typed into the search field.
						 */
						suggestValue: {type: "string"}
					}
				},

				/**
				 * This event is fired when a suggested post is selected in the suggestions popup list. This event is only fired when the showSuggestion property is set to <code>true</code> and there is at least one post in the suggestions popup list. See also the suggestionItems aggregation.
				 * @since 1.26.1
				 * @deprecated Since version 1.46.0.
				 * Replaced by {@link sap.collaboration.components.feed.Component}.
				 */
				suggestionItemSelected: {
					deprecated: true,
					parameters: {

						/**
						 * The item selected in the suggestions popup.
						 */
						selectedItem: {type: "sap.ui.core.Item"}
					}
				}
			},
			associations: {

				/**
				 * Assigns label to a control using the ID of the associated control. (Refer WAI-ARIA attribute aria-labelledby).
				 */
				ariaLabelledBy: { type: "sap.ui.core.Control", multiple: true, singularName: "ariaLabelledBy" }
			},
			designTime: true
		}
	});

	/**
	 * Adds a new property to the destination object. If this new property is set to a function,
	 * any getter or setter methods invoked on this new property will not return the function
	 * itself but rather its return value
	 *
	 * @param {Object} destinationObject object which is injected by new property
	 * @param {String} propertyName name of the new property
	 * @private
	 */
	function generateDynamicDateProperty(destinationObject, propertyName) {
		var _propertyDate;
		Object.defineProperty(destinationObject, propertyName, {
			get: function () {
				return typeof _propertyDate === "function" ? _propertyDate() : _propertyDate;
			},
			set: function (value) {
				_propertyDate = value;
			}
		});
	}

	var resourceBundle = sap.ui.getCore().getLibraryResourceBundle("sap.suite.ui.commons"),
		SortOrder = Object.freeze({
			ASCENDING: "ASCENDING",
			DESCENDING: "DESCENDING"
		}),
		DateRoundType = Object.freeze({
			UP: "UP",
			DOWN: "DOWN",
			NONE: "NONE"
		}),
		// used for displaying formatted date in group header text
		DateFormats = {
			Year: DateFormat.getDateInstance({
				pattern: "yyyy"
			}),
			Quarter: DateFormat.getDateInstance({
				pattern: "QQQQ yyyy"
			}),
			Month: DateFormat.getDateInstance({
				format: "yyyyMMMM"
			}),
			Week: DateFormat.getDateInstance({
				pattern: "w"
			}),
			Day: DateFormat.getDateInstance({
				style: "long"
			}),
			MonthDay: DateFormat.getDateInstance({
				style: "medium"
			})
		};

	/**
	 * Compatibility wrapper around javascript Array.findIndex function. findIndex is not supported by IE.
	 *
	 * @param {number} aArray Array to find index in.
	 * @param {function} fnCallback Function to execute on each value in the array.
	 * @returns {int} An index in the array if an element passes the test; otherwise, -1.
	 * @private
	 */
	function findIndex(aArray, fnCallback) {
		var i;

		assert(Array.isArray(aArray), "aArray must be an array.");

		if (typeof aArray.findIndex === "function") {
			return aArray.findIndex(fnCallback);
		}

		for (i = 0; i < aArray.length; i++) {
			if (fnCallback(aArray[i], i, aArray)) {
				return i;
			}
		}
		return -1;
	}

	Timeline.prototype.init = function () {
		// collection for all items (icons, buttons) which are not directly rendered but library stuff are used (like sap.m.buttons)
		// we use this collection for destroying all items by one function call when it is necessary
		this._objects = new ManagedObjectRegister();

		this.reset();

		// Create _endDate and _startDate properties
		generateDynamicDateProperty(this, "_endDate");
		generateDynamicDateProperty(this, "_startDate");

		// controls init
		this._initControls();

		this.setBusyIndicatorDelay(0);
	};

	/* =========================================================== */
	/* API methods */
	/* =========================================================== */
	/**
	 * Resets the start date and end date of the timeline, so that they are calculated once again. Call this method when changing the date of a timeline post.
	 * @public
	 */
	Timeline.prototype.resetTimeLimits = function () {
		this._maxDate = null;
		this._minDate = null;
	};

	/**
	 * Adds a custom filter in addition to the default filters: items filter, time range filter, and search.
	 * @param {string} sFilterId A unique filter ID that the caller can use to manipulate the filter after it has been
	 * created, for example, to remove it.
	 * @param {sap.ui.model.Filter} oFilter Filter object.
	 * @public
	 */
	Timeline.prototype.setCustomModelFilter = function (sFilterId, oFilter) {
		var oBinding = this.getBinding("content");

		if (oBinding) {
			var filters = oBinding.aFilters || [];

			// remove filter id from collection
			var filterIndex = findIndex(filters, function (oFilter) {
				return oFilter._customTimelineId === sFilterId;
			});

			if (filterIndex !== -1) {
				filters.splice(filterIndex, 1);
			}

			if (oFilter !== null) {
				oFilter._customTimelineId = sFilterId;
				filters.push(oFilter);
			}

			oBinding.filter(filters, FilterType.Control);
		}
	};

	/**
	 * Sets custom grouping function. This function must have one parameter that is a date object. This date object
	 * is used for grouping timeline posts. The function should return an object that has three properties:
	 * <ul>
	 * <li>
	 * <code>key</code> &ndash; The key of the related group.
	 * </li>
	 * <li>
	 * <code>title</code> &ndash;  The title of the related group.
	 * </li>
	 * <li>
	 * <code>date</code> &ndash; The same value as the one entered in the input parameter.
	 * </li>
	 * </ul>
	 *
	 * @param {function} fnGroupBy Grouping function.
	 * @public
	 */
	Timeline.prototype.setCustomGrouping = function (fnGroupBy) {
		var oBindingInfo = this.getBindingInfo("content");
		this._fnCustomGroupBy = fnGroupBy;

		if (oBindingInfo) {
			this._bindGroupingAndSorting(oBindingInfo);
			this.updateAggregation("content");
		}
	};


	/**
	 * Sets values for the time range filter.
	 * @param {object} [mArguments] Time filter arguments
	 * @param {Date} [mArguments.from] Start date of the time range filter.
	 * @param {Date} [mArguments.to] End date of the time range filter.
	 * @param {sap.suite.ui.commons.TimelineGroupType} [mArguments.type] The step of the time range filter scale. Can be set to day, month, quarter, or year.
	 * Week is currently not supported.
	 * @public
	 */
	Timeline.prototype.setCurrentTimeFilter = function (mArguments) {
		this._startDate = mArguments.from;
		this._endDate = mArguments.to;
		this._rangeFilterType = mArguments.type;
	};

	/**
	 * Sets values for the search field.
	 * @param {string} sSearchTerm Search term value.
	 * @public
	 */
	Timeline.prototype.setCurrentSearch = function (sSearchTerm) {
		this._objects.getSearchField().setValue(sSearchTerm);
	};

	/**
	 * Sets filter criteria keys.
	 * @param {string[]} aSelectedItemKeys Filter criteria keys to select. Can be also used as a single value.
	 * @public
	 */
	Timeline.prototype.setCurrentFilter = function (aSelectedItemKeys) {
		var that = this, //eslint-disable-line
			fnHasKey = function (sValue) {
				for (var i = 0; i < aSelectedItemKeys.length; i++) {
					if (aSelectedItemKeys[i] === sValue) {
						return true;
					}
				}
				return false;
			};

		if (!aSelectedItemKeys) {
			return;
		}

		if (!Array.isArray(aSelectedItemKeys)) {
			aSelectedItemKeys = [aSelectedItemKeys];
		}

		if (this._aFilterList.length === 0) {
			this._setFilterList();
		}

		that._currentFilterKeys = [];
		this._aFilterList.forEach(function (oItem) {
			var sKey = oItem.key;
			if (fnHasKey(sKey)) {
				that._currentFilterKeys.push({
					key: sKey,
					text: oItem.text ? oItem.text : oItem.key
				});
			}
		});

	};

	/**
	 * Returns all group items.
	 * @returns {sap.ui.core.Control[]} Timeline groups
	 * @public
	 */
	Timeline.prototype.getGroups = function () {
		return this._useBinding() ? this.getContent().filter(function (oItem) {
			return oItem._isGroupHeader;
		}) : this._aGroups;
	};

	/**
	 * Cleans up the element instance before destruction
	 * @public
	 */
	Timeline.prototype.exit = function () {
		this._objects.destroyAll();

		if (this.oItemNavigation) {
			this.removeDelegate(this.oItemNavigation);
			this.oItemNavigation.destroy();
			this.oItemNavigation = null;
		}
		if (this._oScroller) {
			this._oScroller.destroy();
			this._oScroller = null;
		}

		if (this.oResizeListener) {
			ResizeHandler.deregister(this.oResizeListener);
			this.oResizeListener = null;
		}
	};

	/**
	 * Refreshes the UI.
	 * @public
	 */
	Timeline.prototype.adjustUI = function () {
		this._performUiChanges(true);
	};

	/**
	 * Sets the message displayed in the filter message strip. This message overwrites the default filter message.
	 * @param {sap.suite.ui.commons.TimelineFilterType} sType Filter type. Supports only items filter (<code>Data</code>)
	 * and time range filter (<code>Time</code>).
	 * @param {string} sMessage The message that should replace the default message.
	 * @public
	 */
	Timeline.prototype.setModelFilterMessage = function (sType, sMessage) {
		if (sType === TimelineFilterType.Data) {
			this._dataMessage = sMessage;
		}
		if (sType === TimelineFilterType.Time) {
			this._rangeMessage = sMessage;
		}
	};

	/**
	 * Sets a custom message for the filter message strip. This message is appended to the default
	 * filter text.
	 * @param {string} sMessage Message to append.
	 * @public
	 */
	Timeline.prototype.setCustomFilterMessage = function (sMessage) {
		this._customFilterMessage = sMessage;
	};

	/**
	 * Replaces one of the default filters: items filter, time range filter, or search.
	 * @param {object} mArguments The object containing settings
	 * @param {sap.suite.ui.commons.TimelineFilterType} mArguments.type Type of the filter to replace.
	 * @param {sap.ui.model.Filter} mArguments.filter Filter object to specify the filter criteria.
	 * @param {boolean} mArguments.refresh If set to <code>true</code> (default), filters are automatically recreated,
	 * and the content is updated.
	 * @public
	 */
	Timeline.prototype.setModelFilter = function (mArguments) {
		switch (mArguments.type) {
			case TimelineFilterType.Data:
				this._dataFilter = mArguments.filter;
				break;

			case TimelineFilterType.Time:
				this._rangeDataFilter = mArguments.filter;
				break;

			case TimelineFilterType.Search:
				this._searchFilter = mArguments.filter;
				break;
			default:
		}

		if (mArguments.refresh !== false) {
			this.recreateFilter();
		}
	};

	/**
	 * An override which exposes hidden aggregations. Hidden aggregations are used by Run Time Adaptation.
	 *
	 * @param {string} sAggregationName the name of the aggregation
	 * @returns {sap.ui.base.ManagedObject|sap.ui.base.ManagedObject[]|null} the aggregation array in case of 0..n-aggregations or the managed object or null in case of 0..1-aggregations
	 * @protected
	 */
	Timeline.prototype.getAggregation = function (sAggregationName) {
		switch (sAggregationName) {
			case "headerBar":
				return this.getHeaderBar();
			case "searchField":
				return this._objects.getSearchField();
			case "sortIcon":
				return this._objects.getSortIcon();
			case "filterIcon":
				return this._objects.getFilterIcon();
			default:
				return Control.prototype.getAggregation.apply(this, arguments);
		}
	};


	/* =========================================================== */
	/* Private methods */
	/* =========================================================== */
	/**
	 * Returns key and title for oDate object based on format type (Year, Month, ...)
	 * @param {Date} oDate Item's date by group is determined
	 * @param {sap.suite.ui.commons.TimelineGroupType} sType Type of grouping
	 * @returns {object} Group data
	 * @private
	 */
	Timeline.prototype._formatGroupBy = function (oDate, sType) {
		if (this._fnCustomGroupBy) {
			return this._fnCustomGroupBy(oDate);
		}

		var sKey = oDate,
			sTitle = oDate;
		if (oDate instanceof Date) {
			switch (sType) {
				case TimelineGroupType.Year:
					sKey = oDate.getFullYear();
					sTitle = DateFormats.Year.format(oDate);
					break;

				case TimelineGroupType.Quarter:
					sKey = oDate.getFullYear() + "/" + Math.floor(oDate.getMonth() / 4);
					sTitle = DateFormats.Quarter.format(oDate);
					break;

				case TimelineGroupType.Month:
					sKey = oDate.getFullYear() + "/" + oDate.getMonth();
					sTitle = DateFormats.Month.format(oDate);
					break;

				case TimelineGroupType.Week:
					var dateFrom = new Date(oDate),
						dateTo = new Date(oDate),
						year = oDate.getFullYear(),
						week = DateFormats.Week.format(oDate),
						// find first and last day of the week
						first = oDate.getDate() - oDate.getDay(),
						last = first + 6,
						firstDay = new Date(dateFrom.setDate(first)),
						lastDay = new Date(dateTo.setDate(last));

					sKey = year + "/" + week;

					sTitle = DateFormats.MonthDay.format(firstDay) + " \u2013 " + DateFormats.MonthDay.format(lastDay);
					break;

				case TimelineGroupType.Day:
					sKey = oDate.getFullYear() + "/" + oDate.getMonth() + "/" + oDate.getDate();
					sTitle = DateFormats.Day.format(oDate);
					break;

				default:
			}
		}

		return {
			key: sKey,
			title: sTitle,
			date: oDate
		};
	};

	/**
	 * Calculate difference between two dates based by type (Year, month, ...)
	 * @param {sap.suite.ui.commons.TimelineGroupType} type Type of return result(Year, month,)
	 * @param {Date} [dateA] Date A - optional
	 * @param {Date} [dateB] Date B - optional
	 * @returns {number} Difference between dates
	 * @private
	 */
	Timeline.prototype._fnDateDiff = function (type, dateA, dateB) {
		var iMonths,
			iYearDiff, iDateAQuarter, iDateBQuarter,
			fResult = 0;

		dateA = dateA || this._minDate;
		dateB = dateB || this._maxDate;

		switch (type) {
			case TimelineGroupType.Year:
				fResult = dateB.getFullYear() - dateA.getFullYear();
				break;
			case TimelineGroupType.Month:
				iMonths = (dateB.getFullYear() - dateA.getFullYear()) * 12;
				iMonths += dateB.getMonth() - dateA.getMonth();
				fResult = iMonths <= 0 ? 0 : iMonths;
				break;
			case TimelineGroupType.Quarter:
				iYearDiff = (dateB.getFullYear() - dateA.getFullYear()) * 4;
				iDateAQuarter = Math.floor(dateA.getMonth() / 3);
				iDateBQuarter = Math.floor(dateB.getMonth() / 3);

				fResult = iYearDiff + (iDateBQuarter - iDateAQuarter);
				break;
			case TimelineGroupType.Day:
				// hours * minutes * seconds * milliseconds
				var oneDay = 24 * 60 * 60 * 1000;
				fResult = Math.round(Math.abs((dateA.getTime() - dateB.getTime()) / (oneDay)));
				break;
			default:
		}

		return fResult;
	};

	/**
	 * Add value to '_minDate'  based type (f.e. if type is YEAR and value is 4, we add 4 year)
	 * sDateRoundType means whether we want START of the 'interval' or its end
	 * lets say we add two years from 2014 -> we may have 2016/1/1 or 2016/12/31 depending whether we want to display 'from' or 'to'
	 * @param {number} iValue Number of units to add to minDate (type defined by second parameter)
	 * @param {sap.suite.ui.commons.TimelineGroupType} sDateRoundType Type of date to add
	 * @returns {Date} New date created by minDate + iValue
	 * @private
	 */
	Timeline.prototype._fnAddDate = function (iValue, sDateRoundType) {
		var oNewDate, oRoundedDown, oRoundedUp,
			fnSetTime = function (iHour, iMinute, iSecond) {
				this.setHours(iHour);
				this.setMinutes(iMinute);
				this.setSeconds(iSecond);
			},
			fnReturnCorrectDate = function (oDirect, oRoundedDown, oRoundedUp) {
				if (sDateRoundType === DateRoundType.UP) {
					fnSetTime.call(oRoundedUp, 23, 59, 59);
					return new Date(Math.min.apply(null, [this._maxDate, oRoundedUp]));
				}
				if (sDateRoundType === DateRoundType.DOWN) {
					fnSetTime.call(oRoundedDown, 0, 0, 0);
					return new Date(Math.max.apply(null, [this._minDate, oRoundedDown]));
				}
				return oDirect;
			};

		switch (this._rangeFilterType) {
			case TimelineGroupType.Year:
				oNewDate = new Date(new Date(this._minDate).setFullYear(this._minDate.getFullYear() + iValue));
				oRoundedDown = new Date(oNewDate.getFullYear(), 0, 1);
				oRoundedUp = new Date(oNewDate.getFullYear(), 11, 31);
				break;

			case TimelineGroupType.Month:
				oNewDate = new Date(new Date(this._minDate).setMonth(this._minDate.getMonth() + iValue));
				oRoundedDown = new Date(oNewDate.getFullYear(), oNewDate.getMonth(), 1);
				oRoundedUp = new Date(oNewDate.getFullYear(), oNewDate.getMonth() + 1, 0);
				break;

			case TimelineGroupType.Quarter:
				oNewDate = new Date(new Date(this._minDate).setMonth(this._minDate.getMonth() + (iValue * 3)));
				var iQuarterStart = oNewDate.getMonth() % 3;
				oRoundedDown = new Date(oNewDate.getFullYear(), oNewDate.getMonth() - iQuarterStart, 1);
				oRoundedUp = new Date(oNewDate.getFullYear(), oNewDate.getMonth() + (2 - iQuarterStart) + 1, 0);
				break;

			case TimelineGroupType.Day:
				// we do this for hour rounding
				oNewDate = oRoundedDown = oRoundedUp = new Date(new Date(this._minDate).setDate(this._minDate.getDate() + iValue));
				break;

			default:
		}

		return fnReturnCorrectDate.call(this, oNewDate, oRoundedDown, oRoundedUp);
	};

	/**
	 * Set default value for time range selector based by limit values
	 * @returns {sap.suite.ui.commons.TimelineGroupType} Selected value
	 * @private
	 */
	Timeline.prototype._calculateRangeTypeFilter = function () {
		var daysDiff = this._fnDateDiff(TimelineGroupType.Day);
		if (daysDiff > 500) {
			return TimelineGroupType.Year;
		} else if (daysDiff > 200) {
			return TimelineGroupType.Quarter;
		} else if (daysDiff > 62) {
			return TimelineGroupType.Month;
		}

		return TimelineGroupType.Day;
	};

	/**
	 * Setup range filter (min and max)
	 * @private
	 */
	Timeline.prototype._setRangeFilter = function () {
		var diff = this._fnDateDiff(this._rangeFilterType);
		this._objects.getTimeRangeSlider().setMin(0);
		this._objects.getTimeRangeSlider().setMax(diff);
		this._objects.getTimeRangeSlider().setRange([0, diff]);
		this._objects.getTimeRangeSlider().invalidate();
		var aRangeSliderRange = this._objects.getTimeRangeSlider().getRange();
		if ((aRangeSliderRange[1] - aRangeSliderRange[0]) === 0) {
			this._objects.getTimeRangeSlider().addStyleClass("sapTimelineRangeFilterNoDiff");
		} else {
			this._objects.getTimeRangeSlider().removeStyleClass("sapTimelineRangeFilterNoDiff");
		}
	};

	/**
	 * Action after sort arrow is clicked
	 * @private
	 */
	Timeline.prototype._sortClick = function () {
		var oBinding, sPath;
		this._sortOrder = this._sortOrder === SortOrder.ASCENDING ? SortOrder.DESCENDING : SortOrder.ASCENDING;
		this._objects.getSortIcon().setIcon(this._sortOrder === SortOrder.ASCENDING ?
			"sap-icon://arrow-bottom" : "sap-icon://arrow-top");

		this._objects.getSortIcon().setTooltip(this._sortOrder === SortOrder.ASCENDING
			? resourceBundle.getText("TIMELINE_SORT_ASCENDING")
			: resourceBundle.getText("TIMELINE_SORT_DESCENDING"));

		if (this._useModelFilter()) {
			oBinding = this.getBinding("content");
			sPath = this._findDateTimeBindingPath();

			oBinding.sort(this._getDefaultSorter(sPath, this._sortOrder === SortOrder.ASCENDING));
		} else {
			this.invalidate();
		}
	};

	/**
	 * Sort items
	 * @param {Array} aData Data
	 * @param {string} sortOrder The order of sorting. Can be ASCENDING or DESCENDING.
	 * @returns {Array} Sorted data
	 * @private
	 */
	Timeline.prototype._sort = function (aData, sortOrder) {
		var sSortOrder = sortOrder || this._sortOrder;
		aData.sort(function (itemA, itemB) {
			var dateA = itemA.getDateTime(),
				dateB = itemB.getDateTime(),
				sign = (sSortOrder === SortOrder.ASCENDING) ? -1 : 1;

			return dateA < dateB ? 1 * sign : -1 * sign;
		});

		return aData;
	};

	/**
	 * When user clicks 'more' button or scrolls down (when lazy loading is ON)
	 * @private
	 */
	Timeline.prototype._loadMore = function () {
		var oBindingInfo, aData,
			fnSetNewItemCount = function () {
				var increase = this._displayShowMore() ? this.getGrowingThreshold() : this._calculateItemCountToLoad(this.$());
				this._iItemCount += increase;
				// limit maximum items to binding limit
				this._iItemCount = Math.min(this._getMaxItemsCount(), this._iItemCount);
			}.bind(this);

		this._lastScrollPosition.more = this._isVertical() ? this._$content.get(0).scrollTop : this._$content.get(0).scrollLeft;
		this._setBusy(true);
		this.fireGrow();

		if (this._useBinding()) {
			if (this._isMaxed()) {
				this._setBusy(false);
				return;
			}

			fnSetNewItemCount();

			oBindingInfo = this.getBindingInfo("content");
			oBindingInfo.startIndex = 0;

			// we already loaded all data so we don't want to overwrite it now
			if (!this._loadAllData()) {
				oBindingInfo.length = this._iItemCount;
			}

			// check whether we use OData binding
			aData = this.getBinding("content").getContexts(0, oBindingInfo.length);

			// don't update for oData service
			// TL will be updated when data are loaded
			if (aData && aData.dataRequested) {
				return;
			}

			this.updateAggregation("content");
		} else {
			fnSetNewItemCount();
			this.invalidate();
		}
		this.oItemNavigation.refocusOnNextUpdate();
	};

	/**
	 * Recreate all default filters (data, time, search) and refresh content.
	 * @param {boolean} bResetAll Indicates whether to clear all current filters (including custom filters)
	 */
	Timeline.prototype.recreateFilter = function (bResetAll) {
		var oBinding = this.getBinding("content"),
			that = this, //eslint-disable-line
			aFilters = [],
			aCurrentFilters = [];
		if (oBinding) {
			if (!bResetAll) {
				aFilters = oBinding.aFilters || [];
			}

			if (this._dataFilter) {
				aCurrentFilters.push(this._dataFilter);
			}

			if (this._rangeDataFilter) {
				aCurrentFilters.push(this._rangeDataFilter);
			}

			if (this._searchFilter) {
				aCurrentFilters.push(this._searchFilter);
			}
			// we don't want to override custom filters in binding, so find our filter and replace it
			if (this._filter && !bResetAll) {
				var filterIndex = findIndex(aFilters, function (oFilter) {
					return oFilter === that._filter;
				});

				if (filterIndex !== -1) {
					aFilters.splice(filterIndex, 1);
				}
			}

			// if there is any new filter add it as new one, otherwise this method only delete old filter
			if (aCurrentFilters.length > 0) {
				this._filter = new Filter(aCurrentFilters, true);
				aFilters.push(this._filter);
			}

			oBinding.filter(aFilters, FilterType.Control);
		} else {
			// for usecase without binding we just invalidate, because all filtering is done right before render is called
			this.invalidate();
		}
	};

	/**
	 * Returns either custom range message (set by function call) or create new one based on 'from' and 'to' selection from time filter.
	 * @returns {string} Message for range info bar.
	 * @private
	 */
	Timeline.prototype._getRangeMessage = function () {
		var text = this._rangeMessage;
		if (!text) {
			var titleStart = this._formatGroupBy(this._startDate, this._rangeFilterType).title,
				titleEnd = this._formatGroupBy(this._endDate, this._rangeFilterType).title;

			text = resourceBundle.getText("TIMELINE_RANGE_SELECTION") + " (";
			text += titleStart + " - " + titleEnd + ")";
		}
		return text;
	};

	/**
	 * Returns either custom data filter message (set by function call) or create new one based on selected filter items.
	 * @returns {string} Message for filter info bar.
	 * @private
	 */
	Timeline.prototype._getFilterMessage = function () {
		var sText = "",
			sMessage = null;

		if (this._dataMessage) {
			sText = this._dataMessage;
		} else if (this._currentFilterKeys.length > 0) {
			sText = this._currentFilterKeys.map(function (oItem) {
				return oItem.text ? oItem.text : oItem.key;
			}).join(", ");

			sText = this._getFilterTitle() + " (" + sText + ")";
		}

		if (this._rangeDataFilter || this._rangeMessage || (this._startDate && this._endDate)) {
			sText = sText ? sText + ", " : "";
			sText += this._getRangeMessage();
		}

		if (this._customFilterMessage) {
			sText = sText ? sText + ", " + this._customFilterMessage : this._customFilterMessage;
		}

		if (sText) {
			sMessage = resourceBundle.getText("TIMELINE_FILTER_INFO_BY", sText);
		}

		return sMessage;
	};

	/**
	 * Proper oData handling. We call 'updateAggregation' not right away but we wait after data are loaded
	 * @private
	 */
	Timeline.prototype.refreshContent = function () {
		var oBinding = this.getBinding("content"),
			oBindingInfo = this.getBindingInfo("content"),
			iGrowingThreshold = this.getGrowingThreshold();
		this._setBusy(true);

		// this is to prevent refreshing before data are loaded
		// 'updateAggregation' triggers UI update even if data are not loaded yet
		if (oBinding && oBindingInfo) {
			this._iItemCount = oBinding.sOperationMode === "Default" ? 0 : this._iItemCount;
			oBinding.getContexts(0, iGrowingThreshold);
			oBindingInfo.length = iGrowingThreshold;
			oBinding.attachEventOnce("dataReceived", jQuery.proxy(function () {
				this.updateAggregation("content");
			}, this));
		} else {
			this.updateAggregation("content");
		}
	};

	/**
	 * Proper oData handling.
	 * @private
	 */
	Timeline.prototype.updateContent = function () {
		this._setBusy(false);
		this.updateAggregation("content");
		var oSortIcon = this._objects.getSortIcon();
		if (this.getAggregation("content").length <= 1) {
			oSortIcon.setEnabled(false);
		} else {
			oSortIcon.setEnabled(true);
		}
		// for empty returned dataset in oData updateAggregation doesn't trigger rerender so we call it manually
		this.invalidate();
	};

	/**
	 * Destroy additional objects which are not directly in content aggregation but need to removed when content is destroyed.
	 * @private
	 */
	Timeline.prototype.destroyContent = function () {
		// if there is request pending, don't invalidate right now
		var $line = this.$("line"),
			$showMore = this.$().find(".sapSuiteUiCommonsTimelineItemGetMoreButton");

		if ($line.get(0)) {
			$line.remove();
		}

		if ($showMore.get(0)) {
			$showMore.remove();
		}

		this.destroyAggregation("content");

		return this;
	};

	/**
	 * Process search
	 * @param {string} sSearchTerm term to search
	 * @private
	 */
	Timeline.prototype._search = function (sSearchTerm) {
		var that = this, //eslint-disable-line
			bExecuteDefault, aTextPaths, aTitlePaths, aUserNamePaths,
			columns = [];

		this._searchValue = sSearchTerm;

		if (this._useModelFilter()) {
			bExecuteDefault = this._fireSelectionChange({
				searchTerm: this._searchValue,
				type: TimelineFilterType.Search
			});

			if (bExecuteDefault) {
				this._searchFilter = null;
				if (this._searchValue) {
					// add filter columns - by default we search in text, title and username binding fields
					aTextPaths = this._findBindingPaths("text");
					aTitlePaths = this._findBindingPaths("title");
					aUserNamePaths = this._findBindingPaths("userName");

					if (aTextPaths.length > 0) {
						columns.push(aTextPaths);
					}
					if (aTitlePaths.length > 0) {
						columns.push(aTitlePaths);
					}
					if (aUserNamePaths.length > 0) {
						columns.push(aUserNamePaths);
					}
					if (columns.length > 0) {
						//create filters based on the filter columns
						this._searchFilter = new Filter(columns.map(function (paths) {
							return new Filter(paths.map(function (onePath) {
								return new Filter(onePath, FilterOperator.Contains, that._searchValue);
							}), false);
						}));
					}
				}

				this.recreateFilter();
			}
		} else {
			this.invalidate();
		}
	};

	/**
	 * @param {boolean} bRange Whether or not to use a range
	 * @private
	 */
	Timeline.prototype._filterData = function (bRange) {
		var bExecuteDefault,
			sPath;
		this._dataMessage = "";

		if (this._useModelFilter()) {
			// filter

			this._dataFilter = null;
			bExecuteDefault = this._fireSelectionChange({
				// backward compatibility
				selectedItem: this._currentFilterKeys[0] ? this._currentFilterKeys[0].key : "",
				selectedItems: this._currentFilterKeys,
				type: TimelineFilterType.Data
			});

			if (bExecuteDefault) {
				if (this._currentFilterKeys.length > 0) {
					sPath = this._findBindingPath("filterValue");
					if (sPath) {
						this._dataFilter = new Filter(this._currentFilterKeys.map(function (oItem) {
							return new Filter(sPath, FilterOperator.EQ, oItem.key);
						}), false);
					}
				}
			}

			this._rangeDataFilter = null;
			if (bRange) {
				// range
				bExecuteDefault = this._fireSelectionChange({
					type: TimelineFilterType.Time,
					timeKeys: {
						from: this._startDate,
						to: this._endDate
					}
				});

				if (bExecuteDefault) {
					sPath = this._findDateTimeBindingPath();
					if (sPath) {
						this._rangeDataFilter = new Filter({
							path: sPath,
							operator: FilterOperator.BT,
							value1: this._startDate,
							value2: this._endDate
						});
					}

				}
			}

			this._setBusy(true);

			this.recreateFilter();
		} else {
			this.invalidate();
		}
	};

	/**
	 * @private
	 */
	Timeline.prototype._filterRangeData = function () {
		var bExecuteDefault, sPath;
		this._rangeMessage = "";

		if (this._useModelFilter()) {
			bExecuteDefault = this._fireSelectionChange({
				from: this._startDate,
				to: this._endDate,
				type: TimelineFilterType.Time
			});

			if (bExecuteDefault) {
				sPath = this._findDateTimeBindingPath();
				this._rangeDataFilter = null;
				if (sPath) {
					this._rangeDataFilter = new Filter({
						path: sPath,
						operator: FilterOperator.BT,
						value1: this._startDate,
						value2: this._endDate
					});
				}

				this._setBusy(true);
				this.recreateFilter();
			}
		} else {
			this.invalidate();
		}
	};

	/**
	 * Override apply settings to postpone bind aggregation.
	 * Bind aggregation may be called before all settings are applied
	 *
	 * @param {object} mSettings The settings to apply to this managed object
	 * @param {object} [oScope] Scope object to resolve types and formatters
	 * @private
	 */
	Timeline.prototype.applySettings = function (mSettings, oScope) {
		ManagedObject.prototype.applySettings.apply(this, [mSettings, oScope]);

		this._settingsApplied = true;

		// if bind aggregation was already called (which is most likely if we use timeline content binding from XML),
		// call it now
		if (this._bindOptions) {
			this.bindAggregation("content", this._bindOptions);
			this._bindOptions = null;
		}
	};

	/**
	 * Create filters for filter dialog. If modelfilter is ON, user can set own filter items, otherwise items are
	 * grabbed from items as distinct value for {filterValue} column in data set.
	 * @private
	 */
	Timeline.prototype._setFilterList = function () {
		var bSort = false,
			aData,
			oItems,
			sKey,
			oFilteredItems = {},
			oBinding,
			oBindingData;

		this._aFilterList = [];

		if (this._useModelFilter()) {
			this._aFilterList = this.getFilterList().map(function (oItem) {
				return {
					key: oItem.getProperty("key"),
					text: oItem.getProperty("text")
				};
			});

			if (this._aFilterList.length === 0) {
				// JSON model only, for oData model values have to be set via filter list
				oBindingData = this._findBindingData("filterValue");
				oBinding = this.getBinding("content");
				if (oBindingData && oBinding) {
					aData = oBinding.getDistinctValues(oBindingData.path);
					if (Array.isArray(aData)) {
						this._aFilterList = aData.map(function (sItem) {
							return {
								key: sItem,
								text: oBindingData.formatter ? oBindingData.formatter(sItem) : sItem
							};
						});
						this._aFilterList = this._aFilterList.filter(function (oItem) {
							return oItem.key;
						});
					}
					bSort = true;
				}
			}
		} else {
			oItems = this.getContent();
			bSort = true;
			// grab all unique values from all items
			for (var i = 0; i < oItems.length; i++) {
				sKey = oItems[i].getFilterValue();
				if (!sKey) {
					continue;
				}
				if (!(sKey in oFilteredItems)) {
					oFilteredItems[sKey] = 1;
					this._aFilterList.push({
						key: sKey,
						text: sKey
					});
				}
			}
		}

		if (bSort) {
			this._aFilterList.sort(function (a, b) {
				if (a.text.toLowerCase) {
					return a.text.toLowerCase().localeCompare(b.text.toLowerCase());
				} else {
					return a.text > b.text;
				}
			});
		}
	};

	/**
	 * Clear data filter. Remove filter from model binding and clear all selected items for filter selection.
	 * @private
	 */
	Timeline.prototype._clearFilter = function () {
		var fnClearTimeRangeFilter = function () {
				var bExecuteDefault,
					oSlider = this._objects.getTimeRangeSlider();
				this._startDate = null;
				this._endDate = null;
				this._rangeMessage = null;

				oSlider.setRange([oSlider.getMin(), oSlider.getMax()]);

				if (this._useModelFilter()) {
					bExecuteDefault = this._fireSelectionChange({
						clear: true,
						timeKeys: {
							from: null,
							to: null
						},
						type: TimelineFilterType.Range
					});
				}
				return bExecuteDefault;
			}.bind(this),
			fnClearDataFilter = function () {
				var bExecuteDefault;

				this._currentFilterKeys = [];
				if (this._useModelFilter()) {
					bExecuteDefault = this._fireSelectionChange({
						clear: true,
						selectedItems: [],
						selectedItem: "",
						type: TimelineFilterType.Data
					});
				}
				return bExecuteDefault;
			}.bind(this);

		var bDataDefault = fnClearDataFilter(),
			bRangeDefault = fnClearTimeRangeFilter();

		// clear custom filter
		this._customFilterMessage = "";
		if (bDataDefault || bRangeDefault) {
			if (bDataDefault) {
				this._dataFilter = null;
			}

			if (bRangeDefault) {
				this._rangeDataFilter = null;
			}
			this.recreateFilter(true /*force clear custom filters*/);
		} else {
			this.invalidate();
		}

		//reset filter
		// this is a bit overkill but we are so far unable to reset filter to it's init state without some filter's internal errors
		// or nasty private variables manipulation.
		this._objects.destroyObject("FilterContent");
		this._setupFilterDialog();
	};

	/**
	 * Open time filter dialog. IF max and min not yet set, try to find out.
	 * @private
	 * @returns {Promise} A promise for when all requests have been completed
	 */
	Timeline.prototype._getTimeFilterData = function () {
		var that = this, //eslint-disable-line
			aItems, oMin, oMax, oDate,
			fnCall = function (sType, sName) {
				return fnGetLimit(sType, that[sName]).then(function (oData) { //eslint-disable-line
						if (oData) {
							if (!that._objects.getTimeFilterSelect().getEnabled()) {
								that._objects.getTimeFilterSelect().setEnabled(true);
							}
							// ensure we have valid values
							var parsedDate = DateUtils.parseDate(oData);

							if (parsedDate instanceof Date) {
								that[sName] = parsedDate;
							}
						}
					}
				).catch(function () {
					that._objects.getTimeFilterSelect().setEnabled(false);
				});
			},
			fnGetLimitValue = function (asc) {
				var oModel, sDateTimePath, oSorter, oBinding, oProperties,
					sGroupId = "sap_suite_ui_commons_Timeline";

				// Model should be taken from content aggregation otherwise incorrect model can be fetched while using named models.
				oModel = this.getBinding("content").getModel();
				if (!oModel) {
					return Promise.reject();
				}

				sDateTimePath = this._findDateTimeBindingPath();
				if (!sDateTimePath) {
					return;
				}

				oSorter = new Sorter(sDateTimePath, asc);
				oBinding = this.getBinding("content");

				if (!oBinding) {
					return Promise.reject();
				}

				if (oModel.submitBatch) {
					oProperties = {
						$$groupId: sGroupId
					};
				}

				var oListBinding = oModel.bindList(oBinding.getPath(), oBinding.getContext(), oSorter, undefined, oProperties);
				oListBinding.initialize();

				var aContexts = oListBinding.getContexts(0, 1);
				if (BaseObject.isA(oListBinding, "sap.ui.model.json.JSONListBinding") && aContexts.length === 0) {
					return Promise.reject();
				} else if (aContexts.length > 0) {
					return Promise.resolve(DateUtils.parseDate(aContexts[0].getProperty(sDateTimePath)));
				} else if (BaseObject.isA(oListBinding, "sap.ui.model.odata.v4.ODataListBinding")) {
					that._setBusy(true);
					return new Promise(function (resolve, reject) {
						oModel.submitBatch(sGroupId).then(function () {
							that._setBusy(false);
							fnGetLimitFromBinding(oListBinding, sDateTimePath, resolve, reject);
						});
					});
				} else if (oListBinding && oListBinding.attachDataReceived) {
					that._setBusy(true);
					return new Promise(function (resolve, reject) {
						oListBinding.attachDataReceived(function () {
							that._setBusy(false);
							fnGetLimitFromBinding(oListBinding, sDateTimePath, resolve, reject);
						});
					});
				}

				return Promise.reject();
			}.bind(this),
			fnGetLimit = function (sType, oDate) {
				if (oDate) {
					return Promise.resolve(oDate);
				}

				return fnGetLimitValue(sType === "max");
			},
			fnGetMinAndMax = function () {
				aItems = this.getContent();
				if (aItems.length > 0) {
					this._minDate = aItems[0].getDateTime();
					this._maxDate = aItems[0].getDateTime();

					for (var i = 1; i < aItems.length; i++) {
						oDate = aItems[i].getDateTime();
						if (oDate < this._minDate) {
							this._minDate = oDate;
						}
						if (oDate > this._maxDate) {
							this._maxDate = oDate;
						}
					}
				}
			},
			fnGetLimitFromBinding = function (oListBinding, sDateTimePath, resolve, reject) {
				var aContexts = oListBinding.getContexts(0, 1);
				if (aContexts.length > 0) {
					resolve(DateUtils.parseDate(aContexts[0].getProperty(sDateTimePath)));
				} else {
					reject();
				}
			};

		return new Promise(function (resolve, reject) {
			// if min or max date not loaded yet
			if (!that._maxDate || !that._minDate) {
				if (that._useModelFilter()) {
					// call separate OData requests for min and max
					oMin = fnCall("min", "_minDate", oMin);
					oMax = fnCall("max", "_maxDate", oMax);

					Promise.all([oMin, oMax]).then(function () {
						// wait till all requests are complete then open window
						resolve();
					}).catch(function () {
						reject();
					});
				} else {
					fnGetMinAndMax.call(that);
					resolve();
				}
			} else {
				resolve();
			}
		});
	};

	/**
	 * Opens filter dialog. Can be custom dialog.
	 * @private
	 */
	Timeline.prototype._openFilterDialog = function () {
		var oCustomFilter = this.getCustomFilter();
		if (oCustomFilter) {
			if (typeof oCustomFilter.openBy === "function") {
				oCustomFilter.openBy(this._objects.getFilterIcon());
			} else if (typeof oCustomFilter.open === "function") {
				oCustomFilter.open();
			} else {
				Log.error("CustomFilter is expected to have an openBy or open function. The provided instance doesn't have either.");
			}
			this.fireFilterOpen();
			return;
		}

		this._filterState = {
			data: false,
			range: false
		};

		this._objects.getFilterContent().open();
		this._objects.getTimestampFilterPicker().resizeDialog(this._objects);
		this.fireFilterOpen();
	};

	/**
	 * Creates group header item which is classic TimelineItem with slightly different data
	 * @param {object} oContext oContext group data with {date;key;title}
	 * @param {boolean} bSkipAppend If true, we don't call addAggregation (for cases when there is no binding)
	 * @returns {sap.suite.ui.commons.TimelineItem} newly recreated group item
	 * @private
	 */
	Timeline.prototype._createGroupHeader = function (oContext, bSkipAppend) {
		var sId = this.getId() + "-timelinegroupheader-" + this._groupId,
			sKey = oContext.key,
			oGroupItem = new TimelineItem(sId, {
				text: "GroupHeader",
				dateTime: oContext.date,
				userName: sKey,
				title: oContext.title,
				icon: "sap-icon://arrow-down"
			});

		oGroupItem._isGroupHeader = true;

		// for usage without binding we don't add groupitem to content as we recreate it every render cycle
		if (bSkipAppend) {
			oGroupItem.setParent(this, "content");
			this._aGroups.push(oGroupItem);
		} else {
			this.addAggregation("content", oGroupItem, false);
		}

		this._groupId++;
		return oGroupItem;
	};

	/**
	 * Create sorter object
	 * @param {string} sPropertyName Property name by which sorting is managed
	 * @param {boolean} sAscending Order of sorting
	 * @returns {sap.ui.model.Sorter} Sorter object
	 * @private
	 */
	Timeline.prototype._getDefaultSorter = function (sPropertyName, sAscending) {
		var that = this; //eslint-disable-line

		if (sPropertyName) {
			return new Sorter(sPropertyName, !sAscending, function (oContext) {
				var sValue = oContext.getProperty(sPropertyName),
					oDate = DateUtils.parseDate(sValue);

				return oDate instanceof Date ? that._formatGroupBy(oDate, that.getGroupByType()) : {date: oDate};
			});
		}
	};

	/**
	 * Return binding info for given property
	 * @param {string} sPropertyName Property name we are looking for the model source
	 * @param {object} oTemplate Binding template
	 * @returns {object} Binding info
	 * @private
	 */
	Timeline.prototype._findBindingInfoFromTemplate = function (sPropertyName, oTemplate) {
		// if no template is set try to find from binding info
		// this function may be called before 'content' aggregation is initialized
		// so there is a possibility to call it with template object directly
		if (!oTemplate) {
			var oBindingInfo = this.getBindingInfo("content");

			if (oBindingInfo) {
				oTemplate = oBindingInfo.template;
			}
		}

		if (oTemplate) {
			var oInfo = oTemplate.getBindingInfo(sPropertyName);
			if (oInfo && oInfo.parts && oInfo.parts[0]) {
				return oInfo;
			}
		}

		return null;
	};

	/**
	 * Return all binding paths for property name
	 * @param {string} sPropertyName Property name we are looking for the model source
	 * @param {object} oTemplate Binding template
	 * @returns {Array} All property's paths
	 * @private
	 */
	Timeline.prototype._findBindingPaths = function (sPropertyName, oTemplate) {
		var oInfo = this._findBindingInfoFromTemplate(sPropertyName, oTemplate);
		if (oInfo && oInfo.parts) {
			return oInfo.parts.map(function (oItem) {
				return oItem.path;
			});
		}

		return [];
	};

	Timeline.prototype._findDateTimeBindingPath = function (oBindingInfo) {
		var sDateTimePath = this.getDateTimePath();
		if (sDateTimePath) {
			return sDateTimePath;
		}

		var oInfo = this._findBindingInfoFromTemplate("dateTime", oBindingInfo);
		if (oInfo) {
			return oInfo.parts[0].path;
		}

		return null;
	};


	/**
	 * Return binding path for property name
	 * @param {string} sPropertyName Property name we are looking for the model source
	 * @param {object} oTemplate Binding template
	 * @returns {string} First property path (rest are ignored)
	 * @private
	 */
	Timeline.prototype._findBindingPath = function (sPropertyName, oTemplate) {
		var oInfo = this._findBindingInfoFromTemplate(sPropertyName, oTemplate);
		if (oInfo) {
			return oInfo.parts[0].path;
		}

		return null;
	};

	/**
	 * Return information for property (path and formatter)
	 * @param {string} sPropertyName Property name we are looking for the model source
	 * @param {object} oTemplate Binding template
	 * @returns {object} Formatter and path
	 * @private
	 */
	Timeline.prototype._findBindingData = function (sPropertyName, oTemplate) {
		var oInfo = this._findBindingInfoFromTemplate(sPropertyName, oTemplate);
		if (oInfo) {
			return {
				path: oInfo.parts[0].path,
				formatter: oInfo.formatter
			};
		}
		return null;
	};

	/**
	 * Based on settings apply grouping and sorting to binding info
	 * @param {object} oBindingInfo Binding information
	 * @private
	 */
	Timeline.prototype._bindGroupingAndSorting = function (oBindingInfo) {
		// get the sorter and add to bindaggregation options
		// only if there is date time binding
		if (!this._isGrouped() && this.getSort()) {
			var sDateTimeBinding = this._findDateTimeBindingPath(oBindingInfo.template);
			if (sDateTimeBinding) {
				oBindingInfo.sorter = this._getDefaultSorter(sDateTimeBinding, this.getSortOldestFirst());
			}
		}
		oBindingInfo.groupHeaderFactory = null;
		if (this._isGrouped()) {
			// add sort by for group by field
			oBindingInfo.sorter = this._getDefaultSorter(this.getGroupBy(), this.getSortOldestFirst());

			//get group header factory and add to bindaggration options
			oBindingInfo.groupHeaderFactory = jQuery.proxy(this._createGroupHeader, this);
		}
	};

	Timeline.prototype.updateBindingContext = function () {
		this.reset();
		return ManagedObject.prototype.updateBindingContext.apply(this, arguments);
	};

	/**
	 * Overrides ManagedObject bindAggregation function, we add some special features for binding
	 * @param {string} sName Aggregation name
	 * @param {object} oOptions Binding options
	 * @private
	 * @returns {sap.suite.ui.commons.Timeline} This to allow method chaining
	 */
	Timeline.prototype.bindAggregation = function (sName, oOptions) {
		if (sName === "content") {
			// this prevents calling bindaggregation before all settings are loaded
			// when we have context={/...} in XML timeline definition we can't guarantee order of settings applied
			// so this method could be called before all settings are parsed from XML and setup
			// we store options parameter and called it after settings are loaded (after applysettings method is called)
			if (!this._settingsApplied) {
				this._bindOptions = oOptions;
				return null;
			}

			this._bindGroupingAndSorting(oOptions);

			// if we have 'Show More' ON, limit the length to the growing treshold (stored in _itemCount)
			// otherwise download minimum limit for auto-scrolling
			if (this._lazyLoading()) {
				this._iItemCount = this._calculateItemCountToLoad(jQuery(window));
				if (!this._loadAllData(true)) {
					oOptions.length = this._iItemCount;
				}
			} else if (this._displayShowMore() && !this._loadAllData(oOptions.template || oOptions.factory)) {
				this._iItemCount = this.getGrowingThreshold();
				oOptions.length = this._iItemCount;
			}

			this._oOptions = oOptions;
		}

		return ManagedObject.prototype.bindAggregation.apply(this, [sName, oOptions]);
	};

	/**
	 * Calculate items to be loaded for lazy loading based on item size and screen size
	 * @param {object} $parent Parent object where we fit timeline
	 * @returns {number} Number of items to load
	 * @private
	 */
	Timeline.prototype._calculateItemCountToLoad = function ($parent) {
		var isVertical = TimelineAxisOrientation.Vertical === this.getAxisOrientation(),
			size = isVertical ? $parent.height() : $parent.width(),
			isDblSided = this.getEnableDoubleSided(),
			// double sided layout stores more items to one screen
			ratio = isDblSided ? 0.6 : 1,
			SIZE = isVertical ? 1200 : 2000,
			// aprox. item size with margin
			ITEM_SIZE = isVertical ? 120 : 280,
			MIN_COUNT = (13 * ratio),
			result;

		if (!size) {
			// for some reason we are not able to find out parent size -> calculate with 1200 then
			size = SIZE;
		}

		// count aprox. number of items available for one screen (multiple by 2 cause we want to be sure, scrollbar is shown)
		result = (size / (ITEM_SIZE * ratio)) * 1.5;
		return Math.floor(Math.max(result, MIN_COUNT));
	};

	/**
	 * Before render event
	 * @private
	 */
	Timeline.prototype.onBeforeRendering = function () {
		var iGrowingThreshold = this.getGrowingThreshold(),
			aContent;
		this._bRtlMode = sap.ui.getCore().getConfiguration().getRTL();

		this._objects.getSortIcon().setIcon(this._sortOrder === SortOrder.ASCENDING ?
			"sap-icon://arrow-bottom" : "sap-icon://arrow-top");

		this._objects.getSortIcon().setTooltip(this._sortOrder === SortOrder.ASCENDING
			? resourceBundle.getText("TIMELINE_SORT_ASCENDING")
			: resourceBundle.getText("TIMELINE_SORT_DESCENDING"));

		this._aGroups = [];
		this._bRendered = false;

		aContent = this.getContent();

		// limit size for non binding lazy loading items
		if (!this._iItemCount && !this._useBinding() && this._lazyLoading()) {
			this._iItemCount = this._calculateItemCountToLoad(jQuery(window));
		}

		// limit size for growing threshold
		if (!this._iItemCount) {
			if (iGrowingThreshold !== 0) {
				this._iItemCount = iGrowingThreshold;
			}
		}

		// in case growing is off we want to set itemcount to all downloaded items in case
		// data was changed meanwhile
		if (!this._iItemCount || !this._useGrowing()) {
			// if still not set all data are probably loaded -> itemCount === all content data
			this._iItemCount = aContent.filter(function (oItem) {
				return !oItem._isGroupHeader;
			}).length;
		}

		this._setOutput(aContent);
	};

	/**
	 * Function override for grouping
	 * @param {object} context Data for grouping
	 * @private
	 */
	Timeline.prototype.addContentGroup = function (context) {
		//managed object requires this function to be declared to set bGrouped flag true
	};

	/**
	 * Action after user clicks scrolling button. Scrolls content to specific direction by set amount.
	 * @param {number} size Number to determine how many content to scroll
	 * @private
	 */
	Timeline.prototype._performScroll = function (size) {
		var that = this, //eslint-disable-line
			newScrollValue = this._isVertical() ? this._$content.get(0).scrollTop + size : this._$content.get(0).scrollLeft + size;

		newScrollValue = Math.max(newScrollValue, 0);
		if (this._isVertical()) {
			this._$content.get(0).scrollTop = newScrollValue;
		} else {
			this._$content.get(0).scrollLeft = newScrollValue;
		}

		if (this._manualScrolling) {
			setTimeout(that._performScroll.bind(that, size), 50);
		}
	};

	/**
	 * For default scrolling we use .more to store last scrolling position when load is pressed
	 * when some Timeline Item is selected it is focused by 'applyFocusInfo' which scrolls scrollbar to the focused item
	 * but we want to scroll to last visible (scrolled) position
	 * we use backup because we still want more to be erased after scrolling for cases when item is not focused so this method is called
	 * @param {boolean} bUseBackup Indicates whether store value for next use. Value is deleted after it.
	 * @private
	 */
	Timeline.prototype._moveScrollBar = function (bUseBackup) {
		if (this._lastScrollPosition.more || this._lastScrollPosition.backup) {
			if (bUseBackup) {
				this._lastScrollPosition.more = this._lastScrollPosition.backup;
			}

			// store backup
			if (!bUseBackup) {
				this._lastScrollPosition.backup = this._lastScrollPosition.more;
			}

			// reset flag, wait for next data load
			this._lastScrollPosition.more = 0;
		}
	};

	/**
	 * After render event
	 * @private
	 */
	Timeline.prototype.onAfterRendering = function () {
		var $this = this.$();

		if (this._isVertical()) {
			this._$content = this.$("content");
			this._$scroll = this.$("scroll");
		} else {
			this._$content = this.$("contentH");
			this._$scroll = this.$("scrollH");
		}

		this.setBusy(false);

		if (!this._oScroller) {
			this._oScroller = new ScrollEnablement(this, this._$scroll.attr('id'), {});
		}

		this._oScroller._$Container = this._$scroll.parent();

		this._oScroller.setVertical(this._isVertical());
		this._oScroller.setHorizontal(!this._isVertical());

		this._startItemNavigation();

		// setup
		this._scrollersSet = false;
		this._scrollMoreEvent = true;
		// after rendering we always want to trigger double sided changes (if the setting is ON)
		this._lastStateDblSided = null;

		this._showCustomMessage();
		this._setupScrollEvent();
		this._performUiChanges();

		this._moveScrollBar();
		this._bRendered = true;

		$this.css("opacity", 1);
	};

	/**
	 * Client data filtering for use cases without binding or for useModelFilter=false
	 * @param {Array} aItems Data to filter
	 * @returns {Array} filtered data
	 * @private
	 */
	Timeline.prototype._clientFilter = function (aItems) {
		var aFilteredItems = [],
			oItem, bFilter, oReasons, iFindIndex, oDate,
			sSearchValue, sText, sTitle, sUserName,
			bReverseDefault;

		function fnCheckKey(oSelectedFilterItem) {
			return oSelectedFilterItem.key === oItem.getProperty("filterValue");
		}

		// filtering - for cases without binding (or mixed by calling addContent) - we filter and sort items
		// even thou in most cases they are already filtered and sorted out.
		for (var i = 0; i < aItems.length; i++) {
			oItem = aItems[i];
			bFilter = false;
			oReasons = {};

			// data filter
			if (this._currentFilterKeys.length > 0) {
				iFindIndex = findIndex(this._currentFilterKeys, fnCheckKey);

				if (iFindIndex === -1) {
					bFilter = true;
					oReasons[TimelineFilterType.Data] = 1;
				}
			}

			// range filter
			if (this._startDate && this._endDate) {
				oDate = oItem.getDateTime();
				if (oDate < this._startDate || oDate > this._endDate) {
					bFilter = true;
					oReasons[TimelineFilterType.Time] = 1;
				}
			}

			// search filter
			if (this._searchValue) {
				sSearchValue = this._searchValue.toLowerCase();
				sText = oItem.getProperty("text") || "";
				sTitle = oItem.getProperty("title") || "";
				sUserName = oItem.getProperty("userName") || "";

				if (!((sText.toLowerCase().indexOf(sSearchValue) !== -1) ||
					(sTitle.toLowerCase().indexOf(sSearchValue) !== -1) ||
					(sUserName.toLowerCase().indexOf(sSearchValue) !== -1))) {
					bFilter = true;
					oReasons[TimelineFilterType.Search] = 1;
				}
			}

			bReverseDefault = !this.fireEvent("itemFiltering", {
				item: oItem,
				reasons: oReasons,
				dataKeys: this._currentFilterKeys,
				timeKeys: {
					from: this._startDate,
					to: this._endDate
				},
				searchTerm: this._searchValue
			}, true);

			if (bReverseDefault) {
				bFilter = !bFilter;
			}

			if (!bFilter) {
				aFilteredItems.push(oItem);
			}
		}

		return aFilteredItems;
	};

	/**
	 * Fills _outputItem collection which is used as items collection in renderer.
	 * @param {Array} aItems Data to process
	 * @private
	 */
	Timeline.prototype._setOutput = function (aItems) {
		var aFilteredItems;
		var fnTrimDataAfterItemCount = function () {
				// for cases when growingthreshold may be changed runtime, we trim additional data
				var iNonGroupItemCount = 0,
					aTrimmedItems = [],
					i = 0;
				if (this._iItemCount !== aFilteredItems.length) {
					for (; i < aFilteredItems.length; i++) {
						if (!aFilteredItems[i]._isGroupHeader) {
							iNonGroupItemCount++;
						}

						if (iNonGroupItemCount > this._iItemCount) {
							break;
						}

						aTrimmedItems.push(aFilteredItems[i]);
					}

					aFilteredItems = aTrimmedItems;
				}
			},
			fnAppendGroupHeaderItemsWithoutBinding = function () {
				var aItemList = [],
					oItem, oCurrentGroupHeaderData,
					oGroupHeaderData = {
						key: ""
					};

				for (var i = 0; i < aFilteredItems.length; i++) {
					oItem = aFilteredItems[i];
					oCurrentGroupHeaderData = this._formatGroupBy(oItem.getDateTime(), this.getGroupByType());

					if (oCurrentGroupHeaderData.key != oGroupHeaderData.key) {
						// we recreate group header items for every render run
						// we don't want to add it to content as it is not recreated every render cycle when binding is not in
						aItemList.push(this._createGroupHeader(oCurrentGroupHeaderData, true));
						oGroupHeaderData = oCurrentGroupHeaderData;
					}

					aItemList.push(oItem);
				}

				return aItemList;
			},
			fnGetMinOrMaxFromItems = function () {
				var oItem;
				// first time load with no filter and search
				if (!this._maxDate && !this._minDate) {
					if (this.getSort() || this._isGrouped()) {
						// find first non group item value
						for (var i = 0; i < aItems.length; i++) {
							oItem = aItems[i];
							if (!oItem._isGroupHeader) {
								this._sortOrder === SortOrder.ASCENDING ? this._minDate = oItem.getDateTime() : this._maxDate = oItem.getDateTime(); //eslint-disable-line
								break;
							}
						}
					}
				}
			},
			oGroup, oConvertedDate, iGroupID, bGroupFirstItem;

		// start
		fnGetMinOrMaxFromItems.call(this);

		// sort for client filtering or for usage without binding
		if ((!this._useBinding() || !this._useModelFilter()) && this.getSort()) {
			aItems = this._sort(aItems);
		}

		// if we don't use model filter, filter data now
		aFilteredItems = this._useModelFilter() ? aItems : this._clientFilter(aItems);

		// remove group items -> required for correct computing show more
		aFilteredItems = aFilteredItems.filter(function (item) {
			return !item._isGroupHeader;
		});

		this._showMore = this.getForceGrowing();
		if (!this._showMore && this._displayShowMore()) {
			// there are more items to display then we can => show more for both model or client filter
			this._showMore = aFilteredItems.length > this._iItemCount;
			// for model filter we need to take care of equality of items to show and items to display
			// for client model this is already covered by first condition because aFilteredItems should contain
			// all available items
			if (!this._showMore && this._useModelFilter()) {
				this._showMore = aFilteredItems.length === this._iItemCount && this._iItemCount < this._getMaxItemsCount();
			}
		}

		// filter for visible items - both cases, model binding or content adding
		aFilteredItems = aFilteredItems.filter(function (item) {
			return item.getVisible();
		});

		fnTrimDataAfterItemCount.call(this);

		this._outputItem = [];

		// if we don't have binding we have to create groups ourself
		if (this._isGrouped()) {
			if (!this._useBinding()) {
				aItems = fnAppendGroupHeaderItemsWithoutBinding.call(this);
			}

			// get group timeline item values  eg: 2016,2015,2014 etc
			var aGroupItems = aItems.filter(function (oItem) {
				return oItem._isGroupHeader;
			});
			this._groupCount = aGroupItems.length;

			for (var i = 0; i < aGroupItems.length; i++) {
				oGroup = aGroupItems[i];
				//get the year from timelineitem
				iGroupID = oGroup.getUserName();
				// for client filtering, we want to add only groups with at least one item.
				bGroupFirstItem = true;

				oGroup._groupID = iGroupID;

				for (var k = 0; k < aFilteredItems.length; k++) {
					var item = aFilteredItems[k];

					//check if it is not a group timelineitem
					oConvertedDate = this._formatGroupBy(item.getDateTime(), this.getGroupByType());

					if (oConvertedDate.key == iGroupID && !item._isGroupHeader) {
						if (bGroupFirstItem) {
							//add group timeline item to the outputItem array
							this._outputItem.push(oGroup);
							bGroupFirstItem = false;
						}
						item._groupID = iGroupID;
						this._outputItem.push(item);
					}
				}
			}

		} else {
			//TODO: global jquery call found
			this._outputItem = jQuery.extend(true, [], aFilteredItems);
		}
	};

	/**
	 * maximum items either in binding (model) or in content (timeline without binding)
	 * @returns {Number} Maximum items for timeline
	 * @private
	 */
	Timeline.prototype._getMaxItemsCount = function () {
		var oBinding = this.getBinding("content"),
			oModel, iModelSizeLimit, iBindingLength;

		if (oBinding) {
			iBindingLength = oBinding.getLength() || 0;
			// Model should be taken from content aggregation otherwise incorrect model can be fetched while using named models.
			oModel = oBinding.getModel();
			iModelSizeLimit = oModel && oModel.iSizeLimit;

			return Math.min(iBindingLength, iModelSizeLimit || iBindingLength);
		}

		return this.getContent().length;
	};

	/**
	 * Display custom message (if there is any)
	 * @private
	 */
	Timeline.prototype._showCustomMessage = function () {
		var bShow = !!this._customMessage,
			$obj = this._objects.getMessageStrip().$();

		this._objects.getMessageStrip().setVisible(bShow);
		this._objects.getMessageStrip().setText(this._customMessage);

		if (bShow) {
			$obj.show();
		} else {
			$obj.hide();
		}
	};

	/**
	 * Performs expand or collapse for group
	 * @param {string} sGroupID Collapsing (expanding) group ID
	 * @param {boolean} bExpand Indicates whether we are expanding or collapsing.
	 * @private
	 * @returns {Promise} A new promise for when expansion/collapse is done
	 */
	Timeline.prototype._performExpandCollapse = function (sGroupID, bExpand) {
		var that = this, //eslint-disable-line
			$items,
			$itemsWrapper,
			$this = this.$(),
			sMethod = bExpand ? "slideDown" : "slideUp",
			ANIMATIONS_SPEED = 250;

		$this.find('li[groupid="' + sGroupID + '"][nodeType="GroupHeaderBar"]').each(function (iIndex, oLi) {
			var $li = jQuery(oLi);
			if (!bExpand) {
				$li.addClass("sapSuiteUiCommonsTimelineItemGroupCollapsedBar");
			} else {
				$li.removeClass("sapSuiteUiCommonsTimelineItemGroupCollapsedBar");
			}
		});

		$items = $this.find('li[groupid="' + sGroupID + '"][nodeType!="GroupHeader"][nodeType!="GroupHeaderBar"]');
		if (this.getAxisOrientation() === "Vertical") {
			$itemsWrapper = $items.parent();
		}

		return new Promise(function (resolve, reject) {
			if (that._noAnimation) {
				if (bExpand) {
					if ($itemsWrapper) {
						$itemsWrapper.show();
					} else {
						$items.show();
					}
				} else {
					if ($itemsWrapper) {
						$itemsWrapper.hide();
					} else {
						$items.hide();
					}
				}
				resolve();
			} else {
				if (that._isVertical()) {
					$items[sMethod](ANIMATIONS_SPEED);
				} else {
					$items.animate({width: 'toggle'}, 350);
				}

				$items.promise().done(function () {
					resolve();
				});
			}
		});
	};

	/**
	 * This method is called after rendering of the item is completed
	 * @private
	 */
	Timeline.prototype._itemRendered = function () {
		// if the whole timeline is already rendered this indicates single item was invalidated
		// in such case we need to trigger the whole timeline UI calculation
		// most important case is double sided timeline but there may be other misproprotions
		if (this._bRendered) {
			this.adjustUI();
		}
	};

	/* =========================================================== */
	/* Keyboard handling */
	/* =========================================================== */
	Timeline.prototype._startItemNavigation = function (oEvent) {
		var oItemsInfo = this._getItemsForNavigation(),
			$content = this.$("content").get(0) || this.$("contentH").get(0),
			fnGetSelectedItems = function (oFocuseDomRef, aOutputItems) {
				return aOutputItems.filter(function (oItem) {
					return oItem.getFocusDomRef() === oFocuseDomRef;
				});
			};

		if (!this.oItemNavigation) {
			this.oItemNavigation = new TimelineNavigator($content, oItemsInfo.items, false, oItemsInfo.rows);
			this.oItemNavigation.setPageSize(10);
			this.oItemNavigation.attachEvent("AfterFocus", function (oEvent) {
				var oFocusedDomRef = this.oItemNavigation.getItemDomRefs()[oEvent.getParameter("index")],
					aFocusedItems = fnGetSelectedItems(oFocusedDomRef, this._outputItem);
				if (aFocusedItems[0]) {
					this.fireSelect({
						selectedItem: aFocusedItems[0],
						userAction: oEvent.mParameters.event && oEvent.mParameters.event.type === "mousedown"
					});
				}
			}, this);
			this.oItemNavigation.attachEvent("Enter", function (oEvent) {
				var aFocusedItems = fnGetSelectedItems(oEvent.getParameter("domRef"), this._outputItem);
				if (aFocusedItems[0]) {
					this.fireSelect({selectedItem: aFocusedItems[0], userAction: true});
				}
			}, this);
			this.addDelegate(this.oItemNavigation);
		} else {
			this.oItemNavigation.updateReferences($content, oItemsInfo.items, oItemsInfo.rows);
		}
		if (oItemsInfo.columns) {
			this.oItemNavigation.setColumns(oItemsInfo.columns, false);
		}
	};

	Timeline.prototype._getItemsForNavigation = function () {
		var oItemsInfo = {},
			oShowMore,
			aTop,
			aBottom,
			aCurrentRow,
			iMaxRowSize;
		if (this._renderDblSided) {
			if (this._isVertical()) {
				oItemsInfo.items = this._outputItem;
				oItemsInfo.rows = [];
				aCurrentRow = [];
				oItemsInfo.items.forEach(function (oItem) {
					var $item = oItem.$(),
						bIsLeft = $item.hasClass("sapSuiteUiCommonsTimelineItemWrapperVLeft") || $item.hasClass("sapSuiteUiCommonsTimelineItemOdd");
					if (bIsLeft && aCurrentRow.length === 1) {
						aCurrentRow.push(null);
					} else if (!bIsLeft && aCurrentRow.length === 0) {
						aCurrentRow.push(null);
					}
					if (aCurrentRow.length > 1) {
						oItemsInfo.rows.push(aCurrentRow);
						aCurrentRow = [];
					}
					aCurrentRow.push(oItem);
				});
				if (aCurrentRow.length > 0) {
					oItemsInfo.rows.push(aCurrentRow);
				}
			} else {
				aTop = [];
				aBottom = [];
				this._outputItem.forEach(function (oItem) {
					if (oItem._placementLine === "top") {
						aTop.push(oItem);
					} else {
						while (aBottom.length + 1 < aTop.length) {
							aBottom.push(null);
						}
						aBottom.push(oItem);
					}
				});
				oItemsInfo.items = this._outputItem;
				oItemsInfo.rows = [aTop, aBottom];
			}
		} else {
			oItemsInfo.items = this._outputItem;
		}
		oItemsInfo.items = oItemsInfo.items.map(function (oItem) {
			return oItem.getFocusDomRef();
		});
		if (oItemsInfo.rows) {
			iMaxRowSize = 0;
			oItemsInfo.rows = oItemsInfo.rows.map(function (aRow) {
				if (aRow.length > iMaxRowSize) {
					iMaxRowSize = aRow.length;
				}
				return aRow.map(function (oItem) {
					return oItem === null ? null : oItem.getFocusDomRef();
				});
			});
			// Make sure all rows have same length.
			oItemsInfo.rows.forEach(function (aRow) {
				while (aRow.length < iMaxRowSize) {
					aRow.push(null);
				}
			});
		}
		if (this._showMore) {
			oShowMore = this._objects.getMoreButton().getFocusDomRef();
			oItemsInfo.items.push(oShowMore);
			if (oItemsInfo.rows) {
				if (this._isVertical()) {
					if (oItemsInfo.rows.length > 0) {
						oItemsInfo.rows.push(oItemsInfo.rows[0].map(function (oValue, i, aRow) {
							if (i === aRow.length - 1) {
								return oShowMore;
							} else {
								return null;
							}
						}));
					} else {
						oItemsInfo.rows.push([oShowMore]);
					}
				} else {
					oItemsInfo.rows.forEach(function (aRow, i, aArray) {
						if (i === aArray.length - 1) {
							aRow.push(oShowMore);
						} else {
							aRow.push(null);
						}
					});
				}
			}
		}
		return oItemsInfo;
	};

	/* =========================================================== */
	/* Setters & getters */
	/* =========================================================== */
	Timeline.prototype.setShowItemFilter = function (bShowItemFilter) {
		this.setProperty("showItemFilter", bShowItemFilter, true);
		if (this._objects.isObjectInitialized("FilterContent")) {
			this._setupFilterFirstPage(this._objects.getFilterContent());
		}
		this._objects.getFilterIcon().setVisible(bShowItemFilter || this.getShowTimeFilter());

		return this;
	};

	Timeline.prototype.setShowTimeFilter = function (bShowTimeFilter) {
		this.setProperty("showTimeFilter", bShowTimeFilter, true);
		if (this._objects.isObjectInitialized("FilterContent")) {
			this._setupFilterFirstPage(this._objects.getFilterContent());
		}
		this._objects.getFilterIcon().setVisible(bShowTimeFilter || this.getShowItemFilter());

		return this;
	};

	Timeline.prototype._getFilterTitle = function () {
		var sValue = this.getFilterTitle();
		if (!sValue) {
			sValue = resourceBundle.getText("TIMELINE_FILTER_ITEMS");
		}
		return sValue;
	};

	Timeline.prototype.getNoDataText = function () {
		var sText = this.getProperty("noDataText");
		if (!sText) {
			sText = resourceBundle.getText('TIMELINE_NO_DATA');
		}

		return sText;
	};

	Timeline.prototype.setSortOldestFirst = function (bOldestFirst) {
		this._sortOrder = bOldestFirst ? SortOrder.ASCENDING : SortOrder.DESCENDING;
		this._objects.getSortIcon().setIcon(this._sortOrder === SortOrder.ASCENDING ?
			"sap-icon://arrow-bottom" : "sap-icon://arrow-top");

		this._objects.getSortIcon().setTooltip(this._sortOrder === SortOrder.ASCENDING
			? resourceBundle.getText("TIMELINE_SORT_ASCENDING")
			: resourceBundle.getText("TIMELINE_SORT_DESCENDING"));

		this.setProperty("sortOldestFirst", bOldestFirst);

		return this;
	};

	Timeline.prototype.setGrowingThreshold = function (growingThreshold) {
		this.setProperty("growingThreshold", growingThreshold, true);
		this._iItemCount = growingThreshold;

		return this;
	};

	Timeline.prototype.setShowHeaderBar = function (oShowHeaderBar) {
		this.setProperty("showHeaderBar", oShowHeaderBar, true);
		this._objects.getHeaderBar().setVisible(oShowHeaderBar);

		return this;
	};

	Timeline.prototype.setSort = function (bSort) {
		this.setProperty("sort", bSort);
		this._objects.getSortIcon().setVisible(bSort && this.getShowSort());

		return this;
	};

	Timeline.prototype.setAxisOrientation = function (bAxisOrientation) {
		this.setProperty("axisOrientation", bAxisOrientation);

		// for axis change in runtime --> scroller ID will change, so we have to recreate scroller
		if (this._oScroller) {
			this._oScroller.destroy();
			this._oScroller = null;
		}

		return this;
	};

	Timeline.prototype.setEnableDoubleSided = function (sEnableDoubleSided) {
		this.setProperty("enableDoubleSided", sEnableDoubleSided);
		this._renderDblSided = sEnableDoubleSided;

		return this;
	};

	/**
	 * Returns the keys of the currently applied filter criteria as an array of objects, containing
	 * filter criteria keys (<code>key</code>) and values (<code>text</code>).
	 * @returns {Array} List Applied filters
	 * @public
	 */
	Timeline.prototype.getCurrentFilter = function () {
		return this._currentFilterKeys.map(function (oItem) {
			return {
				key: oItem.key,
				text: oItem.text || oItem.key
			};
		});
	};

	/**
	 * Reset all internal settings. Use if you want the same instance with different data.
	 * @public
	 */
	Timeline.prototype.reset = function () {
		// number of items currently selected
		this._iItemCount = 0;

		// array of selected filter items. { key; text }
		this._aFilterList = [];

		// associative array of groups which are collapsed (are in array with true value)
		this._collapsedGroups = {};

		// indicator whether timeline is rendered as double sided. It is working only for vertical mode. The flag 'enableDoubleSided' must be true,
		// but also there have to be enough width
		this._renderDblSided = null;

		// internal counter of groups. The group iteration goes through '_createGroupHeader'
		this._groupId = 0;

		// store last scroll position. 'x' and 'y' are for scrolling buttons, 'more' is used when new load of data is loaded to scroll back to focused item
		this._lastScrollPosition = {
			x: 0,
			y: 0,
			more: 0
		};

		this._sortOrder = this.getSortOldestFirst() ? SortOrder.ASCENDING : SortOrder.DESCENDING;

		// indicator that scrolling buttons are already set and there is no need to recalculate them again (performance)
		this._scrollersSet = false;

		// array of objects {key; text} with data filter selected items
		this._currentFilterKeys = [];

		// indicates whether timeline is rendered or not
		this._bRendered = false;

		this._noAnimation = true;
	};

	Timeline.prototype.setShowFilterBar = function (flag) {
		this.setProperty("showFilterBar", flag, true);
		this.setShowHeaderBar(flag);

		return this;
	};

	Timeline.prototype.setShowSearch = function (flag) {
		this.setProperty("showSearch", flag, true);
		this._objects.getSearchField().setVisible(!!flag);

		return this;
	};

	Timeline.prototype.setShowSort = function (bFlag) {
		this.setProperty("showSort", bFlag, true);
		this._objects.getSortIcon().setVisible(this.getSort() && bFlag);

		return this;
	};

	/**
	 * Sets a custom message that is displayed below the timeline's header.
	 * @param {string} sMsg Message text.
	 * @public
	 */
	Timeline.prototype.setCustomMessage = function (sMsg) {
		this._customMessage = sMsg;
		this._showCustomMessage();

		return this;
	};

	Timeline.prototype.getHeaderBar = function () {
		return this._objects.getHeaderBar();
	};

	Timeline.prototype.getMessageStrip = function () {
		return this._objects.getMessageStrip();
	};

	Timeline.prototype.setContent = function (oContents) {
		this.removeAllContent();
		var currentGroup = 0;
		for (var i = 0; i < oContents.length; i++) {
			var oItem = oContents[i];
			if (oItem instanceof TimelineItem) {
				if (this._isGrouped()) {
					var oGroup = this._formatGroupBy(oItem.getDateTime(), this.getGroupByType());
					if (oGroup.key !== currentGroup.key) {
						this._createGroupHeader(oGroup);
						currentGroup = oGroup;
					}
				}
				this.addContent(oItem);
			}
		}

		// reset itemcount and set it again in onbeforeloading
		this._iItemCount = 0;

		return this;
	};

	Timeline.prototype.setData = function (oData) {
		var INTERNAL_MODEL_NAME = "sapsuiteuicommonsTimelineInternalModel",
			oInternalModel = new JSONModel(),
			sPath, oBindOptions,
			fnDefaultItemsFactory = function (sId, oContext) {
				var oTimelineItem = new TimelineItem({
					dateTime: oContext.getProperty("dateTime"),
					icon: oContext.getProperty("icon"),
					userName: oContext.getProperty("userName"),
					title: oContext.getProperty("title"),
					text: oContext.getProperty("text"),
					filterValue: oContext.getProperty("filterValue")
				});

				if (oContext.getProperty("content")) {
					oTimelineItem.setEmbeddedControl(oContext.getProperty("content"));
				}
				return oTimelineItem;
			},
			fnBuildPath = function (sPath, sModelName) {
				var sCompletePath = sPath;
				if (sModelName) {
					sCompletePath = sModelName + ">" + sPath;
				}
				return sCompletePath;
			};

		if (typeof oData === "undefined") {
			return this;
		}

		sPath = fnBuildPath("/", INTERNAL_MODEL_NAME);
		oInternalModel.setData(oData);

		this.setModel(oInternalModel, INTERNAL_MODEL_NAME);
		this.setProperty("data", oData, true);

		oBindOptions = {
			path: sPath,
			sorter: this._getDefaultSorter('dateTime', this.getSortOldestFirst()),
			factory: jQuery.proxy(fnDefaultItemsFactory, this)
		};

		if (this._isGrouped()) {
			oBindOptions.groupHeaderFactory = jQuery.proxy(this._getGroupHeader, this);
		}

		this.bindAggregation("content", oBindOptions);
		return this;
	};

	Timeline.prototype.getSuspendSocialFeature = function () {
		return this._suspenseSocial;
	};

	Timeline.prototype.setSuspendSocialFeature = function (bSuspense) {
		this._suspenseSocial = bSuspense;
		if (!this.getEnableSocial()) {
			return;
		}

		var oItems = this.getContent();
		for (var i = 0; i < oItems.length; i++) {
			oItems[i]._objects.getReplyLink().setEnabled(!bSuspense);
		}

		this.invalidate();

		return this;
	};

	Timeline.prototype.updateFilterList = function () {
		this.updateAggregation("filterList");
		this._setFilterList();
	};

	Timeline.prototype.setGroupByType = function (sType) {
		var oBindingInfo = this.getBindingInfo("content");
		this.setProperty("groupByType", sType);

		if (oBindingInfo) {
			this._bindGroupingAndSorting(oBindingInfo);
			this.updateAggregation("content");
		}

		return this;
	};

	/* =========================================================== */
	/* Backward compatibility */
	/* =========================================================== */
	Timeline.prototype.getGroup = function () {
		return this.getGroupByType() !== "None";
	};

	Timeline.prototype.setGroup = function (bGroup) {
		if (bGroup && this.getGroupByType() === TimelineGroupType.None) {
			this.setGroupByType(TimelineGroupType.Year);
		}

		if (!bGroup) {
			this.setGroupByType(TimelineGroupType.None);
		}

		return this;
	};

	Timeline.prototype.setGrowing = function (bGrowing) {
		if (!bGrowing) {
			this.setGrowingThreshold(0);
		}

		return this;
	};

	Timeline.prototype.getGrowing = function (bGrowing) {
		return this.getGrowingThreshold() !== 0;
	};

	Timeline.prototype.setEnableBackendFilter = function (sBackEndFilter) {
		this.setProperty("enableModelFilter", sBackEndFilter);

		return this;
	};

	Timeline.prototype.getEnableBackendFilter = function () {
		return this.getProperty("enableModelFilter");
	};

	/* =========================================================== */
	/* Helper methods */
	/* =========================================================== */
	Timeline.prototype._isGrouped = function () {
		return (this.getGroupByType() !== TimelineGroupType.None || this._fnCustomGroupBy) &&
			(this.getGroupBy() !== "");
	};

	Timeline.prototype._lazyLoading = function () {
		return this.getEnableScroll() && this.getLazyLoading();
	};

	Timeline.prototype._loadAllData = function (useBinding) {
		// we load all possible data when use have modelfilter OFF -> filtering above only client data
		return !this._useModelFilter(useBinding);
	};

	Timeline.prototype._isVertical = function () {
		return TimelineAxisOrientation.Vertical === this.getAxisOrientation();
	};

	Timeline.prototype._displayShowMore = function () {
		return this.getForceGrowing() || (this.getGrowingThreshold() !== 0 && !this._lazyLoading());
	};

	Timeline.prototype._useGrowing = function () {
		return this.getForceGrowing() || this.getGrowingThreshold() !== 0 || this._lazyLoading();
	};

	Timeline.prototype._isMaxed = function () {
		return this._iItemCount >= this._getMaxItemsCount();
	};

	Timeline.prototype._useModelFilter = function (useBinding) {
		return this.getEnableModelFilter() && (useBinding || this._useTemplateBinding() || this._useFactoryBinding());
	};

	Timeline.prototype._scrollingFadeout = function (useBinding) {
		return this.getScrollingFadeout() !== ScrollingFadeout.None && this.getEnableScroll();
	};

	Timeline.prototype._setBusy = function (bBusy) {
		if (this.getEnableBusyIndicator()) {
			this.setBusy(bBusy);
		}
	};

	Timeline.prototype._fireSelectionChange = function (oParams) {
		return this.fireEvent("filterSelectionChange", oParams, true);
	};

	Timeline.prototype._isLeftAlignment = function () {
		return this.getAlignment() === TimelineAlignment.Left || this.getAlignment() === TimelineAlignment.Top;
	};

	Timeline.prototype._useBinding = function (bTemplateBinding) {
		return this.getBindingInfo("content") != null;
	};

	Timeline.prototype._useTemplateBinding = function () {
		var oContent = this.getBindingInfo("content");
		return oContent && oContent.template != null;
	};

	Timeline.prototype._useFactoryBinding = function () {
		var oContent = this.getBindingInfo("content");
		return oContent && oContent.factory != null;
	};


	Timeline.prototype._useAutomaticHeight = function () {
		return this.getTextHeight().toLowerCase() === "automatic" && !this._isVertical();
	};

	/**
	 * Returns number of items which are to be rendered.
	 * @returns {Number} The number of items
	 * @private
	 */
	Timeline.prototype._getItemsCount = function () {
		return this._outputItem ? this._outputItem.length : 0;
	};

	TimelineRenderManager.extendTimeline(Timeline);

	return Timeline;
});
