/*
 * Slider Nav - jQuery plugin for a navigation / product selection controlled by a slider device.
 *
 * Copyright (c) 2009 EMC Conchango
 *
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 *
 * Options:
 * 	sliderSpeed - speed that a slider will move if the left/right keys are held down - default 100ms
 * 	sliderPosition - location that the slider is inserted in the container element - default "top", also supports "bottom"
 *	sliderClass - CSS class to use when creating or getting the slider element - default "slider"
 *	controllerClass - CSS class to use when getting the set of navigation elements that relate to the slider - default "sliderControl"
 * 	controllerItemContainer - element selector for navigation items container - default "li"
 * 	controllerItemSelector - element selector for navigation items, useful if you want to target an internal element to attach events - default "li"
 * 	controllerBehaviour - the event type to apply to navigation items that will control the slider - default "hover", also supports "click"
 * 	wrapperClass - CSS class to use when constructing markup to hold a scrolling controller list - default "scrollContainer"
 *	usingClass - CSS class to apply to currently selected navigation item - default "active"
 *	remoteDisplay - element selector for optional extra display list controlled by slider - default null
 *  constrainScroll - boolean to select if a scrolling display should be bounded by the lowest/highest snap points rather than the whole slide - default true
 *	hideRadios - boolean to show/hide a related radio button input for each navigation item - default false
 *	showTrail - boolean to show/hide a "trail" element to the left of the slider bar - default false
 *	showArrows - boolean to show/hide a left/right control buttons to the slider - this will adjust slider size to fit them in - default false
 *	snap - boolean to configure whether the slider snaps on drag end - default false
 *	snapEnds - boolean to configure whether the slider puts the first/last snap points at the ends of the slider - default false
 * 
 * Notes:
 *  An over-rideable function ($.fn.emc_sliderNav.submitFromSlider) is provided to give a hook for the user hitting enter when a slider 
 *  is in focus but no specific behaviour has been defined.
 *
 */

(function($) {
	jQuery.fn.backgroundPosition = function() {
		var p = $(this).css('background-position');
		if (typeof(p) === 'undefined') {
			return $(this).css('background-position-x') + ' ' + $(this).css('background-position-y');
		}
		else {
			return p;
		}
	};
})(jQuery);

