/*!
 * jQuery.serialScroll
 * Copyright (c) 2007-2015 Ariel Flesler - aflesler<a>gmail<d>com | http://flesler.blogspot.com
 * Licensed under MIT.
 * @projectDescription Animated scrolling of series with jQuery
 * @author Ariel Flesler
 * @version 1.3.1
 * https://github.com/flesler/jquery.serialScroll
 */
;(function($) {

	var NAMESPACE = '.serialScroll';
	
	var $serialScroll = $.serialScroll = function(settings) {
		return $(window).serialScroll(settings);
	};

	// Many of these defaults, belong to jQuery.ScrollTo, check it's demo for an example of each option.
	// @link {http://demos.flesler.com/jquery/scrollTo/ ScrollTo's Demo}
	$serialScroll.defaults = {// the defaults are public and can be overriden.
		duration:1000, // how long to animate.
		axis:'x', // which of top and left should be scrolled
		event:'click', // on which event to react.
		start:0, // first element (zero-based index)
		step: 1, // how many elements to scroll on each action
		lock:true,// ignore events if already animating
		cycle:true, // cycle endlessly (constant velocity)
		constant:true // use contant speed ?
		/*
		navigation:null,// if specified, it's a selector to a collection of items to navigate the container
		target:window, // if specified, it's a selector to the element to be scrolled.
		interval:0, // it's the number of milliseconds to automatically go to the next
		lazy:false,// go find the elements each time (allows AJAX or JS content, or reordering)
		stop:false, // stop any previous animations to avoid queueing
		force:false,// force the scroll to the first element on start ?
		jump: false,// if true, when the event is triggered on an element, the pane scrolls to it
		items:null, // selector to the items (relative to the matched elements)
		prev:null, // selector to the 'prev' button
		next:null, // selector to the 'next' button
		onBefore: function() {}, // function called before scrolling, if it returns false, the event is ignored
		exclude:0 // exclude the last x elements, so we cannot scroll past the end
		*/
	};

	$.fn.serialScroll = function(options) {

		return this.each(function() {
			var 
				settings = $.extend({}, $serialScroll.defaults, options),
				// this one is just to get shorter code when compressed
				event = settings.event, 
				// ditto
				step = settings.step, 
				// ditto
				lazy = settings.lazy, 
				// if a target is specified, then everything's relative to 'this'.
				context = settings.target ? this : document, 
				// the element to be scrolled (will carry all the events)
				$pane = $(settings.target || this, context),
				// will be reused, save it into a variable
				pane = $pane[0], 
				// will hold a lazy list of elements
				items = settings.items, 
				// index of the currently selected item
				active = settings.start, 
				// boolean, do automatic scrolling or not
				auto = settings.interval, 
				// save it now to make the code shorter
				nav = settings.navigation, 
				// holds the interval id
				timer; 

			// Incompatible with $().animate()
			delete settings.step;
			delete settings.start;

			// If no match, just ignore
			if (!pane) return;
				
			// if not lazy, save the items now
			if (!lazy) {
				items = getItems();
			}

			// generate an initial call
			if (settings.force || auto) {
				jump({}, active);
			}

			// Button binding, optional
			$(settings.prev||[], context).on(event, -step, move);
			$(settings.next||[], context).on(event, step, move);

			// Custom events bound to the container
			if (!pane._bound_) {
				$pane
					 // You can trigger with just 'prev'
					.on('prev'+NAMESPACE, -step, move)
					// f.e: $(container).trigger('next');
					.on('next'+NAMESPACE, step, move)
					// f.e: $(container).trigger('goto', 4);
					.on('goto'+NAMESPACE, jump);
			}

			if (auto) {
				$pane
					.on('start'+NAMESPACE, function(e) {
						if (!auto) {
							clear();
							auto = true;
							next();
						}
					 })
					.on('stop'+NAMESPACE, function() {
						clear();
						auto = false;
					});
			}

			// Let serialScroll know that the index changed externally
			$pane.on('notify'+NAMESPACE, function(e, elem) {
				var i = index(elem);
				if (i > -1) {
					active = i;
				}
			});

			// Avoid many bindings
			pane._bound_ = true;

			// Can't use jump if using lazy items and a non-bubbling event
			if (settings.jump) {
				(lazy ? $pane : getItems()).on(event, function(e) {
					jump(e, index(e.target));
				});
			}

			if (nav) {
				nav = $(nav, context).on(event, function(e) {
					e.data = Math.round(getItems().length / nav.length) * nav.index(this);
					jump(e, this);
				});
			}

			function move(e) {
				e.data += active;
				jump(e, this);
			}

			function jump(e, pos) {
				if (!$.isNumeric(pos)) {
					pos = e.data;
				}

				var	n, 
					// Is a real event triggering ?
					real = e.type, 
					// Handle a possible exclude
					$items = settings.exclude ? getItems().slice(0,-settings.exclude) : getItems(),
					limit = $items.length - 1,
					elem = $items[pos],
					duration = settings.duration;

				if (real) e.preventDefault();

				if (auto) {
					// clear any possible automatic scrolling.
					clear();
					timer = setTimeout(next, settings.interval); 
				}

				// exceeded the limits
				if (!elem) {
					n = pos < 0 ? 0 : limit;
					// we exceeded for the first time
					if (active !== n) {
						pos = n;
					// this is a bad case
					} else if (!settings.cycle) {
						return;
					// invert, go to the other side
					} else {
						pos = limit - n;
					}
					elem = $items[pos];
				}

				// no animations while busy
				if (!elem || settings.lock && $pane.is(':animated') ||
					real && settings.onBefore &&
					// Allow implementors to cancel scrolling
					settings.onBefore(e, elem, $pane, getItems(), pos) === false) return;

				if (settings.stop) {
					// remove all running animations
					$pane.stop(true);
				}

				if (settings.constant) {
					// keep constant velocity
					duration = Math.abs(duration/step * (active - pos));
				}

				$pane.scrollTo(elem, duration, settings);
				
				// in case serialScroll was called on this elemement more than once.
				trigger('notify', pos);
			}

			function next() {
				trigger('next');
			}

			function clear() {
				clearTimeout(timer);
			}

			function getItems() {
				return $(items, pane);
			}
			
			// I'll use the namespace to avoid conflicts
			function trigger(event) {
				$pane.trigger(
					event+NAMESPACE,
					[].slice.call(arguments,1)
				);
			}
			
			function index(elem) {
				// Already a number
				if ($.isNumeric(elem)) {
					return elem;
				}

				var $items = getItems(), i;
				// See if it matches or one of its ancestors
				while((i = $items.index(elem)) === -1 && elem !== pane) {
					elem = elem.parentNode;
				}
				return i;
			}
		});
	};

})(jQuery);
