/**
 *	CentredScroller: A jQuery plugin for creating a scrollable slideshow that loops
 *	The currently active slide is always centred
 *
 *	Dependencies: jquery-1.4.2+
 *
 *	@project	ca.thomasjbradley.applications.centredscroller
 *	@author		Thomas J Bradley <hey@thomasjbradley.ca>
 *	@link		http://thomasjbradley.ca/labs/centred-scroller
 *	@link		http://github.com/thomasjbradley/centred-scroller
 *	@copyright	Copyright MMX–, Thomas J Bradley
 *	@license	New BSD License
 *	@version	1.0.0
 */

/**
 *	Usage:
 *		$('#slideshow').centredScroller();
 *
 *	@package	src
 */
(function($){

function CentredScroller(selector, options)
{
	/**
	 *	Reference to the object for use in public methods
	 *
	 *	@access	private
	 *	@var	object
	 */
	var self = this;

	/**
	 *	Holds the merged default settings and user passed settings
	 *
	 *	@access	private
	 *	@var	object
	 */
	var settings = $.extend({}, $.fn.centredScroller.defaults, options);

	/**
	 *	The current context, as passed by jQuery, of selected items
	 *
	 *	@access	private
	 *	@var	object
	 */
	var $context = $(selector);

	/**
	 *	The index of the currently displayed photo
	 *
	 *	@access	private
	 *	@var	int
	 */
	var currentIndex = 0;

	/**
	 *	A clone of the slides for moving around in the slideshow
	 *
	 *	@access	private
	 *	@var	object
	 */
	var $slides = $getSlideGroup().children().clone();

	/**
	 *	Holds references to all the plugin's timeouts
	 *
	 *	@access	private
	 *	@var	object
	 */
	var timeouts = {'transitionend': null, 'resize': null, 'autoadvance': null};

	/**
	 *	Holds references to all the plugin's callbacks
	 *
	 *	@access	private
	 *	@var	object
	 */
	var callbacks = {'next': null, 'prev': null, 'display': null, 'onautoadvanceafter': null, 'onautoadvancebefore': null};

	/**
	 *	On window resize, adjust the centre position of the slideshow
	 *	Also re-align all the slides
	 */
	$(window).bind('resize', function()
	{
		removeTransitions();
		clearTimeout(timeouts.resize);
		timeouts.resize = setTimeout(transitionEnd, 10);
	});

	tagSlides();
	$context.prepend('<div style="height:0; position:absolute; top:-2px; width:100%;"><div id="centredScroller-left-measure" style="width:' + getItemWidth() + 'px; margin:0 auto;"></div></div>')
	displaySlides();
	addTransitions();
	startAutoAdvance(settings.autoAdvance);

	/**
	 *	Public methods
	 */
	$.extend(self,
	{
		/**
		 *	@access	public
		 *	@return	int	The index of the current slide
		 */
		getIndex: function()
		{
			return currentIndex;
		},

		/**
		 *	Slide to a specific slide in the deck
		 *	Will take the shortest path to get there
		 *
		 *	@access	public
		 *	@return	void
		 */
		display: function(index, callback)
		{
			stopAutoAdvance();
			display(index, callback);
			timeouts.autoadvance = setTimeout(startAutoAdvance, settings.interactionDelay);
		},

		/**
		 *	Advance to the next slide in the deck
		 *
		 *	@access	public
		 *	@return	void
		 */
		next: function(callback)
		{
			stopAutoAdvance();
			next(callback);
			timeouts.autoadvance = setTimeout(startAutoAdvance, settings.interactionDelay);
		},

		/**
		 *	Slide to the previous slide in the deck
		 *
		 *	@access	public
		 *	@return	void
		 */
		prev: function(callback)
		{
			stopAutoAdvance();
			prev(callback);
			timeouts.autoadvance = setTimeout(startAutoAdvance, settings.interactionDelay);
		},

		/**
		 *	Enables the timer to advance the slides automatically
		 *
		 *	@access	public
		 *	@param	int	time	OPTIONAL; the time in milliseconds to advance to the next slide
		 *	@return	void
		 */
		startAutoAdvance: function(time)
		{
			startAutoAdvance(time);
		},

		/**
		 *	Disables the time for automatically advancing the slides
		 *
		 *	@access	public
		 *	@return	void
		 */
		stopAutoAdvance: function()
		{
			stopAutoAdvance();
		},

		/**
		 *	Store a callback for just before the auto advance occurs
		 *
		 *	@access	public
		 *	@param	function	callback
		 *	@return	void
		 */
		onAutoAdvanceBefore: function(callback)
		{
			callbacks.onautoadvancebefore = null;

			if(jQuery.isFunction(callback))
			{
				callbacks.onautoadvancebefore = callback;
			}
		},

		/**
		 *	Store a callback for after the auto advanced slide animation has completed
		 *
		 *	@access	public
		 *	@param	function	callback
		 *	@return	void
		 */
		onAutoAdvanceAfter: function(callback)
		{
			callbacks.onautoadvanceafter = null;

			if(jQuery.isFunction(callback))
			{
				callbacks.onautoadvanceafter = callback;
			}
		},

		/**
		 *	Whether the browser supports Css transitions or not
		 *
		 *	@access	public
		 *	@return	bool
		 */
		cssTransitionsAvailable: function()
		{
			return cssTransitionsAvailable();
		}
	});

	/**
	 *	Adds the data attribute to all the slides to track their original index
	 *
	 *	@access	private
	 *	@return	void
	 */
	function tagSlides()
	{
		$slides.each(function(index)
		{
			$(this).attr(settings.dataAttr, index);
			$(this).removeClass(settings.currentClass);
		});
	}

	/**
	 *	Gets the collection of slides off the page, live
	 *	Could not be cached because it is later replace instead of manipulated live
	 *
	 *	@access	private
	 *	@return	object
	 */
	function $getSlideGroup()
	{
		return $context.children(settings.slideGroup);
	}

	/**
	 *	Determines the width of each individual slide
	 *
	 *	@access	private
	 *	@return	int
	 */
	function getItemWidth()
	{
		return $getSlideGroup().children().eq(0).width();
	}

	/**
	 *	Calculates how many slides are required to b visible on the screen
	 *	Adds padding to both sides for when advancing
	 *
	 *	@access	private
	 *	@return	int
	 */
	function getDisplayItemsCount()
	{
		var itemWidth = getItemWidth();
		var sWidth = $context.width();
		var displayItemsCount = Math.ceil(sWidth / itemWidth);
		displayItemsCount += (displayItemsCount % 2 === 0) ? 1 : 0;
		displayItemsCount += 2;

		return displayItemsCount;
	}

	/**
	 *	Replaces all the slides in the group with the minimum number required to display
	 *	Also forces the slides to look as though they loop around
	 *
	 *	@access	private
	 *	@return	void
	 */
	function displaySlides()
	{
		var itemWidth = getItemWidth();
		var margin = $('#centredScroller-left-measure').offset().left - $context.offset().left;
		var displayItemsCount = getDisplayItemsCount();
		var extra = Math.floor(displayItemsCount / 2);
		var prev = getPrevIndex(currentIndex);
		var next = getNextIndex(currentIndex);

		var $itemsClone = $getSlideGroup().clone();
		$itemsClone.children().remove();
		$itemsClone.append($slides.eq(prev).clone());
		$itemsClone.append($slides.eq(currentIndex).clone());
		$itemsClone.append($slides.eq(next).clone());

		for(var i=0; i<extra-1; i++)
		{
			prev = getPrevIndex(prev);
			$itemsClone.prepend($slides.eq(prev).clone());
		}

		for(var j=0; j<extra-1; j++)
		{
			next = getNextIndex(next);
			$itemsClone.append($slides.eq(next).clone());
		}

		$('[' + settings.dataAttr + '=' + currentIndex + ']', $itemsClone).addClass(settings.currentClass);
		$itemsClone.css({
			'width': itemWidth * displayItemsCount,
			'left': margin - itemWidth * extra
		});

		$getSlideGroup().replaceWith($itemsClone);
	}

	/**
	 *	Using either transitions or plain Javascript animates the slide to a new left coordinate
	 *
	 *	@access	private
	 *	@return	void
	 */
	function animateTo(newLeft, callbackIndex)
	{
		addTransitions();

		// Fixes some weird bug related to the movement
		var bugFixCurrentLeft = $getSlideGroup().position().left;

		if(cssTransitionsAvailable())
		{
			$getSlideGroup().css({'left': newLeft});
			timeouts.transitionend = setTimeout(function(){ transitionEnd(callbackIndex); }, settings.duration);
		}
		else
		{
			$getSlideGroup().animate({'left': newLeft}, settings.duration, 'swing', function()
			{
				transitionEnd(callbackIndex);
			});
		}
	}

	/**
	 *	Triggered after the slide animation has completed
	 *	Re-displays the slides to the corret amount and calls the callback
	 *
	 *	@access	private
	 *	@return	void
	 */
	function transitionEnd(callbackIndex)
	{
		clearTimeout(timeouts.transitionend);
		timeouts.transitionend = null;
		removeTransitions();
		displaySlides();

		if(jQuery.isFunction(callbacks[callbackIndex]))
		{
			callbacks[callbackIndex].apply(self);
		}
	}

	/**
	 *	Adds the required Css transitions to the slide elements
	 *
	 *	@access	private
	 *	@return	void
	 */
	function addTransitions()
	{
		if(cssTransitionsAvailable())
		{
			$getSlideGroup().css({
				'-moz-transition': 'left ' + settings.duration + 'ms ease-in-out',
				'-o-transition': 'left ' + settings.duration + 'ms ease-in-out',
				'-webkit-transition': 'left ' + settings.duration + 'ms ease-in-out',
				'transition': 'left ' + settings.duration + 'ms ease-in-out'
			});
		}
	}

	/**
	 *	Removes the slide transitions
	 *
	 *	@access	private
	 *	@return	void
	 */
	function removeTransitions()
	{
		$getSlideGroup().css({
			'-moz-transition': 'left 0s linear',
			'-o-transition': 'left 0s linear',
			'-webkit-transition': 'left 0s linear',
			'transition': 'left 0s linear'
		});
	}

	/**
	 *	Using feature detection, determines if Css transitions are available
	 *
	 *	@access	private
	 *	@return	bool
	 */
	function cssTransitionsAvailable()
	{
		var	s = document.createElement('slideshow'),
		s_style = s.style;

		if(
			typeof s_style.transitionProperty !== 'undefined'
			|| typeof s_style.WebkitTransitionProperty !== 'undefined'
			|| typeof s_style.MozTransitionProperty !== 'undefined'
			|| typeof s_style.OTransitionProperty !== 'undefined'
			|| typeof s_style.msTransitionProperty !== 'undefined'
			|| typeof s_style.KhtmlTransitionProperty !== 'undefined'
		){
			return true;
		}

		return false;
	}

	/**
	 *	Slide to a specific slide in the deck
	 *	Will take the shortest path to get there
	 *
	 *	@access	private
	 *	@return	void
	 */
	function display(index, callback)
	{
		var $slideGroup = $getSlideGroup();

		if(index > $slideGroup.children().length - 1)
		{
			return;
		}

		var $current = $('.' + settings.currentClass, $slideGroup);

		if(parseInt($current.attr(settings.dataAttr), 10) == index)
		{
			return;
		}

		var $visibleSlides = $('[' + settings.dataAttr + '=' + index + ']', $slideGroup);

		if($visibleSlides.length > 0)
		{
			var triggered = false;

			/**
			 *	If the slide is the next or prev just trigger those methods
			 */
			$visibleSlides.each(function()
			{
				if($(this).next().hasClass(settings.currentClass))
				{
					prev(callback);
					triggered = true;
					return;
				}
				else if($(this).prev().hasClass(settings.currentClass))
				{
					next(callback);
					triggered = true;
					return;
				}
			});

			if(triggered)
			{
				return;
			}

			/**
			 *	If the slide is in the deck, but not the immediate next or prev
			 *	Pad the appropriate amount then animate to the slide
			 */
			if($visibleSlides.eq(0).index() > $current.index())
			{
				// slide to right
				var displayItemsCount = getDisplayItemsCount();
				var extra = Math.floor(displayItemsCount / 2);
				var padTotal = extra - ($slideGroup.children().length - 1 - $visibleSlides.eq(0).index());
				var itemWidth = getItemWidth();
				var newLeft = $getSlideGroup().position().left - (($visibleSlides.eq(0).index() - $current.index()) * itemWidth);
				padRight(padTotal);
				callbacks.display = callback;
				currentIndex = index;
				animateTo(newLeft, 'display');
			}
			else
			{
				// slide to left
				var displayItemsCount = getDisplayItemsCount();
				var extra = Math.floor(displayItemsCount / 2);
				var padTotal = extra - $visibleSlides.eq(0).index();
				var newLeft = $getSlideGroup().position().left;
				padLeft(padTotal);
				callbacks.display = callback;
				currentIndex = index;
				animateTo(newLeft, 'display');
			}

			return;
		}

		/**
		 *	The slide doesn't exist in the current deck
		 *	Pad enough slides on either side to display the slide we want and the extras
		 */
		if(index > currentIndex)
		{
			// slide to right
			var lastIndex = parseInt($slideGroup.children().last().attr(settings.dataAttr), 10);
			var displayItemsCount = getDisplayItemsCount();
			var extra = Math.floor(displayItemsCount / 2);
			var padTotal = extra + (index - lastIndex);
			var itemWidth = getItemWidth();
			var newLeft = $getSlideGroup().position().left - (padTotal * itemWidth);
			padRight(padTotal);
			callbacks.display = callback;
			currentIndex = index;
			animateTo(newLeft, 'display');
		}
		else
		{
			// slide too left
			var lastIndex = parseInt($slideGroup.children().first().attr(settings.dataAttr), 10);
			var displayItemsCount = getDisplayItemsCount();
			var extra = Math.floor(displayItemsCount / 2);
			var padTotal = extra + (lastIndex - index);
			var newLeft = $getSlideGroup().position().left;
			padLeft(padTotal);
			callbacks.display = callback;
			currentIndex = index;
			animateTo(newLeft, 'display');
		}
	}

	/**
	 *	Advance to the next slide in the deck
	 *
	 *	@access	private
	 *	@return	void
	 */
	function next(callback)
	{
		var itemWidth = getItemWidth();
		var newLeft = $getSlideGroup().position().left - itemWidth;
		callbacks.next = callback;
		currentIndex = getNextIndex(currentIndex);
		animateTo(newLeft, 'next');
	}

	/**
	 *	Slide to the previous slide in the deck
	 *
	 *	@access	private
	 *	@return	void
	 */
	function prev(callback)
	{
		var itemWidth = getItemWidth();
		var newLeft = $getSlideGroup().position().left + itemWidth;
		callbacks.prev = callback;
		currentIndex = getPrevIndex(currentIndex);
		animateTo(newLeft, 'prev');
	}

	/**
	 *	Called from the auto advance timer, and triggers the next method
	 *
	 *	@access	private
	 *	@return	void
	 */
	function autoAdvance()
	{
		if(jQuery.isFunction(callbacks.onautoadvancebefore))
		{
			callbacks.onautoadvancebefore.apply(self);
		}

		var itemWidth = getItemWidth();
		var newLeft = $getSlideGroup().position().left - itemWidth;
		currentIndex = getNextIndex(currentIndex);
		animateTo(newLeft, 'onautoadvanceafter');
	}

	/*
		 *	Enables the timer to advance the slides automatically
		*
		*	@access	public
		*	@param	int	time	OPTIONAL; the time in milliseconds to advance to the next slide
		*	@return	void
		*/
	function startAutoAdvance(time)
	{
		clearInterval(timeouts.autoadvance);

		var interval = settings.autoAdvance;

		if(typeof time != 'undefined')
		{
			interval = settings.autoAdvance;
		}

		if(interval > 99)
		{
			timeouts.autoadvance = setInterval(autoAdvance, settings.autoAdvance);
		}
	}

	/**
	 *	Disables the time for automatically advancing the slides
	 *
	 *	@access	private
	 *	@return	void
	 */
	function stopAutoAdvance()
	{
		clearInterval(timeouts.autoadvance);
		timeouts.autoadvance = null;
	}

	/**
	 *	Determines the index of the next slide in the deck
	 *	Loops around to the beginning if required
	 *
	 *	@access	private
	 *	@param	int	index	The current index
	 *	@return	int
	 */
	function getNextIndex(index)
	{
		return (index + 1 >= $slides.length) ? 0 : index + 1;
	}

	/**
	 *	Determines the index of the prev slide in the deck
	 *	Loops around to the end if required
	 *
	 *	@access	private
	 *	@param	int	index	The current index
	 *	@return	int
	 */
	function getPrevIndex(index)
	{
		return (index - 1 < 0) ? $slides.length - 1 : index - 1;
	}

	/**
	 *	Adds the slides to the right of the deck
	 *
	 *	@access	private
	 *	@param	int	total	How many slides to pad on the right
	 *	@return	void
	 */
	function padRight(total)
	{
		if(total <= 0)
		{
			return;
		}

		var $slideGroup = $getSlideGroup();
		var itemWidth = getItemWidth();
		var lastIndex = parseInt($slideGroup.children().last().attr(settings.dataAttr), 10);
		var nextIndex = getNextIndex(lastIndex);

		for(var i=0; i<total; i++)
		{
			$slideGroup.append($slides.eq(nextIndex).clone());
			nextIndex = getNextIndex(nextIndex);
		}

		$slideGroup.css({'width': $slideGroup.width() + total * itemWidth});
	}

	/**
	 *	Adds the slides to the left of the deck
	 *
	 *	@access	private
	 *	@param	int	total	How many slides to pad on the right
	 *	@return	void
	 */
	function padLeft(total)
	{
		if(total <= 0)
		{
			return;
		}

		var $itemsClone = $getSlideGroup().clone();
		var oldLeft = $getSlideGroup().position().left;
		var itemWidth = getItemWidth();
		var lastIndex = parseInt($itemsClone.children().first().attr(settings.dataAttr), 10);
		var prevIndex = getPrevIndex(lastIndex);

		for(var i=0; i<total; i++)
		{
			$itemsClone.prepend($slides.eq(prevIndex).clone());
			prevIndex = getPrevIndex(prevIndex);
		}

		$itemsClone.css({
			'width': itemWidth * $itemsClone.children().length,
			'left': oldLeft - itemWidth * total
		});

		$getSlideGroup().replaceWith($itemsClone);
	}
}

/**
 *	Create the plugin
 *	Returns an Api which can be used to call specific methods
 *
 *	@param	object	options	The options array
 *
 *	@return	object	The Api for controlling the instance
 */
$.fn.centredScroller = function(options)
{
	var api = null;

	this.each(function(){
		api = new CentredScroller(this, options);
	});

	return api;
};

/**
 *	Expose the defaults so they can be overwritten for multiple instances
 *
 *	@var	 object
 */
$.fn.centredScroller.defaults = {
	autoAdvance: 0,
	duration: 800,
	interactionDelay: 5000,
	currentClass: 'current',
	slideGroup: '.slides',
	dataAttr: 'data-centredscroller-index'
};

})(jQuery);

