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

sap.ui.define([
	'sap/ui/core/Control',
	'./TimePickerSliderRenderer',
	'sap/ui/core/IconPool',
	'sap/ui/Device',
	"sap/ui/events/KeyCodes",
	"sap/m/Button",
	"sap/ui/thirdparty/jquery",
	"sap/ui/core/Configuration"
],
	function(Control, TimePickerSliderRenderer, IconPool, Device, KeyCodes, Button, jQuery, Configuration) {
		"use strict";

		/**
		 * Constructor for a new <code>TimePickerSlider</code>.
		 *
		 * @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
		 * A picker list control used inside a {@link sap.m.TimePicker} to choose a value.
		 * @extends sap.ui.core.Control
		 *
		 * @author SAP SE
		 * @version 1.108.49
		 *
		 * @constructor
		 * @private
		 * @since 1.32
		 * @alias sap.m.TimePickerSlider
		 */
		var TimePickerSlider = Control.extend("sap.m.TimePickerSlider", /** @lends sap.m.TimePickerSlider.prototype */ {
			metadata: {
				library: "sap.m",
				properties: {
					/**
					 * The key of the currently selected value of the slider.
					 */
					selectedValue: {type: "string", defaultValue: null},
					/**
					 * Indicates whether the slider supports cyclic scrolling.
					 */
					isCyclic: {type: "boolean", defaultValue: true},
					/**
					 * Defines the descriptive text for the slider, placed as a label above it.
					 */
					label: {type: "string", defaultValue: null},
					/**
					 * Indicates whether the slider is currently expanded.
					 */
					isExpanded: {type: "boolean", defaultValue: false}
				},
				aggregations: {
					/**
					 * Aggregation that contains the items of the slider.
					 */
					items: {type: "sap.m.VisibleItem", multiple: true, singularName: "item"},

					/**
					 * Aggregation that contains the up arrow.
					 */
					_arrowUp: {type: "sap.m.Button", multiple: false, visibility: "hidden" },

					/**
					 * Aggregation that contains the down arrow.
					 */
					_arrowDown: {type: "sap.m.Button", multiple: false, visibility: "hidden" }
				},
				events: {
					/**
					 * Fires when the slider is expanded.
					 */
					expanded: {},
					/**
					 * Fires when the slider is collapsed.
					 * @since 1.54
					 */
					collapsed: {}
				}
			},
			renderer: TimePickerSliderRenderer
		});

		var SCROLL_ANIMATION_DURATION = Configuration.getAnimation() ? 200 : 0;
		var MIN_ITEMS = 50;
		var LABEL_HEIGHT = 32;
		var ARROW_HEIGHT = 32;

		/**
		 * Initializes the control.
		 *
		 * @public
		 */
		TimePickerSlider.prototype.init = function() {
			this._bIsDrag = null;
			this._selectionOffset = 0;
			this._mousedown = false;
			this._dragSession = null;
			this._iSelectedItemIndex = -1;
			this._animatingSnap = false;
			this._iSelectedIndex = -1;
			this._animating = false;
			this._intervalId = null;
			this._maxScrollTop = null;
			this._minScrollTop = null;
			this._marginTop = null;
			this._marginBottom = null;
			this._bOneTimeValueSelectionAnimation = false;
			this._bEnabled = true;

			if (Device.system.desktop) {
				this._fnHandleTypeValues = fnTimedKeydownHelper.call(this);
			}

			this._initArrows();
		};

		TimePickerSlider.prototype.exit = function() {
			this._detachEvents();

			var $Slider = this._getSliderContainerDomRef();

			if ($Slider) {
				$Slider.stop();
			}

			if (this._intervalId) {
				clearInterval(this._intervalId);
				this._intervalId = null;
			}
		};

		/**
		 * Called before the control is rendered.
		 */
		TimePickerSlider.prototype.onBeforeRendering = function () {
			this._detachEvents();
		};

		/**
		 * Called after the control is rendered.
		 */
		TimePickerSlider.prototype.onAfterRendering = function () {
			if (Device.system.phone) { //the layout still 'moves' at this point - dialog and its content, so wait a little
				setTimeout(this._afterExpandCollapse.bind(this), 0);
			} else {
				this._afterExpandCollapse();
			}
			this._attachEvents();
		};

		/**
		 * Handles the themeChanged event.
		 *
		 * Does a re-rendering of the control.
		 * @param {jQuery.Event} oEvent Event object
		 */
		TimePickerSlider.prototype.onThemeChanged = function(oEvent) {
			this.invalidate();
		};

		/**
		 * Handles the tap event.
		 *
		 * Expands or selects the taped element.
		 * @param {jQuery.Event} oEvent Event object
		 */
		TimePickerSlider.prototype.fireTap = function(oEvent) {
			var oScrElement,
				sItemText,
				sItemKey;

			//expand column with a click
			if (!this.getIsExpanded()) {
				if (Device.system.desktop) {
					this.focus();
				} else {
					this.setIsExpanded(true);
				}
			} else { //or select an element from the list
				oScrElement = oEvent.srcElement || oEvent.originalTarget;

				if (oScrElement && oScrElement.tagName.toLowerCase() === "li") {
					sItemText = jQuery(oScrElement).text();
					sItemKey  = fnFindKeyByText.call(this, sItemText);

					this._iClickedIndex =
						Array.prototype.slice.call(oScrElement.parentElement.children).indexOf(oScrElement);
					this._bOneTimeValueSelectionAnimation = true;
					this.setSelectedValue(sItemKey);
					this._fireSelectedValueChange(sItemKey);
				} else { //if no selection is happening, return the selected style which was removed ontouchstart
					this._addSelectionStyle();
					this.focus();
				}
			}
		};

		/**
		 * Sets the currently selected value with an item key.
		 *
		 * @override
		 * @param {string} sValue The key of the new selected value
		 * @returns {sap.ui.base.ManagedObject}
		 * @public
		 */
		TimePickerSlider.prototype.setSelectedValue = function(sValue) {
			var iIndexOfValue = findIndexInArray(this._getVisibleItems(), function(oElement) {
					return oElement.getKey() === sValue;
				}),
				that = this,
				iIndex,
				$Slider,
				iItemHeightInPx,
				iContentRepeats;

			if (iIndexOfValue === -1) {
				return this;
			}

			//scroll
			if (this.getDomRef()) {
				$Slider = this._getSliderContainerDomRef();
				iItemHeightInPx = this._getItemHeightInPx();
				iContentRepeats = this._getContentRepeat();

				//list items' values are repeated, so find the one nearest to the middle of the list
				if (iIndexOfValue * iItemHeightInPx >= this._selectionOffset) {
					iIndex = this._getVisibleItems().length * Math.floor(iContentRepeats / 2) + iIndexOfValue;
				} else {
					iIndex = this._getVisibleItems().length * Math.ceil(iContentRepeats / 2) + iIndexOfValue;
				}

				if (this._bOneTimeValueSelectionAnimation) {
					$Slider.scrollTop((iIndex - this._iClickedIndex + this._iSelectedItemIndex) * iItemHeightInPx - this._selectionOffset);
					this._animatingSnap = true;
					$Slider.animate({scrollTop: iIndex * iItemHeightInPx - this._selectionOffset}, SCROLL_ANIMATION_DURATION, 'linear', function () {
						$Slider.clearQueue();
						that._animatingSnap = false;
						that._bOneTimeValueSelectionAnimation = false;
					});
				} else {
					$Slider.scrollTop(iIndex * iItemHeightInPx - this._selectionOffset);
				}

				this._removeSelectionStyle();
				this._iSelectedItemIndex = iIndex; //because we repeated content / values

				this._addSelectionStyle();
			}

			return this.setProperty("selectedValue", sValue, true); // no rerendering
		};

		/**
		 * Sets the <code>isExpanded</code> property of the slider.
		 *
		 * @override
		 * @param {boolean} bValue True or false
		 * @param {boolean} suppressEvent Whether to suppress event firing
		 * @returns {this} this instance, used for chaining
		 * @public
		 */
		TimePickerSlider.prototype.setIsExpanded = function(bValue, suppressEvent) {
			this.setProperty("isExpanded", bValue, true);

			if (!this.getDomRef()) {
				return this;
			}

			var $This = this.$();

			if (bValue) {
				$This.addClass("sapMTPSliderExpanded");

				if (Device.system.phone) {
					setTimeout(function() {
						this._updateDynamicLayout(bValue);
						if (!suppressEvent) {
							this.fireExpanded({ctrl: this});
						}
					}.bind(this), 0);
				} else {
					this._updateDynamicLayout(bValue);
					if (!suppressEvent) {
						this.fireExpanded({ctrl: this});
					}
				}
			} else {
				this._stopAnimation(); //stop any schedule(interval) for animation
				//stop snap animation also
				if (this._animatingSnap === true) {
					this._animatingSnap = false;
					this._getSliderContainerDomRef().stop(true);
					//Be careful not to invoke this method twice (the first time is on animate finish callback).
					//If this is the first animation, the _iSelectedIndex will remain its initial value, so no need
					//to notify the scroller about any snap completion
					if (this._animatingTargetIndex !== null && this._animatingTargetIndex !== undefined) {
						this._scrollerSnapped(this._animatingTargetIndex);
						this._animatingTargetIndex = null;
					} else if (this._iSelectedIndex !== -1) {
						this._scrollerSnapped(this._iSelectedIndex);
					}
				}

				$This.removeClass("sapMTPSliderExpanded");
				this._updateMargins(bValue);

				if (Device.system.phone) {
					setTimeout(this._afterExpandCollapse.bind(this), 0);
				} else {
					this._afterExpandCollapse();
				}
				if (!suppressEvent) {
					this.fireCollapsed({ctrl: this});
				}
			}


			return this;
		};

		/**
		 * Handles the focusin event.
		 *
		 * Expands the focused slider.
		 * @param {jQuery.Event} oEvent Event object
		 */
		TimePickerSlider.prototype.onfocusin = function(oEvent) {
			if (Device.system.desktop && !this.getIsExpanded()) {
				this.setIsExpanded(true);
			}
		};

		/**
		 * Handles the focusout event.
		 *
		 * Make sure the blurred slider is collapsed on desktop.
		 * @param {jQuery.Event} oEvent Event object
		 */
		TimePickerSlider.prototype.onfocusout = function(oEvent) {

			var sFocusedElementId = oEvent.relatedTarget ? oEvent.relatedTarget.id : null,
				aArrowsIds = [this.getAggregation("_arrowUp").getId(), this.getAggregation("_arrowDown").getId()];

			// Do not close, if any of the arrows is clicked
			if (sFocusedElementId && aArrowsIds.indexOf(sFocusedElementId) !== -1) {
				return;
			}

			if (Device.system.desktop && this.getIsExpanded()) {
				this.setIsExpanded(false);
			}
		};

		TimePickerSlider.prototype._onmousewheel = function(oEvent) {
			var oOriginalEvent,
				bDirectionPositive,
				wheelData;

			// prevent the default behavior
			oEvent.preventDefault();
			oEvent.stopPropagation();

			if (!this.getIsExpanded() || this._intervalId) {
				return false;
			}

			oOriginalEvent = oEvent.originalEvent;
			bDirectionPositive = oOriginalEvent.detail ? (-oOriginalEvent.detail > 0) : (oOriginalEvent.wheelDelta > 0);
			wheelData = oOriginalEvent.detail ? (-oOriginalEvent.detail / 3) : (oOriginalEvent.wheelDelta / 120);

			if (!wheelData) {
				return false;
			}

			this._handleWheelScroll(bDirectionPositive, wheelData);
		};

		TimePickerSlider.prototype._handleWheelScroll = function(bDirectionPositive, wheelData) {
			var fnRound = bDirectionPositive ? Math.ceil : Math.floor,
				iResultOffset;

			if (!this._aWheelDeltas) {
				this._aWheelDeltas = [];
			}

			this._aWheelDeltas.push(wheelData);

			if (!this._bWheelScrolling) {
				this._bWheelScrolling = true;

				this._intervalId = setInterval(function () {
					if (!this._aWheelDeltas.length) {
						clearInterval(this._intervalId);
						this._intervalId = null;
						this._bWheelScrolling = false;
					} else {
						iResultOffset = this._aWheelDeltas[0]; //simplification, we could still use the array in some cases
						this._aWheelDeltas = [];

						iResultOffset = fnRound(iResultOffset);
						if (iResultOffset) { // !== 0, actually move
							this._offsetSlider(iResultOffset);
						}
					}
				}.bind(this), 150);
			}

			return false;
		};

		/**
		 * Handles the pageup event.
		 *
		 * Selects the first item value.
		 * @param {jQuery.Event} oEvent Event object
		 */
		TimePickerSlider.prototype.onsappageup = function(oEvent) {
			if (this.getIsExpanded()) {
				var iFirstItem = this._getVisibleItems()[0],
					sValue = iFirstItem.getKey();
				this.setSelectedValue(sValue);
				this._fireSelectedValueChange(sValue);
			}
		};

		/**
		 * Handles the pagedown event.
		 *
		 * Selects the last item value.
		 * @param {jQuery.Event} oEvent Event object
		 */
		TimePickerSlider.prototype.onsappagedown = function(oEvent) {
			if (this.getIsExpanded()) {
				var iLastItem = this._getVisibleItems()[this._getVisibleItems().length - 1],
					sValue = iLastItem.getKey();
				this.setSelectedValue(sValue);
				this._fireSelectedValueChange(sValue);
			}
		};

		/**
		 * Handles the arrowup event.
		 *
		 * Selects the previous item value.
		 * @param {jQuery.Event} oEvent Event object
		 */
		TimePickerSlider.prototype.onsapup = function(oEvent) {
			if (this.getIsExpanded()) {
				this._offsetValue(-1);
			}
		};

		/**
		 * Handles the arrowdown event.
		 *
		 * Selects the next item value.
		 * @param {jQuery.Event} oEvent Event object
		 */
		TimePickerSlider.prototype.onsapdown = function(oEvent) {
			if (this.getIsExpanded()) {
				this._offsetValue(1);
			}
		};

		/**
		 * Handles the keydown event.
		 *
		 * @param {jQuery.Event} oEvent Event object
		 */
		TimePickerSlider.prototype.onkeydown = function(oEvent) {
			var iKC = oEvent.which || oEvent.keyCode,
				oKCs = KeyCodes;

			if (iKC >= oKCs.NUMPAD_0 && iKC <= oKCs.NUMPAD_9){
				iKC = this._convertNumPadToNumKeyCode(iKC);
			}

			//we only recieve uppercase codes here, which is nice
			if ((iKC >= oKCs.A && iKC <= oKCs.Z)
				|| (iKC >= oKCs.DIGIT_0 && iKC <= oKCs.DIGIT_9)) {
				this._fnHandleTypeValues(oEvent.timeStamp, iKC);
			}
		};

		/**
		 * Finds the slider's container in the DOM.
		 *
		 * @returns {object} Slider container's jQuery object
		 * @private
		 */
		TimePickerSlider.prototype._getSliderContainerDomRef = function() {
			return this.$().find(".sapMTimePickerSlider");
		};

		/**
		 * Calculates how many times the slider content should be repeated so that it fills the space.
		 *
		 * The method is called only when isCyclic property is set to true.
		 * @returns {number} Content repetitions needed
		 * @private
		 */
		TimePickerSlider.prototype._getContentRepeat = function() {
			//how many times the content is repeated?
			//we target to get at least MIN_ITEMS items in the list,
			//so repeat the content as many times as it is needed to get that number
			//but repeat the content at least 3 times to ensure cyclic visibility
			var iContentRepeat;
			if (this.getIsCyclic()) {
				iContentRepeat = Math.ceil(MIN_ITEMS / this._getVisibleItems().length);
				iContentRepeat = Math.max(iContentRepeat, 3);
			} else {
				iContentRepeat = 1;
			}

			return iContentRepeat;
		};

		/**
		 * Gets the CSS height of a list item.
		 *
		 * @returns {number} CSS height in pixels
		 * @private
		 */
		TimePickerSlider.prototype._getItemHeightInPx = function() {
			return this.$("content").find("li:not(.TPSliderItemHidden)")[0].getBoundingClientRect().height;
		};

		/**
		 * Calculates the center of the column and places the border frame.
		 * @private
		 */
		TimePickerSlider.prototype._updateSelectionFrameLayout = function() {
			var $Frame,
				iFrameTopPosition,
				topPadding,
				iItemHeight,
				oSliderOffset = this.$().offset(),
				iSliderOffsetTop = oSliderOffset ? oSliderOffset.top : 0,
				oContainerOffset = this.$().parents(".sapMTimePickerContainer").offset(),
				iContainerOffsetTop = oContainerOffset ? oContainerOffset.top : 0;

			if (this.getDomRef()) {
				iItemHeight = this._getItemHeightInPx();
				$Frame = this.$().find(".sapMTPPickerSelectionFrame");

				//the frame is absolutly positioned in the middle of its container
				//its height is the same as the list items' height
				//so the top of the middle === container.height/2 - item.height/2 + label.height/2
				//corrected with the top of the container
				//the label height is added to the calculation in order to display the same amount of items above and below the selected one

				topPadding = iSliderOffsetTop - iContainerOffsetTop;
				iFrameTopPosition = (this.$().height() - iItemHeight + LABEL_HEIGHT) / 2 + topPadding;

				$Frame.css("top", iFrameTopPosition);

				if (Device.system.phone) {
					setTimeout(this._afterExpandCollapse.bind(this), 0);
				} else {
					this._afterExpandCollapse();
				}
			}
		};

		/**
		 * Updates the visibility of the slider's items based on the step and the selected value.
		 * @param {int} iNewValue The new selected value of the slider
		 * @param {int} iStep The precision step used for the slider
		 * @private
		 */
		TimePickerSlider.prototype._updateStepAndValue = function(iNewValue, iStep) {
			var	iVisibleItemsCount = 0,
				$SliderContainer,
				i;

			for (i = 0; i < this.getItems().length; i++) {
				if (i % iStep !== 0 && i !== iNewValue) {
					this.getItems()[i].setVisible(false);
				} else {
					this.getItems()[i].setVisible(true);
					iVisibleItemsCount++;
				}
			}

			if (iVisibleItemsCount > 2 && iVisibleItemsCount < 13 && this.getDomRef()) {
				$SliderContainer = this.$().find(".sapMTimePickerSlider");

				$SliderContainer.className = ""; //remove all classes
				jQuery($SliderContainer).addClass("sapMTimePickerSlider SliderValues" + iVisibleItemsCount.toString());
			}

			this.setIsCyclic(iVisibleItemsCount > 2);
			this.setSelectedValue(iNewValue.toString());
		};

		/**
		 * Updates the margins of a slider.
		 * Covers the cases where the slider is constrained to show an exact number of items.
		 * @param {boolean} bIsExpand If we update margins due to expand
		 * @private
		 */
		TimePickerSlider.prototype._updateMargins = function(bIsExpand) {
			var iItemHeight = this._getItemHeightInPx(),
				$ConstrainedSlider,
				iVisibleItems,
				iVisibleItemsTop,
				iVisibleItemsBottom,
				iFocusedItemTopPosition,
				iArrowHeight,
				iMarginTop,
				iMarginBottom;

			if (this.getDomRef()) {
				iItemHeight = this._getItemHeightInPx();
				//add margins only if the slider is constrained to show an exact number of items
				$ConstrainedSlider = this.$()
					.find(".SliderValues3,.SliderValues4,.SliderValues5,.SliderValues6,.SliderValues7,.SliderValues8,.SliderValues9,.SliderValues10,.SliderValues11,.SliderValues12");

				if (!$ConstrainedSlider.length) {
					return;
				}

				if (bIsExpand) {
					iVisibleItems = this._getVisibleItems().length;
					iVisibleItemsTop = iItemHeight * Math.floor(iVisibleItems / 2);
					iVisibleItemsBottom = iItemHeight * Math.ceil(iVisibleItems / 2);
					// arrow height if the same as label height
					// there are arrows only in compact mode
					iArrowHeight = this.$().parents().hasClass('sapUiSizeCompact') ? ARROW_HEIGHT : 0;
					iFocusedItemTopPosition = (this.$().height() - iItemHeight + LABEL_HEIGHT) / 2;
					iMarginTop = iFocusedItemTopPosition - iVisibleItemsTop - LABEL_HEIGHT - iArrowHeight;
					iMarginBottom = this.$().height() - iFocusedItemTopPosition - iVisibleItemsBottom - iArrowHeight;
					// add a margin only if there are less items than the maximum visible amount
					iMarginTop = Math.max(iMarginTop, 0);
					iMarginBottom = Math.max(iMarginBottom, 0);
				} else {
					iMarginTop = 0;
					iMarginBottom = 0;
				}

				$ConstrainedSlider.css("margin-top", iMarginTop);
				$ConstrainedSlider.css("margin-bottom", iMarginBottom);
			}
		};

		/**
		 * Updates the parts of the layout that depend on the slider's height.
		 * We call this method when the height changes - like at expand/collapse.
		 * @param {boolean} bIsExpand If we update due to expand
		 * @private
		 */
		TimePickerSlider.prototype._updateDynamicLayout = function (bIsExpand) {
			this._updateMargins(bIsExpand);
			this._updateSelectionFrameLayout();
		};

		/**
		 * Calculates the top offset of the border frame relative to its container.
		 * @private
		 * @returns {number} Top offset of the border frame
		 */
		TimePickerSlider.prototype._getSelectionFrameTopOffset = function() {
			var $Frame = this._getSliderContainerDomRef().find(".sapMTPPickerSelectionFrame"),
				oFrameOffset = $Frame.offset();
			return oFrameOffset.top;
		};

		/**
		 * Animates slider scrolling.
		 *
		 * @private
		 * @param {number} iSpeed Animating speed
		 */
		TimePickerSlider.prototype._animateScroll = function(iSpeed) {
			var $Container = this._getSliderContainerDomRef(),
				iPreviousScrollTop = $Container.scrollTop(),
				frameFrequencyMs = 25, //milliseconds - 40 frames per second; 1000ms / 40frames === 25
				$ContainerHeight = $Container.height(),
				$ContentHeight = this.$("content").height(),
				//increase the distance that the slider can be dragged before reaching one end of the list
				//because we do not do updates of list offset while dragging,
				//we have to keep that distance long at least while animating
				iDragMarginBuffer = 200,
				iDragMargin = $ContainerHeight + iDragMarginBuffer,
				iContentRepeat = this._getContentRepeat(),
				bCycle = this.getIsCyclic(),
				fDecelerationCoefficient = 0.9,
				fStopSpeed = 0.05,
				that = this;

			this._intervalId = setInterval(function() {
				that._animating = true;
				//calculate the new scroll offset by subtracting the distance
				iPreviousScrollTop = iPreviousScrollTop - iSpeed * frameFrequencyMs;
				if (bCycle) {
					iPreviousScrollTop = that._getUpdatedCycleScrollTop($ContainerHeight, $ContentHeight, iPreviousScrollTop, iDragMargin, iContentRepeat);
				} else {
					if (iPreviousScrollTop > that._maxScrollTop) {
						iPreviousScrollTop = that._maxScrollTop;
						iSpeed = 0;
					}

					if (iPreviousScrollTop < that._minScrollTop) {
						iPreviousScrollTop = that._minScrollTop;
						iSpeed = 0;
					}
				}
				$Container.scrollTop(iPreviousScrollTop);

				iSpeed *= fDecelerationCoefficient;
				if (Math.abs(iSpeed) < fStopSpeed) {  // px/milliseconds
					//snapping
					var iItemHeight = that._getItemHeightInPx();
					var iOffset = that._selectionOffset ? (that._selectionOffset % iItemHeight) : 0;
					var iSnapScrollTop = Math.round((iPreviousScrollTop  + iOffset) / iItemHeight) * iItemHeight - iOffset;

					clearInterval(that._intervalId);
					that._intervalId = null;
					that._animating = null; //not animating
					that._iSelectedIndex = Math.round((iPreviousScrollTop  + that._selectionOffset) / iItemHeight);

					that._animatingSnap = true;
					$Container.animate({ scrollTop: iSnapScrollTop}, SCROLL_ANIMATION_DURATION, 'linear', function() {
						$Container.clearQueue();
						that._animatingSnap = false;
						//make sure the DOM is still visible
						if ($Container.css("visibility") === "visible") {
							that._scrollerSnapped(that._iSelectedIndex);
						}
					});
				}
			}, frameFrequencyMs);
		};

		/**
		 * Stops the scrolling animation.
		 *
		 * @private
		 */
		TimePickerSlider.prototype._stopAnimation = function() {
			if (this._animating) {
				clearInterval(this._intervalId);
				this._intervalId = null;
				this._animating = null;
			}
		};

		/**
		 * Starts scroll session.
		 *
		 * @param {number} iPageY The starting y-coordinate of the target
		 * @private
		 */
		TimePickerSlider.prototype._startDrag = function(iPageY) {
			//start collecting touch coordinates
			if (!this._dragSession) {
				this._dragSession = {};
				this._dragSession.positions = [];
			}

			this._dragSession.pageY = iPageY;
			this._dragSession.startTop = this._getSliderContainerDomRef().scrollTop();
		};

		/**
		 * Performs vertical scroll.
		 *
		 * @param {number} iPageY The current y-coordinate of the target to scroll to
		 * @param {Date} dTimeStamp Timestamp of the event
		 * @private
		 */
		TimePickerSlider.prototype._doDrag = function(iPageY, dTimeStamp) {
			if (this._dragSession) {
				//calculate the distance between the start of the drag and the current touch
				this._dragSession.offsetY = iPageY - this._dragSession.pageY;

				this._dragSession.positions.push({pageY: iPageY, timeStamp: dTimeStamp});
				//to calculate speed we only need the last touch positions
				if (this._dragSession.positions.length > 20) {
					this._dragSession.positions.splice(0, 10);
				}

				if (this._bIsDrag) {
					//while dragging update the list position inside its container
					this._getSliderContainerDomRef().scrollTop(this._dragSession.startTop - this._dragSession.offsetY);
				}
			}
		};

		/**
		 * Finishes scroll session.
		 *
		 * @param {number} iPageY The last y-coordinate of the target to scroll to
		 * @param {Date} dTimeStamp Timestamp of the event
		 * @private
		 */
		TimePickerSlider.prototype._endDrag = function(iPageY, dTimeStamp) {
			if (this._dragSession) {
				var iOffsetTime, iOffsetY;
				//get only the offset calculated including the touches in the last 100ms
				for (var i = this._dragSession.positions.length - 1; i >= 0; i--) {
					iOffsetTime = dTimeStamp - this._dragSession.positions[i].timeStamp;
					iOffsetY = iPageY - this._dragSession.positions[i].pageY;
					if (iOffsetTime > 100) {
						break;
					}
				}

				var fSpeed = (iOffsetY / iOffsetTime);   // px/ms

				if (this._animating) {
					clearInterval(this._intervalId);
					this._intervalId = null;
					this._animating = null;
				}

				this._dragSession = null;
				this._animateScroll(fSpeed);
			}
		};

		/**
		 * Calculates the slider's selection y-offset and margins and selects the corresponding list value.
		 *
		 * @private
		 */
		TimePickerSlider.prototype._afterExpandCollapse = function () {
			var sSelectedValue = this.getSelectedValue(),
				oSelectionFrameTopOffset = this._getSelectionFrameTopOffset(),
				$Slider = this._getSliderContainerDomRef(),
				oSliderOffset = $Slider.offset(),
				iSliderHeight,
				$List,
				iListContainerHeight,
				iItemHeightInPx;

			//calculate the offset from the top of the list container to the selection frame
			this._selectionOffset =  oSelectionFrameTopOffset - oSliderOffset.top;

			if (!this.getIsCyclic()) {
				$List = this.$("content");
				iItemHeightInPx = this._getItemHeightInPx();
				iListContainerHeight = this.$().height();

				//if we do not cycle the items, we fill the remaining space with margins
				if (this.getIsExpanded()) {
					this._minScrollTop = 0;
					//top margin is as wide as the selection offset
					this._marginTop = oSelectionFrameTopOffset - oSliderOffset.top;
					this._maxScrollTop = iItemHeightInPx * (this._getVisibleItems().length - 1);
					iSliderHeight = $Slider.height();
					//bottom margin allows the bottom of the last item when scrolled down
					//to be aligned with the selection frame - one item offset
					this._marginBottom = iSliderHeight - this._marginTop - iItemHeightInPx;
					if (this._marginBottom < 0) { //android native
						this._marginBottom = iListContainerHeight - this._marginTop - iItemHeightInPx;
					}

					//update top,bottom margins
					$List.css("margin-top", this._marginTop);
					//bottom margin leaves
					$List.css("margin-bottom", this._marginBottom);
				} else {
					this._marginBottom = iListContainerHeight - iItemHeightInPx;
					$List.css("margin-top", 0);
					$List.css("margin-bottom", this._marginBottom);
					//increase the bottom margin so the list can scroll to its last value
				}

				this._selectionOffset = 0;
			}

			if (!this.getIsExpanded()) {
				this._selectionOffset = 0;
			}

			//WAI-ARIA region
			this.$().attr('aria-expanded', this.getIsExpanded());

			this.setSelectedValue(sSelectedValue);
		};

		/**
		 * Handles the cycle effect of the slider's list items.
		 *
		 * @param {number} iContainerHeight Height of the slider container
		 * @param {number} iContentHeight Height of the slider content
		 * @param {number} iTop Current top position
		 * @param {number} fDragMargin Remaining scroll limit
		 * @param {number} iContentRepeatNumber Content repetition counter
		 * @returns {number} Newly calculated top position
		 * @private
		 */
		TimePickerSlider.prototype._getUpdatedCycleScrollTop = function(iContainerHeight, iContentHeight, iTop, fDragMargin, iContentRepeatNumber) {
			var fContentHeight = iContentHeight - iTop - iContainerHeight;

			while (fContentHeight < fDragMargin) {
				iTop = iTop - iContentHeight / iContentRepeatNumber;
				fContentHeight = iContentHeight - iTop - iContainerHeight;
			}

			//they are not exclusive, we depend on a content long enough
			while (iTop < fDragMargin) {
				iTop = iTop + iContentHeight / iContentRepeatNumber;
			}

			return iTop;
		};

		/**
		 * Calculates the index of the snapped element and selects it.
		 *
		 * @param {number} iCurrentItem Index of the selected item
		 * @private
		 */
		TimePickerSlider.prototype._scrollerSnapped = function(iCurrentItem) {
			var iSelectedItemIndex = iCurrentItem,
				iItemsCount = this._getVisibleItems().length,
				sNewValue;

			while (iSelectedItemIndex >= iItemsCount) {
				iSelectedItemIndex = iSelectedItemIndex - iItemsCount;
			}

			if (!this.getIsCyclic()) {
				iSelectedItemIndex = iCurrentItem;
			}

			sNewValue = this._getVisibleItems()[iSelectedItemIndex].getKey();

			this.setSelectedValue(sNewValue);
			this._fireSelectedValueChange(sNewValue);
		};

		/**
		 * Updates the scrolltop value to be on the center of the slider.
		 *
		 * @private
		 */
		TimePickerSlider.prototype._updateScroll = function() {
			var sSelectedValue = this.getSelectedValue();
			if (sSelectedValue !== this._getVisibleItems()[0].getKey()
				&& this._getSliderContainerDomRef().scrollTop() + (this._selectionOffset ? this._selectionOffset : 0) === 0) {
				this.setSelectedValue(sSelectedValue);
				this._fireSelectedValueChange(sSelectedValue);
			}
		};

		/**
		 * Adds CSS class to the selected slider item.
		 *
		 * @private
		 */
		TimePickerSlider.prototype._addSelectionStyle = function() {
			var $aItems = this.$("content").find("li:not(.TPSliderItemHidden)"),
				sSelectedItemText = $aItems.eq(this._iSelectedItemIndex).text(),
				oDescriptionElement,
				sAriaLabel;

			if (!sSelectedItemText) {
				return;
			}

			sAriaLabel = sSelectedItemText;
			if (sAriaLabel && sAriaLabel.length > 1 && sAriaLabel.indexOf('0') === 0) {
				//If the label contains digits (hours, minutes, seconds), we must remove any leading zeros because they
				//are invalid in the context of what will be read out by the screen readers.
				//Values like AM/PM are not changed.
				sAriaLabel = sAriaLabel.substring(1);
			}

			$aItems.eq(this._iSelectedItemIndex).addClass("sapMTimePickerItemSelected");
			//WAI-ARIA region
			oDescriptionElement = this.getDomRef("valDescription");
			if (oDescriptionElement.innerHTML !== sAriaLabel) {
				oDescriptionElement.innerHTML = sAriaLabel;
			}
		};

		/**
		 * Removes CSS class to the selected slider item.
		 *
		 * @private
		 */
		TimePickerSlider.prototype._removeSelectionStyle = function() {
			var $aItems = this.$("content").find("li:not(.TPSliderItemHidden)");
			$aItems.eq(this._iSelectedItemIndex).removeClass("sapMTimePickerItemSelected");
		};

		/**
		 * Attaches all needed events to the slider.
		 *
		 * @private
		 */
		TimePickerSlider.prototype._attachEvents = function () {
			var oElement = this._getSliderContainerDomRef()[0];

			if (Device.system.combi) { // we need both mouse and touch events
				//Attach touch events
				oElement.addEventListener("touchstart", jQuery.proxy(onTouchStart, this), false);
				oElement.addEventListener("touchmove", jQuery.proxy(onTouchMove, this), false);
				document.addEventListener("touchend", jQuery.proxy(onTouchEnd, this), false);
				//Attach mouse events
				oElement.addEventListener("mousedown", jQuery.proxy(onTouchStart, this), false);
				document.addEventListener("mousemove", jQuery.proxy(onTouchMove, this), false);
				document.addEventListener("mouseup", jQuery.proxy(onTouchEnd, this), false);
			} else {
				if (Device.system.phone || Device.system.tablet) {
					//Attach touch events
					oElement.addEventListener("touchstart", jQuery.proxy(onTouchStart, this), false);
					oElement.addEventListener("touchmove", jQuery.proxy(onTouchMove, this), false);
					document.addEventListener("touchend", jQuery.proxy(onTouchEnd, this), false);
				} else {
					//Attach mouse events
					oElement.addEventListener("mousedown", jQuery.proxy(onTouchStart, this), false);
					document.addEventListener("mousemove", jQuery.proxy(onTouchMove, this), false);
					document.addEventListener("mouseup", jQuery.proxy(onTouchEnd, this), false);
				}
			}
		};

		/**
		 * Detaches all attached events to the slider.
		 *
		 * @private
		 */
		TimePickerSlider.prototype._detachEvents = function () {
			var oElement = this._getSliderContainerDomRef()[0];
			if ( oElement == null ) {
				return;
			}
			if (Device.system.combi) {
				//Detach touch events
				oElement.removeEventListener("touchstart", jQuery.proxy(onTouchStart, this), false);
				oElement.removeEventListener("touchmove", jQuery.proxy(onTouchMove, this), false);
				document.removeEventListener("touchend", jQuery.proxy(onTouchEnd, this), false);
				//Detach mouse events
				oElement.removeEventListener("mousedown", jQuery.proxy(onTouchStart, this), false);
				document.removeEventListener("mousemove", jQuery.proxy(onTouchMove, this), false);
				document.removeEventListener("mouseup", jQuery.proxy(onTouchEnd, this), false);
			} else {
				if (Device.system.phone || Device.system.tablet) {
					//Detach touch events
					oElement.removeEventListener("touchstart", jQuery.proxy(onTouchStart, this), false);
					oElement.removeEventListener("touchmove", jQuery.proxy(onTouchMove, this), false);
					document.removeEventListener("touchend", jQuery.proxy(onTouchEnd, this), false);
				} else {
					//Detach mouse events
					oElement.removeEventListener("mousedown", jQuery.proxy(onTouchStart, this), false);
					document.removeEventListener("mousemove", jQuery.proxy(onTouchMove, this), false);
					document.removeEventListener("mouseup", jQuery.proxy(onTouchEnd, this), false);
				}
			}
		};

		/**
		 * Helper function which enables selecting a slider item with an index offset.
		 *
		 * @param {number} iIndexOffset The index offset to be scrolled to
		 * @private
		 */
		TimePickerSlider.prototype._offsetValue = function(iIndexOffset) {
			var $Slider = this._getSliderContainerDomRef(),
				iScrollTop,
				iItemHeight = this._getItemHeightInPx(),
				iSnapScrollTop,
				iSelIndex,
				bCycle = this.getIsCyclic(),
				oThat = this;

			this._stopAnimation(); //stop any schedule(interval) for animation
			//stop snap animation also
			if (this._animatingSnap === true) {
				this._animatingSnap = false;
				this._getSliderContainerDomRef().stop(true);
				//Be careful not to invoke this method twice (the first time is on animate finish callback).
				//If this is the first animation, the _iSelectedIndex will remain its initial value, so no need
				//to notify the scroller about any snap completion
				if (this._animatingTargetIndex !== null) {
					this._scrollerSnapped(this._animatingTargetIndex);
					this._animatingTargetIndex = null;
				} else if (this._iSelectedIndex !== -1) {
					this._scrollerSnapped(this._iSelectedIndex);
				}
			}

			iSelIndex = this._iSelectedItemIndex + iIndexOffset;
			iScrollTop = $Slider.scrollTop();
			iSnapScrollTop = iScrollTop + iIndexOffset * iItemHeight;

			if (!bCycle) {
				if (iSelIndex < 0 || iSelIndex >= this._getVisibleItems().length) {
					return;
				}

				if (iSnapScrollTop > this._maxScrollTop) {
					iSnapScrollTop = this._maxScrollTop;
				}

				if (iSnapScrollTop < this._minScrollTop) {
					iSnapScrollTop = this._minScrollTop;
				}
			}

			this._animatingSnap = true;
			this._animatingTargetIndex = iSelIndex;
			$Slider.animate({ scrollTop: iSnapScrollTop}, SCROLL_ANIMATION_DURATION, 'linear', function() {
				$Slider.clearQueue();
				oThat._animatingSnap = false;
				oThat._animatingTargetIndex = null;
				//make sure the DOM is still visible
				if ($Slider.css("visibility") === "visible") {
					oThat._scrollerSnapped(iSelIndex);
				}
			});
		};

		/**
		 * Repositions the slider to match the current item plus or minus the given integer offset.
		 *
		 * @param {number} iOffsetNumberOfItems The number of items to be added or removed to the current item's index
		 * @private
		 */
		TimePickerSlider.prototype._offsetSlider = function(iOffsetNumberOfItems) {
			var iScrollTop = this._getSliderContainerDomRef().scrollTop(),
				that = this,
				$ContainerHeight = that._getSliderContainerDomRef().height(),
				$ContentHeight = that.$("content").height(),
				iDragMarginBuffer = 200,
				iDragMargin = $ContainerHeight + iDragMarginBuffer,
				iContentRepeat = that._getContentRepeat(),
				bCycle = that.getIsCyclic(),
				iItemHeight = that._getItemHeightInPx();

				//calculate the new scroll offset by subtracting the distance
				iScrollTop = iScrollTop - iOffsetNumberOfItems * iItemHeight;
				if (bCycle) {
					iScrollTop = that._getUpdatedCycleScrollTop($ContainerHeight, $ContentHeight, iScrollTop, iDragMargin, iContentRepeat);
				} else {
					if (iScrollTop > that._maxScrollTop) {
						iScrollTop = that._maxScrollTop;
					}

					if (iScrollTop < that._minScrollTop) {
						iScrollTop = that._minScrollTop;
					}
				}

				that._getSliderContainerDomRef().scrollTop(iScrollTop);
				that._iSelectedIndex = Math.round((iScrollTop + that._selectionOffset) / iItemHeight);
				that._scrollerSnapped(that._iSelectedIndex);
		};

		TimePickerSlider.prototype._initArrows = function() {
			var that = this, oArrowUp, oArrowDown,
				oRB = sap.ui.getCore().getLibraryResourceBundle("sap.m");

			oArrowUp = new Button({
				icon: IconPool.getIconURI("slim-arrow-up"),
				press: function (oEvent) {
					that._offsetValue(-1);
				},
				tooltip: oRB.getText("TIMEPICKER_TOOLTIP_UP"),
				type: 'Transparent'
			});
			oArrowUp.addEventDelegate({
				onAfterRendering: function () {
					oArrowUp.$().attr("tabindex", -1);
				}
			});

			this.setAggregation("_arrowUp", oArrowUp);

			oArrowDown = new Button({
				icon: IconPool.getIconURI("slim-arrow-down"),
				press: function (oEvent) {
					that._offsetValue(1);
				},
				tooltip: oRB.getText("TIMEPICKER_TOOLTIP_DOWN"),
				type: 'Transparent'
			});

			oArrowDown.addStyleClass("sapMTimePickerItemArrowDown");
			oArrowDown.addEventDelegate({
				onAfterRendering: function () {
					oArrowDown.$().attr("tabindex", -1);
				}
			});

			this.setAggregation("_arrowDown", oArrowDown);
		};

		TimePickerSlider.prototype._convertNumPadToNumKeyCode = function (iKeyCode) {
			var oKCs = KeyCodes;

			// Translate keypad code to number row code
			// The difference between NUM pad numbers and numbers in keycode is 48
			if (iKeyCode >= oKCs.NUMPAD_0 && iKeyCode <= oKCs.NUMPAD_9){
				iKeyCode -= 48;
			}

			return iKeyCode;
		};

		/**
		 * Finds the index of an element, satisfying provided predicate.
		 *
		 * @param {array} aArray The array to be predicted
		 * @param {function} fnPredicate Testing function
		 * @returns {number} The index in the array, if an element in the array satisfies the provided testing function
		 * @private
		 */
		function findIndexInArray(aArray, fnPredicate) {
			if (aArray == null) {
				throw new TypeError('findIndex called with null or undefined array');
			}
			if (typeof fnPredicate !== 'function') {
				throw new TypeError('predicate must be a function');
			}

			var iLength = aArray.length;
			var fnThisArg = arguments[1];
			var vValue;

			for (var iIndex = 0; iIndex < iLength; iIndex++) {
				vValue = aArray[iIndex];
				if (fnPredicate.call(fnThisArg, vValue, iIndex, aArray)) {
					return iIndex;
				}
			}
			return -1;
		}

		/**
		 * Default onTouchStart handler.
		 * @param {jQuery.Event} oEvent  Event object
		 */
		var onTouchStart = function (oEvent) {
			var iPageY = oEvent.touches && oEvent.touches.length ? oEvent.touches[0].pageY : oEvent.pageY;
			this._bIsDrag = false;

			if (!this.getIsExpanded()) {
				return;
			}

			this._stopAnimation();
			this._startDrag(iPageY);

			if (!Device.system.desktop) {
				oEvent.preventDefault();
			}
			this._mousedown = true;
		};

		/**
		 * Default onTouchMove handler.
		 * @param {jQuery.Event} oEvent  Event object
		 */
		var onTouchMove = function (oEvent) {
			var iPageY = oEvent.touches && oEvent.touches.length ? oEvent.touches[0].pageY : oEvent.pageY;

			if (!this._mousedown || !this.getIsExpanded()) {
				return;
			}

			//galaxy s5 android 5.0 fires touchmove every time - so see if it's far enough to call it a drag
			if (!this._bIsDrag && this._dragSession && this._dragSession.positions.length) {
				//there is a touch at least 5px away vertically from the initial touch
				var bFarEnough = this._dragSession.positions.some(function(pos) {
					return Math.abs(pos.pageY - iPageY) > 5;
				});

				if (bFarEnough) {
					this._bIsDrag = true;
				}
			}

			this._doDrag(iPageY, oEvent.timeStamp);

			this._mousedown = true;
		};

		/**
		 * Default onTouchEnd handler.
		 * @param {jQuery.Event} oEvent  Event object
		 */
		var onTouchEnd = function (oEvent) {
			var iPageY = oEvent.changedTouches && oEvent.changedTouches.length ? oEvent.changedTouches[0].pageY : oEvent.pageY;

			if (this._bIsDrag === false) {
				this.fireTap(oEvent);
				this._dragSession = null;
			}

			this._bIsDrag = true;

			if (!this.getIsExpanded()) {
				this._dragSession = null;
				return;
			}

			this._endDrag(iPageY, oEvent.timeStamp);

			this._mousedown = false;
		};

		var fnFindKeyByText = function(sText) {
			var aItems = this._getVisibleItems();

			var index = findIndexInArray(aItems, function(el) {
				return el.getText() === sText;
			});

			return aItems[index] ? aItems[index].getKey() : '';
		};

		/*
		 * Returns a function that remembers consecutive keydown events and adjust the slider values
		 * if they together match an item key.
		 */
		var fnTimedKeydownHelper = function() {
			var iLastTimeStamp = -1,
				iLastTimeoutId = -1,
				iWaitTimeout = 1000,
				sCurrentKeyPrefix = "",
				fnTimedKeydown = function(iTimeStamp, iKeyCode) {
					var aMatchingItems;
					//the previous call was more than a second ago or this is the first call
					if (iLastTimeStamp + iWaitTimeout < iTimeStamp) {
						sCurrentKeyPrefix = "";
					} else {
						if (iLastTimeoutId !== -1) {
							clearTimeout(iLastTimeoutId);
							iLastTimeoutId = -1;
						}
					}

					sCurrentKeyPrefix += String.fromCharCode(iKeyCode).toLowerCase();

					aMatchingItems = this._getVisibleItems().filter(function(item) {
						return item.getKey().indexOf(sCurrentKeyPrefix) === 0; //starts with the current prefix
					});

					if (aMatchingItems.length > 1) {
						iLastTimeoutId = setTimeout(function() {
							this.setSelectedValue(sCurrentKeyPrefix);
							sCurrentKeyPrefix = "";
							iLastTimeoutId = -1;
						}.bind(this), iWaitTimeout);
					} else if (aMatchingItems.length === 1) {
						this.setSelectedValue(aMatchingItems[0].getKey());
						sCurrentKeyPrefix = "";
					} else {
						sCurrentKeyPrefix = "";
					}

					iLastTimeStamp = iTimeStamp;
				};

			return fnTimedKeydown;
		};

		/**
		 * Gets only the visible items.
		 * @returns {sap.m.VisibleItem[]} the visible sap.m.TimePickerSlider items
		 * @private
		 */
		TimePickerSlider.prototype._getVisibleItems = function() {
			return this.getItems().filter(function(item) {
				return item.getVisible();
			});
		};

		/**
		 * Setter for enabling/disabling the sliders when 2400.
		 *
		 * @private
		 */
		TimePickerSlider.prototype._setEnabled = function (bEnabled) {
			this._bEnabled = bEnabled;

			if (bEnabled) {
				this.$().removeClass("sapMTPDisabled");
				this.$().attr("tabindex", 0);
			} else {
				this.$().addClass("sapMTPDisabled");
				this.$().attr("tabindex", -1);
			}

			return this;
		};

		/**
		 * Getter for enabling/disabling the sliders when 2400.
		 * @private
		 */
		TimePickerSlider.prototype._getEnabled = function (bEnabled) {
			return this._bEnabled;
		};

		/**
		 * Informs the <code>TimePickerSliders</code> for a change value.
		 * @param {string} sValue selected value
		 * @private
		 */
		TimePickerSlider.prototype._fireSelectedValueChange = function (sValue) {
			this.fireEvent("_selectedValueChange", { value: sValue });
		};

		return TimePickerSlider;

	});