(function($){
	
	$.fn.emc_sliderNav=function(options){

		var private_opts={debug:false,dragControl:'dragControl',clickControl:'clickControl',sliderTrail:'sliderTrail',sliderEnd:'sliderEnd'};

		var opts=$.extend({},$.fn.emc_sliderNav.defaults,options,private_opts);

		_debug=function(msg){
			if (opts.debug === false) {
				return;
			}
			if ($('#debug').length === 0) {
				$('body').prepend('<div id="debug" style="position:absolute;top:0;right:0;color:#999;background-color:#eee;opacity:0.8;"></div>');
			}
			$debug=$('#debug');
			if ($debug.length > 0) {
				$debug.prepend('- ' + msg + '<br />');
			}
			else {
				alert(msg);
			}
		};		

		this.each(function() {

			// used for setInterval variable to run slider when left/right cursor keys are held down
			var autoSlide;
			
			// create a local options object (also containing global options) that can be passed between functions for data access and consistency
			var o = {
				opts: opts, 		// global options object
				zone: null, 		// current selected navigation item
				pos: 0, 			// slider position (0-100)
				remote: false,		// remote display attached to slider control
				sliderObj: $(this)	// jQuery object of slider container DOM element
			};
			
			// add slider bar markup if not found
			if (o.sliderObj.find('.'+o.opts.sliderClass).length === 0) {
				sliderLink = document.createElement('a');
				sliderDiv = document.createElement('div');
				sliderInnerDiv = document.createElement('div');
				
				$(sliderLink).attr({
					'style': 'left:0',
					'class': o.opts.dragControl,
					'href': '#addedByPlugin'
				});
				$(sliderInnerDiv).attr({
					'class': o.opts.sliderTrail
				});
				
				$(sliderDiv).attr('class',opts.sliderClass).append(sliderInnerDiv).append(sliderLink);
				
				// additional markup required for control arrows if enabled
				if(o.opts.showArrows) {
					sliderArrLeft = document.createElement('a');
					sliderArrRight = document.createElement('a');

					$(sliderArrLeft).attr({
						'class': o.opts.clickControl + ' l',
						'href': '#addedByPlugin'
					});
					$(sliderArrRight).attr({
						'class': o.opts.clickControl + ' r',
						'href': '#addedByPlugin'
					});

					$(sliderDiv).append(sliderArrLeft).append(sliderArrRight);			
				// otherwise add a cosmetic div to give the curved end of the slider
				} else {
					sliderInnerEnd = document.createElement('span');

					$(sliderInnerEnd).attr({
						'class': o.opts.sliderEnd
					});

					$(sliderDiv).append(sliderInnerEnd);			
				}
				
				if (o.opts.sliderPosition == 'top') {
					$(sliderDiv).prependTo(this);
				}
				else if (o.opts.sliderPosition == 'bottom') {
					$(sliderDiv).appendTo(this);
				}
			}

			// adjust slider width and position to fit in control arrows, if enabled
			if(o.opts.showArrows) {
				o.sliderObj.find('.'+o.opts.sliderClass).css({
					'margin-left':  $(sliderArrLeft).width()+'px',
					'width': $(this).width()-($(sliderArrLeft).width()+$(sliderArrRight).width())+'px'
				});
				temp = o.sliderObj.find('.'+o.opts.sliderTrail).backgroundPosition().split(' ')[1];
				o.sliderObj.find('.'+o.opts.sliderTrail).css({
					'background-position': 'center '+temp
				});
			}
			
			// remove anchors from controllerItemSelector; anchors maybe present so that non-js users can click through to next page
			o.sliderObj.find('.'+o.opts.controllerClass+' '+o.opts.controllerItemSelector).each(function(i) {
				if($(this).children().is("a") ) {
					var anchorText = $(this).children().text();
					$(this).empty().text(anchorText);
				}
			});
						
			// set a couple more local options that require data created in the object initialisation, and the added markup above			
			o.sliderMax = o.sliderObj.find('.'+o.opts.sliderClass).width() - o.sliderObj.find('a.'+o.opts.dragControl).width();
			o.range = o.sliderMax / o.sliderObj.find('.'+o.opts.controllerClass+' '+o.opts.controllerItemSelector).length;
			o.scrollTarget = o.sliderObj.find('.'+o.opts.controllerClass);		
			o.scroll = (o.sliderObj.width() < o.scrollTarget.width());
			
			o.snaps = [];
						
			// calculate where the 'snap' points are located in px
			o.sliderObj.find('.'+o.opts.controllerClass+' '+o.opts.controllerItemSelector).each(function(i) {
				if (o.opts.snapEnds) {
					// set first/last points at ends of slider
					l = o.sliderObj.find('.'+o.opts.controllerClass+' '+o.opts.controllerItemSelector).length - 1;
					o.snaps.push(Math.round((o.range * i) + ((o.range / l) * i)));				
				} else {
					// or set snaps in mid-points
					o.snaps.push(Math.round((o.range * (i+1)) - (o.range/2)));
				}
			});
			
			_debug('snap points at '+o.snaps);
			
			if (o.opts.remoteDisplay !== null && $('.'+o.opts.remoteDisplay).length > 0) {
				_debug('remote display selected and found');
				o.remote = true;
				o.scrollTarget = $('.'+o.opts.remoteDisplay);
				
				// recalculate display element width based on actual size of child elements (to avoid having to control an accurate width in css)
				var w = 0;				
				o.scrollTarget.children().each(function() {
					$this = $(this);
					w = w + $this.width() + parseInt($this.css('margin-right'), 10) + parseInt($this.css('margin-left'), 10);
				});	
				if (o.scrollTarget.width() != w) {
					o.scrollTarget.width(w);
				}	
				
				o.scroll = (o.scrollTarget.parent().width() < o.scrollTarget.width());
				
			}
			
			// if the size of the controlling items is wider than the container, they should scroll in relation to slider movement - this requires some extra markup
			if (o.scroll) {
				wrapperDiv = document.createElement('div');
				$(wrapperDiv).addClass(o.opts.wrapperClass).css({
					width: o.scrollTarget.parent().width()+'px',
					overflow: 'hidden',
					position: 'relative'					
				});

				o.scrollTarget.wrap(wrapperDiv);				
			}
			
			
			// ensure any radio buttons are deselected
			o.sliderObj.find('input[type=radio]').removeAttr('checked');

			_debug(o.sliderObj.find('.'+opts.controllerClass+' '+opts.controllerItemSelector).length + ' items in slider nav');

			// add behaviour to slider
			o.sliderObj.find('a.'+o.opts.dragControl).click(function() { this.focus(); return false;}).keydown(function(e) {
				// 37 = left arrow, 39 = right arrow
				if (e.keyCode == 37 || e.keyCode == 39) {
				
					var el = this;
					var dir = (e.keyCode == 37) ? '-' : '+';

					if(o.opts.showArrows) {
						if (dir == '-') {
							o.sliderObj.find('a.'+o.opts.clickControl).filter('.l').addClass('lOn');					
						} else {
							o.sliderObj.find('a.'+o.opts.clickControl).filter('.r').addClass('rOn');
						}
					}

					_interact(el,dir);
				}
				// 13 = enter/return
				if (e.keyCode == 13) {
					// over-rideable function reference
					$.fn.emc_sliderNav.submitFromSlider({
						el: o.sliderObj
					});
				}
			
			}).keyup(function(e) {
			
				if (e.keyCode == 37 || e.keyCode == 39) {

					var dir = (e.keyCode == 37) ? '-' : '+';
					
					if(o.opts.showArrows) {
						o.sliderObj.find('a.'+o.opts.clickControl).removeClass('lOn').removeClass('rOn');					
					}
	
					_interactEnd(dir);
					
				}
				
			}).draggable({
				axis: 'x',
				containment: 'parent',
				drag: function(event,ui) {

					if (o.opts.showTrail) {
						o.sliderObj.find('div.' + o.opts.sliderTrail).width(ui.position.left);
					}

					o.pos = Math.round(ui.position.left/(o.sliderMax/100));
					
					_debug(o.snaps + ' ' + ' ' + o.snaps[0] + ' ' + o.snaps[o.snaps.length-1] + ' ' + ui.position.left);//
					
					if (o.scroll) {
						
						var intLeft = ui.position.left;

						_scrolling(intLeft);
						
					}

					
					o.zone = $.fn.emc_sliderNav.moveZone({
						left: ui.position.left,
						o: o
					});			
				},
				start: function(event,ui) {
					
					// objective here is to give a visual indication on the control arrows of the direction the slider is going in
					// the 'start' event does not have a position on first use and does not update during sliding so this seems like the best way to monitor movement
					if (o.opts.showArrows) {
						var intPos = o.pos;
						watchSlide = setInterval(function() {

							dir = (intPos == o.pos) ? '|' : (intPos < o.pos) ? '+' : '-'; 

							if (dir == '-') {
								o.sliderObj.find('a.'+o.opts.clickControl).filter('.l').addClass('lOn');					
							} else if (dir == '+') {
								o.sliderObj.find('a.'+o.opts.clickControl).filter('.r').addClass('rOn');
							} else {
								o.sliderObj.find('a.'+o.opts.clickControl).removeClass('lOn').removeClass('rOn');										
							}
							
							intPos = o.pos;
							
						},350);
					}
					
				},
				stop: function(event,ui) {

					_debug('stopped at ' + ui.position.left + 'px | pos value ' + Math.round(ui.position.left/(o.sliderMax/100)) + ' ' + o.snaps[o.zone]);

					if (o.opts.snap) {
						o.pos = Math.round(o.snaps[o.zone]/(o.sliderMax/100));
						
						$.fn.emc_sliderNav.moveSlider({
							el: this,
							o: o
						});														
					} else {
						o.pos = Math.round(ui.position.left/(o.sliderMax/100));					
					}
					
					if(o.opts.showArrows) {
						clearInterval(watchSlide);
						o.sliderObj.find('a.'+o.opts.clickControl).removeClass('lOn').removeClass('rOn');					
					}					
					
					this.focus();
				}				
			});
			
			// add behaviour to control arrows, if present (will only be in DOM if option is enabled)
			o.sliderObj.find('.'+o.opts.clickControl).mousedown(function() {
				
				var el = o.sliderObj.find('a.'+o.opts.dragControl).get(0);
				var dir = ($(this).hasClass('l')) ? '-' : '+';

				_interact(el,dir);
			}).keydown(function(e) {

				if (e.keyCode == 37 || e.keyCode == 39 || e.keyCode == 13) {
				
					var el = o.sliderObj.find('a.'+o.opts.dragControl).get(0);
					
					if ($(this).hasClass('l')) {
						if (e.keyCode == 39) {
							return false;
						}
						$(this).addClass('lOn');
						dir = '-';
					} else {
						if (e.keyCode == 37) {
							return false;
						}
						$(this).addClass('rOn');
						dir = '+';
					}
					
					_interact(el,dir);
				}
			
			}).mouseup(function() {

				var dir = ($(this).hasClass('l')) ? '-' : '+';

				_interactEnd(dir);
				
			}).keyup(function(e) {
								
				if ($(this).hasClass('l')) {
					dir = '-';
				}
				else {
					dir = '+';
				}
				
				$(this).removeClass('lOn').removeClass('rOn');
			
				_interactEnd(dir);
				
			}).click(function(e) {
				return false;
			});
			
			// add behaviour to nav
			o.sliderObj.find('.'+o.opts.controllerClass+' '+o.opts.controllerItemSelector).mouseover(function() {
				if (opts.controllerBehaviour == "hover") {
					$.fn.emc_sliderNav.controlSlider({
						el: this,
						o: o
					});
				}
			}).click(function() {
				if (o.opts.controllerBehaviour == "click") {
					$.fn.emc_sliderNav.controlSlider({
						el: this,
						o: o
					});
				}
			}).find('input[type=radio]').map(function() {
				// use the options object to determine if any radio buttons should be hidden
				if (o.opts.hideRadios) {
					$(this).hide();
				}
			});
			
			_interact = function(el,dir) {
	
				o.pos = $.fn.emc_sliderNav.moveSlider({
					el: el,
					o: o,
					dir: dir
				});
				
				o.zone = $.fn.emc_sliderNav.moveZone({
					left: o.pos*(o.sliderMax/100),
					o: o
				});
				
				if (o.scroll) {
					var intLeft = (o.pos*(o.sliderMax/100));			
					_scrolling(intLeft);
				}
				
				// moz mac does not repeatedly fire event on keydown so needs some help
				if ($.browser.mozilla && navigator.userAgent.match('Mac')) {
					autoSlide = setInterval(function() {
						o.pos = $.fn.emc_sliderNav.moveSlider({
							el: el,
							o: o,
							dir: dir
						});						
		
						o.zone = $.fn.emc_sliderNav.moveZone({
							left: o.pos*(o.sliderMax/100),
							o: o
						});
		
						if (o.scroll) {
							var intLeft = (o.pos*(o.sliderMax/100));
							_scrolling(intLeft);
						}					
					},opts.sliderSpeed);
				}
			};
			
			_interactEnd = function(dir) {
			
				// clearInterval only created for moz mac
				if ($.browser.mozilla && navigator.userAgent.match('Mac')) {
					clearInterval(autoSlide);
				}

				o.zone = $.fn.emc_sliderNav.moveZone({
					left: o.pos*(o.sliderMax/100),
					o: o
				});				
				
				if (o.opts.snap) {

					if (dir == '+' && (o.zone < o.snaps.length - 1)) {
						o.zone++;
					}
					if (dir == '-' && (o.zone > 0)) {
						o.zone--;
					}

					o.pos = Math.round(o.snaps[o.zone]/(o.sliderMax/100));
					
					$.fn.emc_sliderNav.moveSlider({
						el: o.sliderObj.find('.'+o.opts.sliderClass+' a').get(0),
						o: o
					});		
					
					$.fn.emc_sliderNav.updateActive({
						o: o
					});

				}
			
			};
			
			_scrolling = function(left) {

				var intMax,intPos;
				var scrollZone = true;
				
				if (o.opts.constrainScroll) {
					intMax = o.snaps[o.snaps.length-1] - o.snaps[0];
					intPos = Math.round((left-o.snaps[0])/(intMax/100));
					scrollZone = (left >= o.snaps[0] && left <= o.snaps[o.snaps.length-1]) ? true : false;
				} else {
					intMax = o.sliderMax;
					intPos = o.pos;						
				}
					
				if (scrollZone) {
					diff = o.scrollTarget.width() - o.scrollTarget.parent().width();		
					o.scrollTarget.css({'left':'-'+(diff/100)*intPos+'px'});				
				}
			
			};
			
			if (o.opts.startingPosition) {
				$.fn.emc_sliderNav.controlSlider({
					el: null,
					o: o,
					startingPosition : o.opts.startingPosition
				});
			}
			
		});
		
	};
	
	$.fn.emc_sliderNav.controlSlider = function(args) {

		args.o.zone = $(args.el).prevAll().length;
		
		args.o.sliderObj.find('.'+args.o.opts.controllerClass+' '+args.o.opts.controllerItemSelector).each(function(i) {
			if (this == args.el) {
				args.o.zone = i;
			}
		});
		
		if(args.startingPosition) {
			args.o.pos = args.startingPosition;
		}
		else {
			args.o.pos = Math.round(args.o.snaps[args.o.zone]/(args.o.sliderMax/100));
		}
		
		_debug('snap value ' + args.o.snaps[args.o.zone] + ' | pos value '+args.o.pos);
		
		args.o.pos = $.fn.emc_sliderNav.moveSlider({
			el: args.o.sliderObj.find('.'+args.o.opts.sliderClass+' a').get(0),
			o: args.o
		});			
		$.fn.emc_sliderNav.updateActive({
			o: args.o
		});
			
	};
	
	$.fn.emc_sliderNav.moveSlider = function(args)  {
		
		nextPos = (args.dir) ? ((args.dir == '+') ? args.o.pos+1 : args.o.pos-1) : args.o.pos;

		_debug(args.o.pos + ' ' + args.o.pos*(args.o.sliderMax/100));
		
		if (nextPos > -1 && nextPos < 101) {
			args.el.style.left = nextPos*(args.o.sliderMax/100) + 'px';
			if (args.o.opts.showTrail) {
				args.o.sliderObj.find('div.' + args.o.opts.sliderTrail).width(nextPos * (args.o.sliderMax / 100));
			}
			args.o.pos = nextPos;
		} else {
			if (typeof autoSlide != "undefined") {
				clearInterval(autoSlide);
			}
		}
		
		return args.o.pos;

	};
	
	$.fn.emc_sliderNav.moveZone = function(args)  {

		nextZone = Math.floor(args.left/args.o.range);
		
		if (nextZone == args.o.sliderObj.find('.' + args.o.opts.controllerClass + ' ' + args.o.opts.controllerItemSelector).length) {
			nextZone--;
		}
	
		_debug('px ' + args.left + ' ~ in zone ' + nextZone + '(' + args.o.zone + ')');

		if (args.o.zone != nextZone) {
		
			args.o.zone = nextZone;
		
			$.fn.emc_sliderNav.updateActive({
				o: args.o
			});

		}
	
		return args.o.zone;
		
	};
	
	$.fn.emc_sliderNav.updateActive = function(args) {
	
		args.o.sliderObj.find('.'+args.o.opts.controllerClass).find(args.o.opts.controllerItemContainer).removeClass(args.o.opts.usingClass).end().find('input[type=radio]').removeAttr('checked');
		args.o.sliderObj.find('.'+args.o.opts.controllerClass+' '+args.o.opts.controllerItemContainer).eq(args.o.zone).addClass(args.o.opts.usingClass).find('input[type=radio]').attr('checked','checked');				

		if (args.o.scroll) {
		
			var intMax, intPos;
			var scrollZone = true;
			
			if (args.o.opts.constrainScroll) {
				intMax = args.o.snaps[args.o.snaps.length-1] - args.o.snaps[0];
				intPos = Math.round(((args.o.pos*(args.o.sliderMax/100))-args.o.snaps[0])/(intMax/100));
				scrollZone = ((args.o.pos*(args.o.sliderMax/100)) >= args.o.snaps[0] && (args.o.pos*(args.o.sliderMax/100)) <= args.o.snaps[args.o.snaps.length-1]) ? true : false;
			} else {
				intMax = args.o.sliderMax;
				intPos = args.o.pos;
			}
		
			if (scrollZone) {
				diff = args.o.scrollTarget.width() - args.o.scrollTarget.parent().width();
				args.o.scrollTarget.css({'left':'-'+(diff/100)*intPos+'px'});				
			}
		}					
	
	};
	
	$.fn.emc_sliderNav.submitFromSlider = function(args) {
		_debug(args.el.serialize());		
		return false;	
	};
	
	$.fn.emc_sliderNav.defaults={
		sliderSpeed: 100,
		sliderPosition: 'top',
		sliderClass: 'slider',
		controllerClass: 'sliderControl',
		controllerItemContainer: 'li',
		controllerItemSelector: 'li',
		controllerBehaviour: 'hover',
		wrapperClass: 'scrollContainer',
		usingClass: 'active', 
		remoteDisplay: null,
		constrainScroll: true,
		hideRadios: true,
		showTrail: false,
		showArrows: false,
		snap: false,
		snapEnds: false
	};	
	
	
	

})(jQuery);

