/**
 * jQuery plugin: rotate
 * @author
 * 
 * @param {Object} opt
 */
(function ($) {
	
	// default configuration shared by all instances
	var options = {
		api: false, // true if controls should be returned instead of jquery object
		isCircular: false,
		isGapless: false,
		autoPlay: {
			pages: 1,
			interval: 3000,
			active: false
		},
		namespace: '.rotate',
		classProcessed: 'jq-rotate',
		classSelected: 'selected',
		classInactive: 'inactive',
		classClonedItem: 'clonedItem',
		selectBack: 'a.back',
		selectNext: 'a.next',
		selectPageCurrent: 'span.pageCurrent',
		selectPageCount: 'span.pageCount',
		selectItemCurrent: 'span.itemCurrent',
		selectItemCount: 'span.itemCount',
		selectContainer: 'ul.slideContainer',
		selectClipContainer: 'div.slideWrapper',
		selectItem: 'li',
		direction: 'vertical',
		itemsPerPage: 3,
		cssProperties: {
			h: {
				width: 'outerWidth',
				innerWidth: 'innerWidth',
				pos: 'left',
				animate: 'marginLeft',
				itemPad: 'marginLeft'
			},
			v: {
				width: 'outerHeight',
				innerWidth: 'innerHeight',
				pos: 'top',
				animate: 'marginTop',
				itemPad: 'marginTop'
			}
		},
		onInit: function () {},
		onUpdate: function () {}
	};
	
	// store a list of all instances (mostly debugging purposes)
	var instances = []; // holds all instances of PluginClass
	
	
	/**
	 * Instantiates a single rotate.
	 * @param jQuery el
	 * @param Object opt
	 * @param Array args
	 * @constructor
	 */
	var PluginClass = function (el, opt, args) {
		
		var obj = this;
		opt = obj.opt = $.extend({}, options, opt || {});
		el = obj.el = $(el);
		instances.push(obj);
		el.data('rotate' + this.opt.namespace, obj).addClass(opt.classProcessed);
		
		
		// bind state events to element
		el.bind('init' + opt.namespace, opt.onInit);
		el.bind('update' + opt.namespace, opt.onUpdate);
		el.bind('update' + opt.namespace, function (e, data) {
			obj.findItems();
		});
		
		
		// add methods and default fields to instance
		$.extend(obj, {
			
			isCursorHovering: false,
			isAnimating: false,
			intervalAutoPlay: null,
			currentIndex: 0,
			clipContainerWidth: 0,
			dir: opt.cssProperties[opt.direction.substring(0, 1)],
			container: $(opt.selectContainer, el),
			clipContainer: $(opt.selectClipContainer, el),
			items: null,
			back: $(opt.selectBack, el),
			next: $(opt.selectNext, el),
			pageCurrent: $(opt.selectPageCurrent, el),
			pageCount: $(opt.selectPageCount, el),
			itemCurrent: $(opt.selectItemCurrent, el),
			itemCount: $(opt.selectItemCount, el),
			
			
			
			determineCurrentIndex: function () {
				var index = this.items.index(this.items.filter('.' + this.opt.classSelected)) || 0;
				return this.currentIndex = Math.max(0, index);
			},
			
			updateSlider: function (index, dir) {
				
				this.moveSlider(this.currentIndex, index, dir);
				
				this.updatePaging(index);
				
				this.updateSliderItem(index);
					
				if (this.currentIndex != index) {
					this.el.trigger('change', {
						rotate: this,
						oldIndex: this.currentIndex,
						newIndex: index
					});
				}
				
				this.currentIndex = index;
				
				this.updateControls();
				
			},
			
			updateControls: function () { 
				var obj = this;
				
				var iterate = function (i) {
					var link = $(this);
					var data = link.metadata();
					var steps = data.steps || 1;
					if ((!obj.opt.isCircular && check(steps)) || obj.opt.itemsPerPage >= obj.items.length) {
						link.addClass(obj.opt.classInactive);
					} else {
						link.removeClass(obj.opt.classInactive);
					}
				};
				
				var check = function (steps) {
					return (steps == 1 && obj.currentIndex + steps >= obj.items.length) || (steps > 1 && obj.getSliderPage(obj.currentIndex + steps) == obj.getSliderPage(obj.currentIndex))
				};
				this.next.each(iterate);
				
				check = function (steps) {
					return obj.currentIndex - steps < 0;
				};
				this.back.each(iterate);
			},
			
			updateSliderItem: function () {
				this.items.removeClass(this.opt.classSelected).eq(this.currentIndex).addClass(this.opt.classSelected);
			},
			
			updatePaging: function (index) {
				this.pageCount.html(this.getSliderPage(this.items.length) + 1);
				this.pageCurrent.html(this.getSliderPage(index) + 1);
				this.itemCount.html(this.items.length);
				this.itemCurrent.html(index + 1);
			},
			
			getNextIndex: function (dir, steps) {
				steps = steps || 1;
				var index = this.currentIndex;
				var pages = Math.ceil(this.items.length / this.opt.itemsPerPage);
				var trail = this.opt.isGapless ? 0 : (this.opt.itemsPerPage - (this.items.length % this.opt.itemsPerPage)) % this.opt.itemsPerPage;
				switch (dir) {
					case 1:
						index += steps;
						break;
					case -1:
						index -= steps;
						break;
					case 0:
					default:
						break;
				}
				if (this.opt.isCircular) {
					if (index < 0 || index >= this.items.length) {
						if (dir > 0) {
							index = index - trail;
						} else {
							index = index + trail;
							index = this.items.length + index;
						}
					}
					index = index % this.items.length;
				} else {
					index = Math.min(index, this.items.length - 1);
					index = Math.max(index, 0);
				}
				return index;
			},
			
			moveSlider: function (oldIndex, newIndex, dir) {
				// save scope
				var obj = this;
				this.isAnimating = true;
				
				// determine visible slider portion
				var oldPos = this.getSliderPage(oldIndex) * this.opt.itemsPerPage;
				var newPos = this.getSliderPage(newIndex) * this.opt.itemsPerPage;
				
				// if slider has not moved, return
				if (oldPos == newPos) {
					this.isAnimating = false;
					return false;
				}

				this.el.trigger('move', {
					rotate: obj,
					oldIndex: oldIndex,
					newIndex: newIndex
				});
				
				// get scroll width
				var width = this.getScrollWidth(oldIndex, newIndex, dir);
				var currentPos = +/^(-?\d+)/.exec(this.container.css(obj.dir.animate))[1];
				
				if (this.opt.isCircular && dir == 1 && Math.abs(currentPos) >= 2 * this.getSliderWidth()) {
					currentPos += this.getSliderWidth();
					this.container.css(obj.dir.animate, currentPos + 'px');
				}
				
				if (this.opt.isCircular && dir == -1 && Math.abs(currentPos) <= 1 * this.getSliderWidth()) {
					currentPos -= this.getSliderWidth();
					this.container.css(obj.dir.animate, currentPos + 'px');
				}
				
				// scroll the slider
				var anim = {};
				
				if (dir == -1) {
					width = '+=' + width;
				}
				if (dir == 1) {
					width = '-=' + width;
				}
				
				
				anim[obj.dir.animate] = width + 'px';
				this.container.animate(anim, function () {
					obj.isAnimating = false;
				});
				
				
				return true;
			},
			
			getScrollWidth: function (oldIndex, newIndex, dir) {
				var oldPos = this.getSliderPage(oldIndex) * this.opt.itemsPerPage;
				var newPos = this.getSliderPage(newIndex) * this.opt.itemsPerPage;
				var wrapped = (dir == 1 && oldPos > newPos) || (dir == -1 && oldPos < newPos);
				var width = 0;
				var items = this.getScrollItems(oldIndex, newIndex, dir, oldPos, newPos, wrapped);
				items.each(function (i) {
					width += $(this)[obj.dir.width]({
						margin: true,
						border: true,
						padding: true
					});
				});
				if (wrapped) {
					if (!this.opt.isGapless) {
						width += this.getSliderPadding();
					}
				}
				return width;
			},
			
			getSliderWidth: function (realWidth) {
				var width = 0, obj = this;
				this.items.each(function (i) {
					width += $(this)[obj.dir.width]({
						margin: true,
						border: true,
						padding: true
					});
				});
				if (!this.opt.isGapless && !realWidth) {
					width += this.getSliderPadding();
				}
				return width;
			},
			
			getScrollItems: function (oldIndex, newIndex, dir, oldPos, newPos, wrapped) {
				var items = [];
				this.items.each(function (i) {
					if (
						(!wrapped && dir == 1 && i < newPos && i >= oldPos) ||
						(!wrapped && dir == -1 &&  i > newPos && i <= oldPos) ||
						(wrapped && dir == -1 && i >= newPos) ||
						(wrapped && dir == 1 && i >= oldPos)
						) {
						items.push(this);
					}
				});
				return $(items);
			},
			
			doBack: function (steps) {
				if (this.isAnimating) {
					return;
				}
				var index = this.getNextIndex(-1, steps);
				this.updateSlider(index, -1);
			},
			
			doNext: function (steps) {
				if (this.isAnimating) {
					return;
				}
				var index = this.getNextIndex(1, steps);
				this.updateSlider(index, 1);
			},
			
			doSelect: function (index) {
				if (this.isAnimating) {
					return;
				}
				this.updateSlider(index, 0);
			},
			
			doPrefetch: function (dir, steps) {
				steps = steps || 1;
				dir = dir || 1;
				var page = this.getSliderPage(this.currentIndex) + dir;
				var lower = page * this.opt.itemsPerPage - 1;
				var upper = (page + 1) * this.opt.itemsPerPage;
				this.items.filter(':gt(' + lower + '):lt(' + upper + ')').trigger('prefetch', {
					rotate: this
				});
			},
			
			doPlay: function () {
				if (!this.intervalAutoPlay) {
					this.initAutoPlay();
				}
			},
			
			doStop: function () {
				if (this.intervalAutoPlay) {
					clearInterval(obj.intervalAutoPlay);
					obj.intervalAutoPlay = null;
				}
			},
			
			initAutoPlay: function () {
				var obj = this;
				this.el.one('click' + this.opt.namespace, function (e) {
					obj.doStop();
				});
				var steps = this.opt.autoPlay.pages ? this.opt.autoPlay.pages * this.opt.itemsPerPage : this.opt.autoPlay.items;
				var dir = steps < 0 ? 'doBack' : 'doNext';
				steps = Math.abs(steps);
				this.intervalAutoPlay = setInterval(function () {
					if (!obj.isCursorHovering) {
						obj[dir](steps);
					}
				}, this.opt.autoPlay.interval);
				
			},
			
			getSliderPage: function (index) {
				return Math.floor(Math.min((this.items.length - 1) / this.opt.itemsPerPage, index / this.opt.itemsPerPage));
			},
			
			findItems: function () {
				var item = this.items ? this.items.eq(this.currentIndex).get(0) : null;
				this.items = $(this.opt.selectItem, this.container).not('.' + this.opt.classClonedItem);
				if (this.items.eq(this.currentIndex).get(0) != item) {
					this.doSelect(0);
				}
				var containerOffset = +/^(-?\d+)/.exec(this.container.css(this.dir.animate))[1];
				if (this.opt.isCircular) {
					var first = this.items.clone(true).removeClass(this.opt.classSelected).addClass(this.opt.classClonedItem).appendTo(this.container);
					var second = this.items.clone(true).addClass(this.opt.classClonedItem).removeClass(this.opt.classSelected).appendTo(this.container);
					var pad = this.getSliderPadding();
					if (!this.opt.isGapless) {
						first.eq(0).css(this.dir.itemPad, pad + 'px');
						second.eq(0).css(this.dir.itemPad, pad + 'px');
					}
					containerOffset += -this.getSliderWidth();
				}
				this.container.css(this.dir.animate, containerOffset + 'px');
			},
			
			getSliderPadding: function () {
				return (this.clipContainerWidth - (this.getSliderWidth(true) % this.clipContainerWidth)) % this.clipContainerWidth;
			},
			
			
			init: function () {

				// bind click handlers to next-button
				this.next.each(function (numLink) {
					var link = $(this);
					var data = link.metadata();
					var steps = data.steps || data.pages * obj.opt.itemsPerPage;
					link.bind('click' + opt.namespace, function(e) {
						obj.doNext(steps);
						obj.doPrefetch(1, opt.itemsPerPage);
						e.preventDefault();
					}).bind('mouseover' + opt.namespace, function (e) {
						obj.doPrefetch(1, steps);
						e.preventDefault();
					});
				});
				
				// bind click handlers to back-button
				this.back.each(function (numLink) {
					var link = $(this);
					var data = link.metadata();
					var steps = data.steps || data.pages * obj.opt.itemsPerPage;
					link.bind('click' + opt.namespace, function(e) {
						obj.doBack(steps);
						obj.doPrefetch(1, opt.itemsPerPage);
						e.preventDefault();
					}).bind('mouseover' + opt.namespace, function (e) {
						obj.doPrefetch(-1, steps);
						e.preventDefault();
					});
				});
				
				this.clipContainerWidth = this.clipContainer[this.dir.innerWidth]();
				this.findItems();
				this.updateControls();
				this.updatePaging(this.determineCurrentIndex());
				
				if (this.opt.autoPlay.active) {
					this.initAutoPlay();
				}
				
				this.el.bind('mouseenter' + this.opt.namespace, function (e) {
					obj.isCursorHovering = true;
				}).bind('mouseleave' + this.opt.namespace, function (e) {
					obj.isCursorHovering = false;
				});
				
				this.el.trigger('init' + this.opt.namespace, {
					rotate: this
				});
				
				
			},
			remove: function () {
				this.el.unbind(this.opt.namespace).removeClass(opt.classProcessed);
				this.el.removeData('rotate' + this.opt.namespace);
				this.removed = true;
			},
			removed: false
		});
		
		obj.init();
		
	};
	
	
	// make plugin configuration public
	var common = {
		options: options,
		instances: instances,
		fn: PluginClass
	};
	$.extend({
		'rotate': common
	});
	
	
	// add method to jquery result sets 
	$.fn['rotate'] = function (opt) {
		
		// get config
		opt = $.extend(true, {}, common.options, opt);
		var jq = this;
		var firstInstance = null;
		
		// iterate over result set
		this.each(function (i) {
			var el = $(this);
			var instance = el.data('rotate' + opt.namespace) || new PluginClass(el, opt, arguments);
			if (i == 0) {
				firstInstance = instance;
			}
		});
		
		// if control object should be returned
		if (opt.api) {
			return firstInstance;	
		} else {
			return jq;
		}
		
	};
	
	
})(jQuery);
