/*
 * Waterwheel Carousel
 * Version 1.2.3
 * http://www.bkosborne.com
 *
 * Copyright 2011 Brian Osborne
 * Licensed under GPL version 3
 * http://www.gnu.org/licenses/gpl.txt
 * 
 * Plugin written by Brian Osborne
 * for use with the jQuery JavaScript Framework
 * http://www.jquery.com
 *
 */
(function($) {

  $.fn.waterwheelCarousel = function (options) {

	// override the default options with user defined options
	options = $.extend({}, $.fn.waterwheelCarousel.defaults, options || {});

	return $(this).each(function () {

	  /* These are univeral values that are used throughout the plugin. Do not modify them
	   * unless you know what you're doing. Most of them feed off the options
	   * so most customization can be achieved by modifying the options values */
	  var data = {
		itemsContainer:         $(this).find(".carousel-images"),
		totalItems:             $(this).find(".carousel-images img").length,
		containerWidth:         $(this).width(),
		containerHeight:        $(this).height(),
		currentCenterItem:      null,
		items:                  [],
		itemDistances:          [],
		waveDistances:          [],
		itemWidths:             [],
		itemHeights:            [],
		itemOpacities:          [],
		carouselRotationsLeft:  0,
		currentlyMoving:        false,
		itemsAnimating:         0,
		currentSpeed:           options.speed,
		intervalTimer:          null
	  }

	  // Setup the carousel
	  beforeLoaded();
	  // Preload the images. Once they are preloaded, the passed in function
	  // will be called and the carousel will be setup
	  preload(function () {
		setupDistanceArrays();
		setupCarousel();
		setupStarterRotation();
		autoPlay();
	  });

	  /**
	   * This function will set the autoplay for the carousel to
	   * automatically rotate it given the time in the options
	   * Can clear the autoplay by passing in true
	   */
	  function autoPlay(stop) {
		// clear interval
		clearInterval(data.intervalTimer);
		
		if (!stop && options.autoPlay != 0) {
		  data.intervalTimer = setInterval(function () {
			(options.autoPlay > 0) ? rotateCarousel(false,1) : rotateCarousel(true, 1);
		  },Math.abs(options.autoPlay));
		}
	  }

	  // get previous feature number - accounts for wrap around
	  function getPreviousNum(num) {
		var newNum = (num == 1) ? null : num--;
		return newNum;
	  }

	  // get next feature number - accounts for wrap around
	  function getNextNum(num) {
		var newNum = (num == data.totalItems) ? null : num++;
		return newNum;
	  }

	  // We want to hide all the images to make sure the viewer doesn't
	  // see them before the carousel moves them to proper positions
	  function beforeLoaded() {
		data.itemsContainer.find("img").hide();
	  }

	  /**
	   * This function will preload all the images in the carousel before
	   * calling the passed in callback function. This is VERY necessary for
	   * the carousel to function properly because it depends heavily on the
	   * width and height of images (which can only be determined post-load)
	   */
	  function preload(callback) {
		var $imageElements = data.itemsContainer.find("img");
		var loadedImages = 0;
		var totalImages = $imageElements.length;

		$imageElements.each(function () {
		  // Attempt to load the images
		  $(this).load(function () {
			// Add to number of images loaded and see if they are all done yet
			loadedImages++;
			if (loadedImages == totalImages) {
			  // All done, perform callback
			  callback();
			}
		  });
		  // The images may already be cached in the browser, in which case they
		  // would have a 'true' complete value and the load callback would never be
		  // fired. This will fire it manually.
		  if (this.complete) {
			$(this).trigger('load');
		  }
		});
	  }

	  /**
	   * This function will setup the various distance arrays that are used in the carousel
	   * These values are calculated beforehand to reduce calculation time while the
	   * carousel is moving
	   */
	  function setupDistanceArrays() {
		// Start each array with the first starting value from the options
		data.itemDistances[0] = options.startingItemSeparation;
		data.waveDistances[0] = options.startingWaveSeparation;
		data.itemWidths[0] = data.itemsContainer.find("img:first").width();
		data.itemHeights[0] = data.itemsContainer.find("img:first").height();
		//data.itemOpacities[0] = 1 * .75;
		data.itemOpacities[0] = 1;
		// Then go thru and calculate the rest of the values all the way up to
		// either edge and beyond 1 (to account for the hidden items)
		for (var i = 1; i < options.flankingItems+1; i++) {
		  data.itemDistances[i] = data.itemDistances[i-1] * options.itemSeparationFactor;
		  data.waveDistances[i] = data.waveDistances[i-1] * options.waveSeparationFactor;
		  data.itemWidths[i] = data.itemWidths[i-1] * options.itemDecreaseFactor;
		  data.itemHeights[i] = data.itemHeights[i-1] * options.itemDecreaseFactor;
		  data.itemOpacities[i] = data.itemOpacities[i-1] * options.opacityDecreaseFactor;
		  // add one more for width and height
		  if (i == options.flankingItems) {
			data.itemWidths[i+1] = data.itemWidths[i] * options.itemDecreaseFactor;
			data.itemHeights[i+1] = data.itemHeights[i] * options.itemDecreaseFactor;
		  }
		}
		// The last opacity should be zero
		data.itemOpacities[data.itemOpacities.length-1] = 0;
	  }

	  /**
	   * This function will perform the necessary steps to setup the carousel and the items
	   * within it. This mostly means positioning the elements properly and setting
	   * their data values
	   */
	  function setupCarousel() {
		// Fill in a data array with jQuery objects of all the images
		data.items = data.itemsContainer.find("img");
		for (var i = 0; i < data.items.length; i++) {
		  data.items[i] = $(data.items[i]);
		}

		data.itemsContainer
		  // Want the container to have relative positioning
		  .css('position','relative')
		  .find("img")
			.each(function (i) {
			  // Put all images in the center default position
			  var newLeft,newTop;			  
			  if (options.orientation == "horizontal") {
				newLeft = (data.containerWidth / 2) - ($(this).width() / 2);
				newTop = options.centerOffset;
			  } else {
				newLeft = options.centerOffset;
				newTop = (data.containerHeight / 2) - ($(this).height() / 2);
			  }			  
			  $(this)
				// Apply positioning and layering to the images
				.css({
				  'left': newLeft,
				  'top': newTop,
				  'visibility': 'visible',
				  'position': 'absolute',
				  'z-index': options.flankingItems+2,
				  'opacity': 1
				})
				// Give each image a data object so it remembers specific data about
				// it's original form
				.data({
				  currentPosition:  0,
				  oldPosition:    0,
				  width:        $(this).width(),
				  owidth:       $(this).width(),
				  height:       $(this).height(),
				  oheight:      $(this).height(),
				  top:        newTop,
				  left:         newLeft,
				  opacity:      1,
				  index:        i
				})
				// The image has been setup... Now we can show it
				.show();
			});
	  }

	  /**
	   * All the items to the left and right of the center item need to be
	   * animated to their starting positions. This function will
	   * figure out what items go where and will animate them there
	   */
	  function setupStarterRotation() {
		// Do we need to calculate the starting item?
		options.startingItem = (options.startingItem == 0) ? Math.round(data.totalItems / 2) : options.startingItem;
	  
		// We will be rotating the carousel, so we set the animation queue to one
		data.carouselRotationsLeft = 1;

		var counter, itemNum, i;
		counter = 1;
		// Move all the left side items to their proper positions
		for (itemNum = options.startingItem - 2; itemNum >= 0; itemNum--) {
		  for (i = 0; i < counter; i++) {
			moveItem(data.items[itemNum],false);
		  }
		  counter++;
		}

		counter = 1;
		// Move all the right side items to their proper positions
		for (itemNum = options.startingItem; itemNum < data.items.length; itemNum++) {
		  for (i = 0; i < counter; i++) {
			moveItem(data.items[itemNum],true);
		  }
		  counter++;
		}
	  }

	  /**
	   * Given the item and position, this function will calculate the new data
	   * for the item. One the calculations are done, it will store that data in
	   * the items data object
	   */
	  function performCalculations($item, newPosition) {

		// Distance to the center
		var oldPosition = $item.data().currentPosition;
		var newDistanceFromCenter = Math.abs(newPosition);

		/** CALCULATE THE NEW WIDTH AND HEIGHT OF THE ITEM **/

		/*
		  var oldWidth = $item.data().width, newWidth = $item.data().owidth;
		  var oldHeight = $item.data().height, newHeight = $item.data().oheight;
		  for (var i = 0; i < newDistanceFromCenter; i++) {
			newWidth = newWidth * options.itemDecreaseFactor;
			newHeight = newHeight * options.itemDecreaseFactor;
		  }
		  var widthDifference = Math.abs(oldWidth - newWidth);
		  var heightDifference = Math.abs(oldHeight - newHeight);
		  */

		  var newWidth = data.itemWidths[Math.abs(newPosition)];
		  var newHeight = data.itemHeights[Math.abs(newPosition)];
		  var widthDifference = Math.abs($item.data().width - newWidth);
		  var heightDifference = Math.abs($item.data().height - newHeight);


		/** CALCULATE THE NEW WAVE SEPARATION OF THE ITEM **/
		  var waveSeparation = 0, centeringNumber
		  // number to center item on horizon (vertical or horizontal)
		  if (options.orientation == "horizontal")
			centeringNumber = heightDifference / 2;
		  else
			centeringNumber = widthDifference / 2;
		  // Item growing
		  if ((newPosition > -1 && (newPosition < oldPosition)) || (newPosition < 1 && (newPosition > oldPosition))) {
			// center item along the horizon
			waveSeparation -= centeringNumber;
			// now add the wave
			waveSeparation += data.waveDistances[Math.abs(newPosition)];
		  // Item shrinking
		  } else if ((newPosition > -1 && (newPosition > oldPosition)) || (newPosition < 1 && (newPosition < oldPosition))) {
			// center item along the horizon
			waveSeparation += centeringNumber;
			// now subtract the wave
			waveSeparation -= data.waveDistances[Math.abs(newPosition) - 1];
		}

		/** CALCULATE THE NEW ITEM SEPARATION OF THE ITEM **/
		  var itemSeparation = 0;
		  // if moving towards the center, the separation value will be different
		  // than if it were moving away from the center
		  if (Math.abs(newPosition) < Math.abs(oldPosition)) {
			itemSeparation = data.itemDistances[Math.abs(newPosition)];
		  // if not moving towards center, just give it normal positioning
		  } else {
			itemSeparation = data.itemDistances[Math.abs(newPosition)-1];
		  }
		  // Need to account for additional size separation only if the item is
		  // on the right side or moving to the center from the right side
		  if (newPosition > 0 || (newPosition == 0 && oldPosition == 1)) {
			if (options.orientation == "horizontal")
			  itemSeparation += widthDifference;
			else
			  itemSeparation += heightDifference;
		  }
		  // We want to separation to be negative if the image is going towards the left
		  if (newPosition < oldPosition) {
			itemSeparation = itemSeparation * -1;
		  }

		/** CALCULATE NEW OPACITY OF THE ITEM **/
		  var newOpacity;
		  if (newPosition == 0) {
			newOpacity = 1;
		  } else {
			newOpacity = data.itemOpacities[Math.abs(newPosition)-1];
		  }

		// Figure out the new top and left values based on the orientation
		var newTop = $item.data().top;
		var newLeft = $item.data().left;
		if (options.orientation == "horizontal") {
		  newTop = $item.data().top + waveSeparation;
		  newLeft = $item.data().left + itemSeparation;
		} else {
		  newTop = $item.data().top + itemSeparation;
		  newLeft = $item.data().left + waveSeparation;
		}

		// Depth will be reverse distance from center
		var newDepth = options.flankingItems + 2 - newDistanceFromCenter;
		// Set calculations
		$item.data('width',newWidth);
		$item.data('height',newHeight);
		$item.data('top',newTop);
		$item.data('left',newLeft);
		$item.data('oldPosition',oldPosition);
		$item.data('currentPosition',newPosition);
		$item.data('depth',newDepth);
		$item.data('opacity',newOpacity);
	  }

	  /**
	   * This function is called when moving an item in the given direction.
	   * It will figure out the new position based on the direction of the carousel
	   * and then obtain the new calcuations for the item and apply them
	   */
	  function moveItem($item, direction) {
		// Get old and new positions
		var oldPosition = $item.data('currentPosition'), newPosition;
		if (direction == false) {
		  newPosition = oldPosition - 1;
		} else {
		  newPosition = oldPosition + 1;
		}		
		// Only want to physically move the item if it is within the boundaries
		// or in the first position just outside either boundary
		if (Math.abs(newPosition) <= options.flankingItems + 1) {
		  // increment number of items animating
		  data.itemsAnimating++;
		  // Obtain the updated data values for the item
		  performCalculations($item, newPosition);
		  // NOTE: After this method is called, the items data object has updated
		  // position values

		  // Change depth of item right away based on its new position
		  $item.css('z-index',$item.data().depth);
		  $item
			// Animate the items to their new position values
			.animate({
			  left: $item.data().left,
			  width: $item.data().width,
			  height: $item.data().height,
			  top: $item.data().top,
			  opacity: $item.data().opacity
			},data.currentSpeed,options.animationEasing, function () {
			  // Animation for the item has completed, call method
			  itemAnimationComplete($item, newPosition, direction);
			});
		// If the item is moving further outside of the boundary, don't move it,
		// just increment its position
		} else if (Math.abs(newPosition) > options.flankingItems) {
		  $item.data('oldPosition',oldPosition);
		  $item.data('currentPosition',newPosition);		  	  
		}

	  }

	  /**
	   * This function is called once an item has finished animating to its
	   * given position. Several different statements are executed here, such as
	   * dealing with the animation queue
	   */
	  function itemAnimationComplete($item, newPosition, direction) {
		// If the item moved to the center position, change the data indicating so
		// We simply need to keep track of it so we can call the moved to center event
		// once we are positive all of the animations are complete
		if (newPosition == 0) {
		  data.currentCenterItem = $item;
		}		
		// Decrement one from the amount of items that are animating
		data.itemsAnimating--;
		// If there are no more items left animating, that means that all the items within
		// the carousel have finished animating
		if (data.itemsAnimating == 0) {
		  // Decrement one from the amount of rotations the carousel has made
		  data.carouselRotationsLeft -= 1;
		  // The carousel has finished rotating and is no longer moving
		  data.currentlyMoving = false;
		  // If there are still rotations left in the queue, rotate the carousel again
		  if (data.carouselRotationsLeft > 0) {
			rotateCarousel(direction, 0);
		  // Otherwise there are no more rotations and...
		  } else {
			// Reset the speed of the carousel to original
			data.currentSpeed = options.speed;
			// Trigger custom 'moved to the center' event
			if (data.currentCenterItem !== null)
			  options.movedToCenter(data.currentCenterItem);
		  }
		}
	  }

	  function stopAnimations() {
		for (var i = 0; i < data.items.length; i++) {
		  data.items[i].stop();
		}
	  }

	  // Short function to determine if a rotation is allowed or not
	  function rotationAllowed(direction) {
		// Deny if currently moving already
		if (data.currentlyMoving == true)
		  return false;
		// Deny if trying to move to right and already at right-most item
		if (direction == true && data.items[0].data().currentPosition == 0)
		  return false;
		// Deny if trying to move to left and already at left-most item
		if (direction == false && data.items[data.totalItems-1].data().currentPosition == 0)
		  return false;

		// Everything is OKAY
		return true;
	  }

	  /**
	   * Function called to rotate the carousel the given number of rotations
	   * in the given direciton. Will check to make sure the carousel should
	   * be able to move, and then adjust speed and move items
	   */
	  function rotateCarousel(direction, rotations) {

		// Check to see that a rotation is allowed
		if (rotationAllowed(direction)) {

		  // Carousel is now moving
		  data.currentlyMoving = true;
		  // Reset items animating to zero
		  data.itemsAnimating = 0;
		  // Add given rotations to queue
		  data.carouselRotationsLeft += rotations;
		  
		  if (options.quickerForFurther == true) {
			  // Figure out how fast the carousel should rotate
			  if (rotations > 1) {
				data.currentSpeed = options.speed / rotations;
			  }
			  // Assure the speed is above the minimum to avoid weird results
			  data.currentSpeed = (data.currentSpeed < 100) ? 100 : data.currentSpeed;
		  } else {
			data.currentSpeed = options.speed;
		  }

		  // Iterate thru each item and move it
		  for (var i = 0; i < data.items.length; i++) {
			var $item = $(data.items[i]);
			var currentPosition = $item.data().currentPosition;
			// Only move items that are within the boundaries of the carousel
			// (but also the first flanking hidden item on either side if there is one)
			if (currentPosition >= ((options.flankingItems*-1)-1) && currentPosition <= (options.flankingItems)+1) {
			  moveItem($item, direction);
			// If the item is not in the boundaries, then that means it is a hidden flank image
			// we don't want to move it, but we want to increment it's position
			} else {
			  $item.data('oldPosition',currentPosition);
			  if (direction == true) {
				$item.data('currentPosition',currentPosition+1);				
			  } else {				
				$item.data('currentPosition',currentPosition-1);				
			  }
			}
		  }
		}
	  }

	  /**
	   * The event handler when an image within the carousel is clicked
	   * This function will rotate the carousel the correct number of rotations
	   * to get the clicked item to the center, or will fire the custom event
	   * the user passed in if the center item is clicked
	   */
	  $(this).find('.carousel-images img').live("click", function () {
		// Remove autoplay
		autoPlay(true);
		
		var itemPosition = $(this).data().currentPosition;
		var rotations = Math.abs(itemPosition);
		if (itemPosition < 0) {
		  rotateCarousel(true, rotations);
		} else if (itemPosition > 0) {
		  rotateCarousel(false, rotations);
		} else {
		  options.clickedCenter($(this));
		}
	  });

	  /**
	   * The user may choose to wrap the images is link tags. If they do this, we need to
	   * make sure that they aren't active for certain situations
	   */
	  $(this).find('.carousel-images a').live("click", function (event) {
		var isCenter = ($(this).find('img').width() == $(this).find('img').data().owidth) ? true : false;
		// should we disable the links?
		if (options.linkHandling == 1 || // turn off all links
			(options.linkHandling == 2 && !isCenter)) // turn off all links except center
		{
		  event.preventDefault();
		  return false;
		}
	  });

	  /**
	   * Event handlers for the optional carousel controls
	   */
	  $(this).find('.carousel-controls .carousel-prev').live('click',function (e) {		
		rotateCarousel(true,1);			
		e.preventDefault();
		return false;
	  });
	  $(this).find('.carousel-controls .carousel-next').live('click',function (e) {
		rotateCarousel(false,1);
		e.preventDefault();
		return false;
	  });

	});

  };

  $.fn.waterwheelCarousel.defaults = {
	startingItem:               0,      // item to place in the center at the start, set to zero to be the middle item
	startingItemSeparation:     150,    // the starting separation distance between each item
	itemSeparationFactor:       .5,     // determines how drastically the item separation decreases
	startingWaveSeparation:     30,     // the starting separation distance for the wave
	waveSeparationFactor:       .75,    // determines how drastically the wave separation decreases
	itemDecreaseFactor:         .8,     // determines how drastically the item's width and height decrease
	opacityDecreaseFactor:      .5,     // determines how drastically the item's opacity decreases
	centerOffset:               40,     // the number of pixels to offset the center item in the carousel
	flankingItems:              4,      // the number of items visible on either side of the center
	speed:                      300,    // speed in milliseconds it will take to rotate from one to the next
	animationEasing:            'linear',// the animation easing when rotating each item
	quickerForFurther:          true,   // set to true to make animations faster when clicking an item that is far away from the center
	movedToCenter:              $.noop, // custom function executed when an item moves to the center
	clickedCenter:              $.noop, // custom function executed when the center item is clicked
	linkHandling:               2,      // 1 to disable all (used for facebox), 2 to disable all but center (to link images out)
	autoPlay:                   0,      // indicate the speed in milliseconds to wait before autorotating. 0 to turn off. Can be negative
	orientation:                'horizontal' // indicate if the carousel should be horizontal or vertical
  };

})(jQuery);
