/*!
 *jQuery UI Spinner 1.23
 *
 *Copyright (c) 2009-2010 Brant Burnett
 *Copyright (c) 2010 Johannes Geppert http://www.jgeppert.com
 *Dual licensed under the MIT or GPL Version 2 licenses.
 *
 *  Depends:
 *	jquery.ui.core.js
 *	jquery.ui.widget.js
 *	jquery.ui.mouse.js
 *	jquery.ui.position.js
 */
(function($, undefined) {

var
	// constants
	active = "ui-state-active",
	hover = "ui-state-hover",
	disabled = "ui-state-disabled",

	keyCode = $.ui.keyCode,
	up = keyCode.UP,
	down = keyCode.DOWN,
	right = keyCode.RIGHT,
	left = keyCode.LEFT,
	pageUp = keyCode.PAGE_UP,
	pageDown = keyCode.PAGE_DOWN,
	home = keyCode.HOME,
	end = keyCode.END,

	msie = $.browser.msie,
	mouseWheelEventName = $.browser.mozilla ? "DOMMouseScroll" : "mousewheel",

	// namespace for events on input
	eventNamespace = ".uispinner",

	// only these special keys will be accepted, all others will be ignored unless CTRL or ALT are pressed
	validKeys = [up, down, right, left, pageUp, pageDown, home, end, keyCode.BACKSPACE, keyCode.DELETE, keyCode.TAB],

	// stores the currently focused spinner
	// Note: due to oddities in the focus/blur events, this is part of a two-part system for confirming focus
	// this must set to the control, and the focus variable must be true
	// this is because hitting up/down arrows with mouse causes focus to change, but blur event for previous control doesn't fire
	focusCtrl;

$.widget( "ui.spinner" , {
	options: {
		min: null,
		max: null,
		allowNull: false,

		group: "" ,
		point: "." ,
		prefix: "" ,
		suffix: "" ,
		places: null, // null causes it to detect the number of places in step

		defaultStep: 1, // real value is "step" , and should be passed as such.  This value is used to detect if passed value should override HTML5 attribute
		largeStep: 10,
		mouseWheel: true,
		increment: "slow" ,
		className: null,
		showOn: "always" ,
		width: 16,
		upIconClass: "ui-icon-triangle-1-n" ,
		downIconClass: "ui-icon-triangle-1-s" ,

		format: function(num, places) {
			var options = this,
				regex = /(\d+)(\d{3})/,
				result = ((isNaN(num) ? 0 : Math.abs(num)).toFixed(places)) + "";

			for (result = result.replace( "." , options.point); regex.test(result) && options.group; result=result.replace(regex, "$1"+options.group+"$2" )) {}
			return (num < 0 ? "-" : "" ) + options.prefix + result + options.suffix;
		},

		parse: function(val) {
			var options = this;

			if (options.group == "." ) {
				val = val.replace( "." , "" );
			 }
			if (options.point != "." ) {
				val = val.replace(options.point, "." );
			 }
			return parseFloat(val.replace(/[^0-9\-\.]/g, "" ));
		}
	},

	// * Widget fields *
	// curvalue - current value
	// places - currently effective number of decimal places
	// oWidth - original input width (used for destroy)
	// oMargin - original input right margin (used for destroy)
	// counter - number of spins at the current spin speed
	// incCounter - index within options.increment of the current spin speed
	// selfChange - indicates that change event is being fired by the widget, so don't reprocess input value
	// inputMaxLength - initial maxLength value on the input
	// focused - this spinner currently has the focus

	_create: function() {
		// shortcuts
		var self = this,
			input = self.element,
			type = input.attr( "type" );

		if (!input.is( "input" ) || ((type != "text" ) && (type != "number" ))) {
			console.error( "Invalid target for ui.spinner" );
			return;
		}

		self._procOptions(true);
		self._createButtons(input);

		if (!input.is( ":enabled" )) {
			self.disable();
		}
	},

	_createButtons: function(input) {
		function getMargin(margin) {
			// IE8 returns auto if no margin specified
			return margin == "auto" ? 0 : parseInt(margin, 10);
		}

		var self = this,
			options = self.options,
			className = options.className,
			buttonWidth = options.width,
			showOn = options.showOn,
			box = $.support.boxModel,
			height = input.outerHeight(),
			rightMargin = self.oMargin = getMargin(input.css( "margin-right" )), // store original width and right margin for later destroy
			wrapper = self.wrapper = input.css({ width: (self.oWidth = (box ? input.width() : input.outerWidth())) - buttonWidth,
												 marginRight: 0, textAlign: "right" })
				.after( "<span class='ui-spinner ui-widget' style='top:0;left:0;float:left;'></span>" ).next(),
			btnContainer = self.btnContainer = $(
				"<div class='ui-spinner-buttons'>" +
					"<div class='ui-spinner-up ui-spinner-button ui-state-default ui-corner-tr'><span class='ui-icon "+options.upIconClass+"'>&nbsp;</span></div>" +
					"<div class='ui-spinner-down ui-spinner-button ui-state-default ui-corner-br'><span class='ui-icon "+options.downIconClass+"'>&nbsp;</span></div>" +
				"</div>" ),

			// object shortcuts
			upButton, downButton, buttons, icons,

			hoverDelay,
			hoverDelayCallback,

			// current state booleans
			hovered, inKeyDown, inSpecialKey, inMouseDown,

			// used to reverse left/right key directions
			rtl = input[0].dir == "rtl";

		// apply className before doing any calculations because it could affect them
		if (className) { wrapper.addClass(className); }

		wrapper.append(btnContainer.css({ height: height, left: -buttonWidth-rightMargin,
			// use offset calculation to fix vertical position in Firefox
			top: (input.offset().top - wrapper.offset().top) + "px" }));

		buttons = self.buttons = btnContainer.find( ".ui-spinner-button" );
		buttons.css({ width: buttonWidth - (box ? buttons.outerWidth() - buttons.width() : 0), height: height/2 - (box ? buttons.outerHeight() - buttons.height() : 0) });
		upButton = buttons[0];
		downButton = buttons[1];

		// fix icon centering
		icons = buttons.find( ".ui-icon" );
		icons.css({ marginLeft: (buttons.innerWidth() - icons.width()) / 2, marginTop:  (buttons.innerHeight() - icons.height()) / 2 });

		// set width of btnContainer to be the same as the buttons
		btnContainer.width(buttons.outerWidth());
		if (showOn != "always" ) {
			btnContainer.css( "opacity" , 0);
		}

		input.addClass( "ui-spinner-input" ).attr({role: "textbox"});

		// use position to place buttons on the right side from input element
		wrapper.position({
			of: this.element,
			my: "left center" ,
			at: "right center" ,
			offset: "0 0" ,
			collision: "flip flip"
		});

		/* Event Bindings */

		// bind hover events to show/hide buttons
		if (showOn == "hover" || showOn == "both" ) {
			buttons.add(input)
				.bind( "mouseenter" + eventNamespace, function() {
					setHoverDelay(function() {
						hovered = true;
						if (!self.focused || (showOn == "hover" )) { // ignore focus flag if show on hover only
							self.showButtons();
						}
					});
				})

				.bind( "mouseleave" + eventNamespace, function hoverOut() {
					setHoverDelay(function() {
						hovered = false;
						if (!self.focused || (showOn == "hover" )) {  // ignore focus flag if show on hover only
							self.hideButtons();
						}
					});
				});
		}


		function mouseDown() {
			if (!options.disabled) {
				var input = self.element[0],
					dir = (this === upButton ? 1 : -1);

				input.focus();
				input.select();
				$(this).addClass(active);

				inMouseDown = true;
				self._startSpin(dir);
			}

			return false;
		}

		function mouseUp() {
			if (inMouseDown) {
				$(this).removeClass(active);
				self._stopSpin();
				inMouseDown = false;
			}
			return false;
		}

		buttons.hover(function() {
					// ensure that both buttons have hover removed, sometimes they get left on
					self.buttons.removeClass(hover);

					if (!options.disabled) { $(this).addClass(hover); }
				}, function() {
					$(this).removeClass(hover);
				})
			.mousedown(mouseDown)
			.mouseup(mouseUp)
			.mouseout(mouseUp);

		if (msie) {
			// fixes dbl click not firing second mouse down in IE
			buttons.dblclick(function() {
					if (!options.disabled) {
						// make sure any changes are posted
						self._change();
						self._doSpin((this === upButton ? 1 : -1) * options.step);
					}

					return false;
				})

				// fixes IE8 dbl click selection highlight
				.bind( "selectstart" , function() {return false;});
		}

		input.bind( "keydown" + eventNamespace, function(e) {
					var dir, large, limit,
						keyCode = e.keyCode; // shortcut for minimization
					if (e.ctrl || e.alt) { return true; } // ignore these events

					if (isSpecialKey(keyCode)) { inSpecialKey = true; }

					if (inKeyDown) { return false; } // only one direction at a time, and suppress invalid keys

					switch (keyCode) {
						case up:
						case pageUp:
							dir = 1;
							large = keyCode == pageUp;
							break;

						case down:
						case pageDown:
							dir = -1;
							large = keyCode == pageDown;
							break;

						case right:
						case left:
							dir = (keyCode == right) ^ rtl ? 1 : -1;
							break;

						case home:
							limit = self.options.min;
							if (limit !== null) { self._setValue(limit); }
							return false;

						case end:
							limit = self.options.max;
							limit = self.options.max;
							if (limit !== null) { self._setValue(limit); }
							return false;
					}

					if (dir) { // only process if dir was set above
						if (!inKeyDown && !options.disabled) {
							keyDir = dir;

							$(dir > 0 ? upButton : downButton).addClass(active);
							inKeyDown = true;
							self._startSpin(dir, large);
						}

						return false;
					}
				})

			.bind( "keyup" + eventNamespace, function(e) {
					if (e.ctrl || e.alt) { return true; } // ignore these events

					if (isSpecialKey(keyCode)) { inSpecialKey = false; }

					switch (e.keyCode) {
						case up:
						case right:
						case pageUp:
						case down:
						case left:
						case pageDown:
							buttons.removeClass(active);
							self._stopSpin();
							inKeyDown = false;
							return false;
					}
				})

			.bind( "keypress" + eventNamespace, function(e) {
					if (invalidKey(e.keyCode, e.charCode)) { return false; }
				})

			.bind( "change" + eventNamespace, function() { self._change(); })

			.bind( "focus" + eventNamespace, function() {
					function selectAll() {
						self.element.select();
					}

					// add delay for Chrome, but breaks IE8
					if(msie) { selectAll(); }
					else { setTimeout(selectAll, 0); }

					self.focused = true;
					focusCtrl = self;
					if (!hovered && (showOn == "focus" || showOn == "both" )) { // hovered will only be set if hover affects show
						self.showButtons();
					}
				})

			.bind( "blur" + eventNamespace, function() {
					self.focused = false;
					if (!hovered && (showOn == "focus" || showOn == "both" )) { // hovered will only be set if hover affects show
						self.hideButtons();
					}
				});

		function isSpecialKey(keyCode) {
			for (var i=0; i<validKeys.length; i++) { // predefined list of special keys
				if (validKeys[i] == keyCode) { return true; }
			}
			return false;
		}

		function invalidKey(keyCode, charCode) {
			if (inSpecialKey) { return false; }

			var ch = String.fromCharCode(charCode || keyCode),
				options = self.options;

			if ((ch >= '0') && (ch <= '9') || (ch == '-')) { return false; }
			if (((self.places > 0) && (ch == options.point)) || (ch == options.group)) { return false; }

			return true;
		}

		// used to delay start of hover show/hide by 100 milliseconds
		function setHoverDelay(callback) {
			if (hoverDelay) {
				// don't do anything if trying to set the same callback again
				if (callback === hoverDelayCallback) { return; }

				clearTimeout(hoverDelay);
			}

			function execute() {
				hoverDelay = 0;
				callback();
			}

			hoverDelayCallback = callback;
			hoverDelay = setTimeout(execute, 100);

		}
	},

	_procOptions: function(init) {
		var self = this,
			input = self.element,
			options = self.options,
			min = options.min,
			max = options.max,
			step = options.step,
			places = options.places,
			maxlength = -1, temp;

		// setup increment based on speed string
		if (options.increment == "slow" ) {
			options.increment = [{count: 1, mult: 1, delay: 250},
								 {count: 3, mult: 1, delay: 100},
								 {count: 0, mult: 1, delay: 50}];
		}
		else if (options.increment == "fast" ) {
			options.increment = [{count: 1, mult: 1, delay: 250},
								 {count: 19, mult: 1, delay: 100},
								 {count: 80, mult: 1, delay: 20},
								 {count: 100, mult: 10, delay: 20},
								 {count: 0, mult: 100, delay: 20}];
		}
		if ((min === null) && ((temp = input.attr( "min" )) !== null)) {
			min = parseFloat(temp);
		}
		if ((max === null) && ((temp = input.attr( "max" )) !== null)) {
			max = parseFloat(temp);
		}
		if (!step && ((temp = input.attr( "step" )) !== null)) {
			if (temp != "any" ) {
				step = parseFloat(temp);
				options.largeStep *= step;
			}
		}
		options.step = step = step || options.defaultStep;

		// Process step for decimal places if none are specified
		if ((places === null) && ((temp = step + "" ).indexOf( "." ) != -1)) {
			places = temp.length - temp.indexOf( "." ) - 1;
		}
		self.places = places;

		if ((max !== null) && (min !== null)) {
			// ensure that min is less than or equal to max
			if (min > max) { min = max; }

			// set maxlength based on min/max
			maxlength = Math.max(Math.max(maxlength, options.format(max, places, input).length), options.format(min, places, input).length);
		}

		// only lookup input maxLength on init
		if (init) { self.inputMaxLength = input[0].maxLength; }
		temp = self.inputMaxLength;

		if (temp > 0) {
			maxlength = maxlength > 0 ? Math.min(temp, maxlength) : temp;
			temp = Math.pow(10, maxlength) - 1;
			if ((max === null) || (max > temp)) { max = temp; }
			temp = -(temp + 1) / 10 + 1;
			if ((min === null) || (min < temp)) { min = temp; }
		}

		if (maxlength > 0) { input.attr( "maxlength" , maxlength); }

		options.min = min;
		options.max = max;

		// ensures that current value meets constraints
		self._change();

		input.unbind(mouseWheelEventName + eventNamespace);
		if (options.mouseWheel) {
			input.bind(mouseWheelEventName + eventNamespace, self._mouseWheel);
		}
	},

	_mouseWheel: function(e) {
		var self = $.data(this, "spinner" );
		if (!self.options.disabled && self.focused && (focusCtrl === self)) {
			// make sure changes are posted
			self._change();
			self._doSpin(((e.wheelDelta || -e.detail) > 0 ? 1 : -1) * self.options.step);
			return false;
		}
	},

	// sets an interval to call the _spin function
	_setTimer: function(delay, dir, large) {
		function fire() {
			self._spin(dir, large);
		}

		var self = this;
		self._stopSpin();
		self.timer = setInterval(fire, delay);
	},

	// stops the spin timer
	_stopSpin: function() {
		if (this.timer) {
			clearInterval(this.timer);
			this.timer = 0;
		}
	},

	// performs first step, and starts the spin timer if increment is set
	_startSpin: function(dir, large) {
		// shortcuts
		var self = this,
			options = self.options,
			increment = options.increment;

		// make sure any changes are posted
		self._change();
		self._doSpin(dir * (large ? self.options.largeStep : self.options.step));

		if (increment && increment.length > 0) {
			self.counter = 0;
			self.incCounter = 0;
			self._setTimer(increment[0].delay, dir, large);
		}
	},

	// called by timer for each step in the spin
	_spin: function(dir, large) {
		// shortcuts
		var self = this,
			increment = self.options.increment,
			curIncrement = increment[self.incCounter];

		this._trigger( "beforeSpin" , event, { item: this.selectedItem } );
		self._doSpin(dir * curIncrement.mult * (large ? self.options.largeStep : self.options.step));
		self.counter++;
		this._trigger( "afterSpin" , event, { item: this.selectedItem } );

		if ((self.counter > curIncrement.count) && (self.incCounter < increment.length-1)) {
			self.counter = 0;
			curIncrement = increment[++self.incCounter];
			self._setTimer(curIncrement.delay, dir, large);
		}
	},

	// actually spins the timer by a step
	_doSpin: function(step) {
		// shortcut
		var self = this,
			value = self.curvalue;

		if (value === null) {
			value = (step > 0 ? self.options.min : self.options.max) || 0;
		}
		self._setValue(value + step);
	},

	// Parse the value currently in the field
	_parseValue: function() {
		var value = this.element.val();
		return value ? this.options.parse(value, this.element) : null;
	},

	_validate: function(value) {
		var options = this.options,
			min = options.min,
			max = options.max;

		if ((value === null) && !options.allowNull) {
			value = this.curvalue !== null ? this.curvalue : min || max || 0; // must confirm not null in case just initializing and had blank value
		}
		if ((max !== null) && (value > max)) {
			return max;
		}
		else if ((min !== null) && (value < min)) {
			return min;
		}
		else {
			return value;
		}
	},

	_change: function() {
		var self = this, // shortcut
			value = self._parseValue(),
			min = self.options.min,
			max = self.options.max;

		// don't reprocess if change was self triggered
		if (!self.selfChange) {
			if (isNaN(value)) {
				value = self.curvalue;
			}
			self._setValue(value, true);
		}
	},

	// overrides _setData to force option parsing
	_setOption: function(key, value) {
		$.Widget.prototype._setOption.call(this, key, value);
		this._procOptions();
	},

	increment: function() {
		this._doSpin(this.options.step);
	},

	decrement: function() {
		this._doSpin(-this.options.step);
	},

	showButtons: function(immediate) {
		var btnContainer = this.btnContainer.stop();
		if (immediate) {
			btnContainer.css( "opacity" , 1);
		}
		else {
			btnContainer.fadeTo( "fast" , 1);
		}
	},

	hideButtons: function(immediate) {
		var btnContainer = this.btnContainer.stop();
		if (immediate) {
			btnContainer.css( "opacity" , 0);
		}
		else {
			btnContainer.fadeTo( "fast" , 0);
		}
		this.buttons.removeClass(hover);
	},

	// Set the value directly
	_setValue: function(value, suppressFireEvent) {
		var self = this;

		self.curvalue = value = self._validate(value);
		self.element.val(value !== null ?
			self.options.format(value, self.places, self.element) :
			"" );

		if (!suppressFireEvent) {
			self.selfChange = true;
			self.element.change();
			self.selfChange = false;
		}
	},

	// Set or retrieve the value
	value: function(newValue) {
		if (arguments.length) {
			this._setValue(newValue);

			// maintains chaining
			return this.element;
		}

		return this.curvalue;
	},

	enable: function() {
		this.buttons.removeClass(disabled);
		this.element[0].disabled = false;
		$.Widget.prototype.enable.call(this);
	},

	disable: function() {
		this.buttons.addClass(disabled)
			// in case hover class got left on
			.removeClass(hover);

		this.element[0].disabled = true;
		$.Widget.prototype.disable.call(this);
	},

	destroy: function(target) {
		this.wrapper.remove();
		this.element.unbind(eventNamespace).css({ width: this.oWidth, marginRight: this.oMargin });

		$.Widget.prototype.destroy.call(this);
	}
});

})( jQuery );
