/**
 * jQuery Carousel plugin with with additional action trigger and event handler functions.
 * 
 * Takes the wrapper of an unordered list of items and allows us to scroll through items automatically or manually.
 * Allows automatic creation of navigation and play/pause controllers and provides several hooks to event triggering
 * and detection
 * 
 * @author		Alex Kaye
 * 
 */
 /*global jQuery, Number, clearInterval, setInterval */
(function($){
	// Global carousel config
	var carouselConfig = {
		actionPrefix	: 'carouselAction',
		eventPrefix		: 'carouselEvent'	
	};
	
	$.fn.carousel = function(config) {
		// Default options
		var defaults = {
			/**
			 * Index of banner to show initially (starts from 1)
			 */
			initial				: 1,
			/**
			 * Animation type to use between items ('slide', 'fade' or 'none')
			 */
			transition			: 'slide',
			/**
			 * Easing method to use (if transition is set to 'slide')
			 */
			easing				: 'linear',
			/**
			 * Direction of slide ('ltr', 'rtl', 'ttb', 'btt' if transition is set to slide)
			 */
			direction			: 'ltr',
			/**
			 * Transition speed
			 */
			speed				: 500,
			/**
			 * Width of carousel wrapper ('auto' will automatically detect the size)
			 */
			width				: 'auto',
			/**
			 * Width of carousel wrapper ('auto' will automatically detect the size)
			 */			
			height				: 'auto',
			/**
			 * Whether to continuously loop through items (if autoScroll is on)
			 */
			loop				: true,
			/**
			 * Whether to allow automatic scrolling through items (shows play/pause control)
			 */
			autoScroll			: true,
			/**
			 * Whether to begin automatically scrolling through items (if autoScroll is on)
			 */
			autoScrollStart		: true,
			/**
			 * Delay between items when autoscrolling (if autoScroll is on)
			 */
			autoScrollDelay		: 3000,
			/**
			 * Whether to pause autoscrolling when the user clicks another navigation item (if autoScroll is on)
			 */
			pauseOnNavigate		: false,
			/**
			 * Whether to show navigation links (if autoScroll is on)
			 */
			showNav				: true,
			/**
			 * Whether to show play/pause control (if autoScroll is on)
			 */
			showControl			: true,
			/**
			 * Callback fired when the carousel structure (including navigation and controls) has been created
			 */
			onLoad				: function(){},
			/**
			 * Callback fired when the transition from one item to another is about to begin
			 */
			onTransition		: function(newIndex, oldIndex){},
			/**
			 * Callback fired when the current item changes and the transition is complete
			 */
			onChange			: function(newIndex, oldIndex){},
			/**
			 * Callback fired when the autoScrollDelay time has elapsed for the last item (if autoScroll is on)
			 */
			onComplete			: function(){},
			/**
			 * Callback fired when autoscrolling is about to take place
			 */
			onAutoScroll		: function(){},
			/**
			 * Callback fired immediately after the user navigates to another item
			 */
			onNavigate			: function(newIndex, oldIndex){},
			/**
			 * Callback fired when autoscrolling is played (not fired initally, if autoscroll is on)
			 */
			onPlay				: function(){},
			/**
			 * Callback fired when autoscrolling is paused (not fired initially, if autoscroll is on)
			 */
			onPause				: function(){},
			/**
			 * Callback fired when any link inside our carousel is clicked. Callback can return false to avoidd
			 * link action firing.
			 */
			onClick				: function(){}
		};
		// Overwrite our default settikngs with any custom ones
		var cfg = $.extend(defaults, config);
		// Iterate over each object (wrapper) and return it for chainability
		return this.each(function() {
			// Store current wrapper so we can attach properties and methods to this instance
			var c = $(this);
			// Cache the carousel list and items first
			c.cache = {
				list		: $(this).children('ul.carousel_items', this),
				items		: $(this).find('ul.carousel_items li', this)
			};
			
			/**
			 * Builds our navigation structure over the top of our items including autoscroll controlls if necessary
			 */
			c.buildNav = function() {
				if(cfg.showNav || cfg.showControl) {
					var nav = '<div class="carousel_controls" style="z-index:4">';
					if(cfg.showNav) {
						nav += '<ul class="carousel_nav">';
						var liClass = '';
						for(var i = 1; i <= c.cache.items.length; i++) {
							liClass = i == cfg.initial ? ' class="current"' : '';
							nav += '<li' + liClass + '><a href="#banner' + i + '">Banner ' + i + '</a></li>';
						}
						nav += '</ul>';						
					}
					if(cfg.showControl) {
						// Add the play/pause button if necessary
						if(cfg.autoScroll) {
							var aClass = cfg.autoScrollStart ? ' carousel_autoscroll_active' : '';
							nav += '<a href="#" class="carousel_autoscroll' + aClass + '" title="Toggle play/pause">Toggle play/pause</a>';
						}						
					}
					nav += '</div>';
					c.cache.list.after(nav);

					// Add the nav list to our cache
					if(cfg.showNav) {
						c.cache.navList = c.find('ul.carousel_nav');
						c.cache.navItems = c.find('ul.carousel_nav li');
						c.cache.navItemsLinks = c.find('ul.carousel_nav li a');
					}
					if(cfg.autoScroll && cfg.showControl) {
						c.cache.autoScrollControl = c.find('.carousel_autoscroll');
					}					
				}
			};
			
			/**
			 * Calculates the size of our caruosel if height and width config options are set to 'auto'
			 */
			c.calculateSize = function() {
				cfg.width = cfg.width == 'auto' ? c.width() : cfg.width;
				cfg.height = cfg.height == 'auto' ? c.height() : cfg.height;
			};
			
			/**
			 * Positions the items initially depending on the transition set in config so our showItem method
			 * can perform transitions correctly from then onwards.
			 */
			c.positionItems = function() {		
				// The position of our items depends on the transition type
				if(cfg.transition == 'slide') {
					// We are sliding items so we need them all in a line
					c.cache.items.show();
					// Now position the items according to the slide direction
					switch(cfg.direction) {
						case 'ltr':
							c.cache.items.each(function(i) {
								$(this).css({
									'top'	:	'0',
									'left'	:	(i * cfg.width) + 'px'
								});
							});
							break;
						case 'rtl':
							var total = c.getTotal();
							c.cache.items.each(function(i) {
								$(this).css({
									'top'	:	'0',
									'left'	:	((total * cfg.width) - ((i + 1) * cfg.width)) + 'px'
								});
							});
							break;
						case 'ttb':
							c.cache.items.each(function(i) {
								$(this).css({
									'top'	:	(i * cfg.height) + 'px',
									'left'	:	'0'
								});
							});
							break;
						case 'btt':
							total = c.getTotal();
							c.cache.items.each(function(i) {
								$(this).css({
									'top'	:	((total * cfg.height) - ((i + 1) * cfg.height)) + 'px',
									'left'	:	'0'
								});
							});
							break;
					}				
				} else {
					c.cache.items.css({
						top		: '0',
						left	: '0'
					}).hide();
				}
				c.showItem(c.getCurrent(), false);
			};
			
			/**
			 * Shows the given banner using the transition defined in config. Uses animate variables as boolean to
			 * determine whether to perform the transition or not (false when showing the initial item) 
			 */
			c.showItem = function(index, animate) {
				var css;
				var total;
				var eq;
				index = index ? Number(index) : 1;
				animate = animate ? animate : false;
				if(animate) {
					c.callback(cfg.onTransition, 'transition', [index, c.getCurrent()]);
				}
				if(cfg.transition == 'slide') {
					switch(cfg.direction) {
						case 'ltr':
							css = {
								left	: '-' + ((index - 1) * cfg.width) + 'px'
							};
							break;					
						case 'rtl':
							total = c.getTotal();
							css = {
								left	: '-' + ((total * cfg.width) - (index * cfg.width)) + 'px'
							};
							break;
						case 'ttb':
							css = {
								top	: '-' + ((index - 1) * cfg.height) + 'px'
							};
							break;
						case 'btt':
							total = c.getTotal();
							css = {
								top	: '-' + ((total * cfg.height) - (index * cfg.height)) + 'px'
							};
							break;
					}
					if(animate) {
						c.cache.list.animate(css, cfg.speed, cfg.easing, function() {
							c.callback(cfg.onChange, 'change', [index, c.getCurrent()]);
							c.cache.current = index;
						});					
					} else {
						c.cache.list.animate(css, 0);
						c.cache.current = index;
					}
				} else {
					if(animate) {
						// Promote the current item to z-index 3
						c.cache.items.eq(c.getCurrent() - 1).css({
							zIndex	: '3'
						});
					}
					// Promote the new item to z-index 2
					c.cache.items.eq(index - 1).show().css({
						zIndex	: '2'
					});
					if(animate) {
						// Fade out the current item
						c.cache.items.eq(c.getCurrent() - 1).fadeOut(cfg.speed, function() {
							// Now it has been faded out reset z-index to 1
							$(this).css({
								zIndex	: '1'
							});
							c.callback(cfg.onChange, 'change', [index, c.getCurrent()]);
							c.cache.current = index;
						});
					} else {
						c.cache.current = index;
					}
				}
				if(cfg.showNav) {
					c.cache.navItems.removeClass('current');
					eq = Number(index) - 1;
					c.cache.navItems.eq(eq).addClass('current');					
				}
			};
			
			/**
			 * Gets the current active item index (starts wiith 1)
			 */
			c.getCurrent = function() {
				return c.cache.current;
			};
			
			/**
			 * Returns the total number of items in the carousel
			 */
			c.getTotal = function() {
				return c.cache.items.length;
			};
			
			/**
			 * Checks whether autoscrolling is currently active
			 */
			c.autoScrollIsActive = function() {
				return c.cache.autoScrollIsActive;
			};
			
			/**
			 * Assigns any necessary click events to our controls
			 */
			c.addClickEvents = function() {
				if(cfg.showNav) {
					c.cache.navItemsLinks.click(function() {
						// Get the index of the item
						var href = $(this).attr('href');
						var index = href.substring(href.indexOf('#') + 7);
						c.callback(cfg.onNavigate, 'navigate', [index, c.getCurrent()]);
						if(c.getCurrent() != index) {
							
							c.showItem(index,  true);
							if(cfg.autoScroll) {
								if(cfg.pauseOnNavigate) {
									c.stopAutoScroll();
								} else {
									// Reset our timer 
									c.startAutoScroll();
								}
							}
						}
						
						return false;
					});					
				}
				c.cache.items.find('a').click(function() {
					// Get the link target
					var href = $(this).attr('href');
					// Get the index of the item in which the link was clicked
					var index = $(this).parents('.carousel_items li:first').index();
					// Fire our callback
					return c.callback(cfg.onClick, 'click', [index, href]);
				});
				if(cfg.autoScroll && cfg.showControl) {
					c.cache.autoScrollControl.click(function() {
						if(c.autoScrollIsActive()) {
							c.callback(cfg.onPause, 'pause', [c.getCurrent()]);
							c.stopAutoScroll();
						} else {
							c.callback(cfg.onPlay, 'play', [c.getCurrent()]);
							c.startAutoScroll();
						}
						return false;
					});					
				}
				// At this point our carousel is completely loaded so fire our callback
				c.callback(cfg.onLoad, 'load');
			};
			
			/**
			 * Starts our autoscroll which creates a transition from one item to another and loops on completion
			 * if necessary. Adds a CSS class to our autoscroll controller
			 */
			c.startAutoScroll = function() {
				var nextIndex;
				clearInterval(c.cache.timer);
				c.cache.timer = setInterval(function() {
					if(c.getCurrent() == c.getTotal()) {
						c.callback(cfg.onComplete, 'complete');
						if(!cfg.loop) {
							c.stopAutoScroll();
							return;	
						}
						nextIndex = 1;
					} else {
						nextIndex = c.getCurrent() + 1;
					}
					c.callback(cfg.onAutoScroll, 'autoScroll', [nextIndex, c.getCurrent()]);
					c.showItem(nextIndex, true);
				}, cfg.autoScrollDelay + cfg.speed);
				if(cfg.showControl) {
					c.cache.autoScrollControl.addClass('carousel_autoscroll_active');					
				}
				c.cache.autoScrollIsActive = true;
			};
			
			/**
			 * Stops our autoscroll and adds a css class to our autoscroll controller
			 */
			c.stopAutoScroll = function() {
				clearInterval(c.cache.timer);
				if(cfg.showControl) {
					c.cache.autoScrollControl.removeClass('carousel_autoscroll_active');
				}
				c.cache.autoScrollIsActive = false;
			};
			
			/**
			 * Fires one of our callback functions if the function has been created
			 */
			c.callback = function(type, eventType, args) {
				var returnVal;
				args = args ? args : [];
				if($.isFunction(type)) {
					returnVal = type.apply(c, args);
				}
				// Trigger the global jquery event with our prefix to avoid clashes
				c.trigger(carouselConfig.eventPrefix + eventType, args);
				return returnVal;
			};
			
			c.bindActions = function() {
				c.bind(carouselConfig.actionPrefix + 'getCurrent', function() {
					return c.getCurrent();
				}).bind(carouselConfig.actionPrefix + 'setCurrent', function(event, data) {
					if(c.getTotal() >= data) {
						c.showItem(data, true);
						c.stopAutoScroll();						
					}
					return c;
				}).bind(carouselConfig.actionPrefix + 'getTotal', function() {
					return c.getTotal();
				}).bind(carouselConfig.actionPrefix + 'play', function() {
					if(!c.autoScrollIsActive()) {
						c.callback(cfg.onPlay, 'play', [c.getCurrent()]);
						c.startAutoScroll();
					}
					return c;
				}).bind(carouselConfig.actionPrefix + 'pause', function() {
					if(c.autoScrollIsActive()) {
						c.callback(cfg.onPause, 'pause', [c.getCurrent()]);
						c.stopAutoScroll();
					}
					return c;
				});
			};
			
			// Work out our height width
			c.calculateSize();
			// Add our navigation
			c.buildNav();
			// Position our items in carousel format
			c.positionItems();
			// Add click events to navigation
			c.addClickEvents();
			// Scroll automatically if necessary
			if(cfg.autoScroll && cfg.autoScrollStart) {
				c.startAutoScroll();
			} else {
				c.cache.autoScrollIsActive = false;
			}
			// Bind actions to our select box so we can perform actions outside of the scope of this function
			c.bindActions();
		});
	};
	// Universal method to allow us call actions on specific carousel instances
	$.fn.carouselAction = function(eventName, data) {
		var returnValue = null;
		var returnObject = true;
		this.each(function() {
			// If we are retrieving the value then simply return it
			if(eventName == 'getCurrent' || 'getTotal') {
				returnValue = $(this).triggerHandler(carouselConfig.actionPrefix + eventName, [data]);
				returnObject = false;
				return false;
			}
			// Wrap our data in an array due to the way the jQuery bind event picks up params
			$(this).triggerHandler(carouselConfig.actionPrefix + eventName, [data]);
		});
		// Determine whether to return a value of the jquery object
		if(returnObject) {
			return this;
		} else {
			return returnValue;
		}
	};
	// Universal method to allow us to create callbacks on our custom events for specific jquery objects
	$.fn.carouselEvent = function(eventName, callback) {
		this.each(function() {
			// Bind our carousel event to the element so we can fire our callback when the event is triggered
			$(this).bind(carouselConfig.eventPrefix + eventName, function(event) {
				if($.isFunction(callback)) {
					// Get any extra args passed
					 var args = [];
					// Copy all other arguments we want to "pass through"
				    for(var i = 1; i < arguments.length; i++) {
				        args.push(arguments[i]);
				    }
				    callback.apply(this, args);
				}
			});
		});
		return this;
	};		
})(jQuery);
