/*!
 * JavaScript Custom Forms : Range Module
 *
 * Copyright 2014-2015 PSD2HTML - http://psd2html.com/jcf
 * Released under the MIT license (LICENSE.txt)
 *
 * Version: 1.2.0
 */
jcf.addModule(function($) {
	'use strict';

	return {
		name: 'Range',
		selector: 'input[type="range"]',
		options: {
			realElementClass: 'jcf-real-element',
			fakeStructure: '<span class="jcf-range"><span class="jcf-range-wrapper"><span class="jcf-range-track"><span class="jcf-range-handle"></span></span></span></span>',
			dataListMark: '<span class="jcf-range-mark"></span>',
			rangeDisplayWrapper: '<span class="jcf-range-display-wrapper"></span>',
			rangeDisplay: '<span class="jcf-range-display"></span>',
			handleSelector: '.jcf-range-handle',
			trackSelector: '.jcf-range-track',
			activeHandleClass: 'jcf-active-handle',
			verticalClass: 'jcf-vertical',
			orientation: 'horizontal',
			range: false, // or "min", "max", "all"
			dragHandleCenter: true,
			snapToMarks: true,
			snapRadius: 5,
			minRange: 0
		},
		matchElement: function(element) {
			return element.is(this.selector);
		},
		init: function() {
			this.initStructure();
			this.attachEvents();
			this.refresh();
		},
		initStructure: function() {
			this.page = $('html');
			this.realElement = $(this.options.element).addClass(this.options.hiddenClass);
			this.fakeElement = $(this.options.fakeStructure).insertBefore(this.realElement).prepend(this.realElement);
			this.track = this.fakeElement.find(this.options.trackSelector);
			this.trackHolder = this.track.parent();
			this.handle = this.fakeElement.find(this.options.handleSelector);
			this.createdHandleCount = 0;
			this.activeDragHandleIndex = 0;
			this.isMultiple = this.realElement.prop('multiple') || typeof this.realElement.attr('multiple') === 'string';
			this.values = this.isMultiple ? this.realElement.attr('value').split(',') : [this.realElement.val()];
			this.handleCount = this.isMultiple ? this.values.length : 1;

			// create range display
			this.rangeDisplayWrapper = $(this.options.rangeDisplayWrapper).insertBefore(this.track);
			if (this.options.range === 'min' || this.options.range === 'all') {
				this.rangeMin = $(this.options.rangeDisplay).addClass('jcf-range-min').prependTo(this.rangeDisplayWrapper);
			}
			if (this.options.range === 'max' || this.options.range === 'all') {
				this.rangeMax = $(this.options.rangeDisplay).addClass('jcf-range-max').prependTo(this.rangeDisplayWrapper);
			}

			// clone handles if needed
			while (this.createdHandleCount < this.handleCount) {
				this.createdHandleCount++;
				this.handle.clone().addClass('jcf-index-' + this.createdHandleCount).insertBefore(this.handle);

				// create mid ranges
				if (this.createdHandleCount > 1) {
					if (!this.rangeMid) {
						this.rangeMid = $();
					}
					this.rangeMid = this.rangeMid.add($(this.options.rangeDisplay).addClass('jcf-range-mid').prependTo(this.rangeDisplayWrapper));
				}
			}

			// grab all handles
			this.handle.detach();
			this.handle = null;
			this.handles = this.fakeElement.find(this.options.handleSelector);
			this.handles.eq(0).addClass(this.options.activeHandleClass);

			// handle orientation
			this.isVertical = (this.options.orientation === 'vertical');
			this.directionProperty = this.isVertical ? 'top' : 'left';
			this.offsetProperty = this.isVertical ? 'bottom' : 'left';
			this.eventProperty = this.isVertical ? 'pageY' : 'pageX';
			this.sizeProperty = this.isVertical ? 'height' : 'width';
			this.sizeMethod = this.isVertical ? 'innerHeight' : 'innerWidth';
			this.fakeElement.css('touchAction', this.isVertical ? 'pan-x' : 'pan-y');
			if (this.isVertical) {
				this.fakeElement.addClass(this.options.verticalClass);
			}

			// set initial values
			this.minValue = parseFloat(this.realElement.attr('min'));
			this.maxValue = parseFloat(this.realElement.attr('max'));
			this.stepValue = parseFloat(this.realElement.attr('step')) || 1;

			// check attribute values
			this.minValue = isNaN(this.minValue) ? 0 : this.minValue;
			this.maxValue = isNaN(this.maxValue) ? 100 : this.maxValue;

			// handle range
			if (this.stepValue !== 1) {
				this.maxValue -= (this.maxValue - this.minValue) % this.stepValue;
			}
			this.stepsCount = (this.maxValue - this.minValue) / this.stepValue;
			this.createDataList();
		},
		attachEvents: function() {
			this.realElement.on({
				focus: this.onFocus
			});
			this.trackHolder.on('jcf-pointerdown', this.onTrackPress);
			this.handles.on('jcf-pointerdown', this.onHandlePress);
		},
		createDataList: function() {
			var self = this,
				dataValues = [],
				dataListId = this.realElement.attr('list');

			if (dataListId) {
				$('#' + dataListId).find('option').each(function() {
					var itemValue = parseFloat(this.value || this.innerHTML),
						mark, markOffset;

					if (!isNaN(itemValue)) {
						markOffset = self.valueToOffset(itemValue);
						dataValues.push({
							value: itemValue,
							offset: markOffset
						});
						mark = $(self.options.dataListMark).text(itemValue).attr({
							'data-mark-value': itemValue
						}).css(self.offsetProperty, markOffset + '%').appendTo(self.track);
					}
				});
				if (dataValues.length) {
					self.dataValues = dataValues;
				}
			}
		},
		getDragHandleRange: function(handleIndex) {
			// calculate range for slider with multiple handles
			var minStep = -Infinity,
				maxStep = Infinity;

			if (handleIndex > 0) {
				minStep = this.valueToStepIndex(parseFloat(this.values[handleIndex - 1]) + this.options.minRange);
			}
			if (handleIndex < this.handleCount - 1) {
				maxStep = this.valueToStepIndex(parseFloat(this.values[handleIndex + 1]) - this.options.minRange);
			}

			return {
				minStepIndex: minStep,
				maxStepIndex: maxStep
			};
		},
		getNearestHandle: function(percent) {
			// handle vertical sliders
			if (this.isVertical) {
				percent = 1 - percent;
			}

			// detect closest handle when track is pressed
			var closestHandle = this.handles.eq(0),
				closestDistance = Infinity,
				self = this;

			if (this.handleCount > 1) {
				this.handles.each(function() {
					var handleOffset = parseFloat(this.style[self.offsetProperty]) / 100,
						handleDistance = Math.abs(handleOffset - percent);

					if (handleDistance < closestDistance) {
						closestDistance = handleDistance;
						closestHandle = $(this);
					}
				});
			}
			return closestHandle;
		},
		onTrackPress: function(e) {
			var trackSize, trackOffset, innerOffset;

			e.preventDefault();
			if (!this.realElement.is(':disabled') && !this.activeDragHandle) {
				trackSize = this.track[this.sizeMethod]();
				trackOffset = this.track.offset()[this.directionProperty];
				this.activeDragHandle = this.getNearestHandle((e[this.eventProperty] - trackOffset) / this.trackHolder[this.sizeMethod]());
				this.activeDragHandleIndex = this.handles.index(this.activeDragHandle);
				this.handles.removeClass(this.options.activeHandleClass).eq(this.activeDragHandleIndex).addClass(this.options.activeHandleClass);
				innerOffset = this.activeDragHandle[this.sizeMethod]() / 2;

				this.dragData = {
					trackSize: trackSize,
					innerOffset: innerOffset,
					trackOffset: trackOffset,
					min: trackOffset,
					max: trackOffset + trackSize
				};
				this.page.on({
					'jcf-pointermove': this.onHandleMove,
					'jcf-pointerup': this.onHandleRelease
				});

				if (e.pointerType === 'mouse') {
					this.realElement.focus();
				}

				this.onHandleMove(e);
			}
		},
		onHandlePress: function(e) {
			var trackSize, trackOffset, innerOffset;

			e.preventDefault();
			if (!this.realElement.is(':disabled') && !this.activeDragHandle) {
				this.activeDragHandle = $(e.currentTarget);
				this.activeDragHandleIndex = this.handles.index(this.activeDragHandle);
				this.handles.removeClass(this.options.activeHandleClass).eq(this.activeDragHandleIndex).addClass(this.options.activeHandleClass);
				trackSize = this.track[this.sizeMethod]();
				trackOffset = this.track.offset()[this.directionProperty];
				innerOffset = this.options.dragHandleCenter ? this.activeDragHandle[this.sizeMethod]() / 2 : e[this.eventProperty] - this.handle.offset()[this.directionProperty];

				this.dragData = {
					trackSize: trackSize,
					innerOffset: innerOffset,
					trackOffset: trackOffset,
					min: trackOffset,
					max: trackOffset + trackSize
				};
				this.page.on({
					'jcf-pointermove': this.onHandleMove,
					'jcf-pointerup': this.onHandleRelease
				});

				if (e.pointerType === 'mouse') {
					this.realElement.focus();
				}
			}
		},
		onHandleMove: function(e) {
			var self = this,
				newOffset, dragPercent, stepIndex, valuePercent, handleDragRange;

			// calculate offset
			if (this.isVertical) {
				newOffset = this.dragData.max + (this.dragData.min - e[this.eventProperty]) - this.dragData.innerOffset;
			} else {
				newOffset = e[this.eventProperty] - this.dragData.innerOffset;
			}

			// fit in range
			if (newOffset < this.dragData.min) {
				newOffset = this.dragData.min;
			} else if (newOffset > this.dragData.max) {
				newOffset = this.dragData.max;
			}

			e.preventDefault();
			if (this.options.snapToMarks && this.dataValues) {
				// snap handle to marks
				var dragOffset = newOffset - this.dragData.trackOffset;
				dragPercent = (newOffset - this.dragData.trackOffset) / this.dragData.trackSize * 100;

				$.each(this.dataValues, function(index, item) {
					var markOffset = item.offset / 100 * self.dragData.trackSize,
						markMin = markOffset - self.options.snapRadius,
						markMax = markOffset + self.options.snapRadius;

					if (dragOffset >= markMin && dragOffset <= markMax) {
						dragPercent = item.offset;
						return false;
					}
				});
			} else {
				// snap handle to steps
				dragPercent = (newOffset - this.dragData.trackOffset) / this.dragData.trackSize * 100;
			}

			// move handle only in range
			stepIndex = Math.round(dragPercent * this.stepsCount / 100);
			if (this.handleCount > 1) {
				handleDragRange = this.getDragHandleRange(this.activeDragHandleIndex);
				if (stepIndex < handleDragRange.minStepIndex) {
					stepIndex = Math.max(handleDragRange.minStepIndex, stepIndex);
				} else if (stepIndex > handleDragRange.maxStepIndex) {
					stepIndex = Math.min(handleDragRange.maxStepIndex, stepIndex);
				}
			}
			valuePercent = stepIndex * (100 / this.stepsCount);

			if (this.dragData.stepIndex !== stepIndex) {
				this.dragData.stepIndex = stepIndex;
				this.dragData.offset = valuePercent;
				this.activeDragHandle.css(this.offsetProperty, this.dragData.offset + '%');

				// update value(s) and trigger "input" event
				this.values[this.activeDragHandleIndex] = '' + this.stepIndexToValue(stepIndex);
				this.updateValues();
				this.realElement.trigger('input');
			}
		},
		onHandleRelease: function() {
			var newValue;
			if (typeof this.dragData.offset === 'number') {
				newValue = this.stepIndexToValue(this.dragData.stepIndex);
				this.realElement.val(newValue).trigger('change');
			}

			this.page.off({
				'jcf-pointermove': this.onHandleMove,
				'jcf-pointerup': this.onHandleRelease
			});
			delete this.activeDragHandle;
			delete this.dragData;
		},
		onFocus: function() {
			if (!this.fakeElement.hasClass(this.options.focusClass)) {
				this.fakeElement.addClass(this.options.focusClass);
				this.realElement.on({
					blur: this.onBlur,
					keydown: this.onKeyPress
				});
			}
		},
		onBlur: function() {
			this.fakeElement.removeClass(this.options.focusClass);
			this.realElement.off({
				blur: this.onBlur,
				keydown: this.onKeyPress
			});
		},
		onKeyPress: function(e) {
			var incValue = (e.which === 38 || e.which === 39),
				decValue = (e.which === 37 || e.which === 40);

			// handle TAB key in slider with multiple handles
			if (e.which === 9 && this.handleCount > 1) {
				if (e.shiftKey && this.activeDragHandleIndex > 0) {
					this.activeDragHandleIndex--;
				} else if (!e.shiftKey && this.activeDragHandleIndex < this.handleCount - 1) {
					this.activeDragHandleIndex++;
				} else {
					return;
				}
				e.preventDefault();
				this.handles.removeClass(this.options.activeHandleClass).eq(this.activeDragHandleIndex).addClass(this.options.activeHandleClass);
			}

			// handle cursor keys
			if (decValue || incValue) {
				e.preventDefault();
				this.step(incValue ? this.stepValue : -this.stepValue);
			}
		},
		updateValues: function() {
			var value = this.values.join(',');
			if (this.values.length > 1) {
				this.realElement.prop('valueLow', this.values[0]);
				this.realElement.prop('valueHigh', this.values[this.values.length - 1]);
				this.realElement.val(value);

				// if browser does not accept multiple values set only one
				if (this.realElement.val() !== value) {
					this.realElement.val(this.values[this.values.length - 1]);
				}
			} else {
				this.realElement.val(value);
			}

			this.updateRanges();
		},
		updateRanges: function() {
			// update display ranges
			var self = this,
				handle;

			if (this.rangeMin) {
				handle = this.handles[0];
				this.rangeMin.css(this.offsetProperty, 0).css(this.sizeProperty, handle.style[this.offsetProperty]);
			}
			if (this.rangeMax) {
				handle = this.handles[this.handles.length - 1];
				this.rangeMax.css(this.offsetProperty, handle.style[this.offsetProperty]).css(this.sizeProperty, 100 - parseFloat(handle.style[this.offsetProperty]) + '%');
			}
			if (this.rangeMid) {
				this.handles.each(function(index, curHandle) {
					var prevHandle, midBox;
					if (index > 0) {
						prevHandle = self.handles[index - 1];
						midBox = self.rangeMid[index - 1];
						midBox.style[self.offsetProperty] = prevHandle.style[self.offsetProperty];
						midBox.style[self.sizeProperty] = parseFloat(curHandle.style[self.offsetProperty]) - parseFloat(prevHandle.style[self.offsetProperty]) + '%';
					}
				});
			}
		},
		step: function(changeValue) {
			var originalValue = parseFloat(this.values[this.activeDragHandleIndex || 0]),
				newValue = originalValue,
				minValue = this.minValue,
				maxValue = this.maxValue;

			if (isNaN(originalValue)) {
				newValue = 0;
			}

			newValue += changeValue;

			if (this.handleCount > 1) {
				if (this.activeDragHandleIndex > 0) {
					minValue = parseFloat(this.values[this.activeDragHandleIndex - 1]) + this.options.minRange;
				}
				if (this.activeDragHandleIndex < this.handleCount - 1) {
					maxValue = parseFloat(this.values[this.activeDragHandleIndex + 1]) - this.options.minRange;
				}
			}

			if (newValue > maxValue) {
				newValue = maxValue;
			} else if (newValue < minValue) {
				newValue = minValue;
			}

			if (newValue !== originalValue) {
				this.values[this.activeDragHandleIndex || 0] = '' + newValue;
				this.updateValues();
				this.realElement.trigger('input').trigger('change');
				this.setSliderValue(this.values);
			}
		},
		valueToStepIndex: function(value) {
			return (value - this.minValue) / this.stepValue;
		},
		stepIndexToValue: function(stepIndex) {
			return this.minValue + this.stepValue * stepIndex;
		},
		valueToOffset: function(value) {
			var range = this.maxValue - this.minValue,
				percent = (value - this.minValue) / range;

			return percent * 100;
		},
		getSliderValue: function() {
			return $.map(this.values, function(value) {
				return parseFloat(value) || 0;
			});
		},
		setSliderValue: function(values) {
			// set handle position accordion according to value
			var self = this;
			this.handles.each(function(index, handle) {
				handle.style[self.offsetProperty] = self.valueToOffset(values[index]) + '%';
			});
		},
		refresh: function() {
			// handle disabled state
			var isDisabled = this.realElement.is(':disabled');
			this.fakeElement.toggleClass(this.options.disabledClass, isDisabled);

			// refresh handle position according to current value
			this.setSliderValue(this.getSliderValue());
			this.updateRanges();
		},
		destroy: function() {
			this.realElement.removeClass(this.options.hiddenClass).insertBefore(this.fakeElement);
			this.fakeElement.remove();

			this.realElement.off({
				keydown: this.onKeyPress,
				focus: this.onFocus,
				blur: this.onBlur
			});
		}
	};
});
