/*!
 * Waterwheel Carousel
 * Version 1.0
 * http://www.bkosolutions.com
 *
 * Copyright 2010 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 (orientation, options) {

        // override the default options with user defined options
        options = $.extend({}, $.fn.waterwheelCarousel.defaults, options || {});

        // make sure the passed in orientation is valid
        if (orientation != "horizontal" || orientation != "vertical") {
            orientation = "vertical";
        }

        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),
                totalItems:             $(this).children().length,
                containerWidth:         $(this).width(),
                containerHeight:        $(this).height(),
                items:                  [],
                itemDistances:          [],
                waveDistances:          [],
                itemOpacities:          [],
                containerIDTag:         "#"+$(this).attr("id"),
                carouselRotationsLeft:  0,
                currentlyMoving:        false,
                itemsAnimating:         0,
                currentSpeed:           options.speed,
                intervalID:             0,
                rotate_direction:       false
            }
            
            

            // Setup the carousel
            beforeLoaded();
            preload(function () {
                setupDistanceArrays();
                setupCarousel();
                setupStarterRotation();
                auto_rotate();
            });
            
            function auto_rotate()
            {
                data.intervalID = setInterval(function () { for_rotateCarousel(data.rotate_direction, 1); }, 2000);
            }

            // Gets the feature container based on the number
            function getItem(itemNum) {
                return data.itemsContainer.children().eq(itemNum - 1);
            }

            // 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.itemOpacities[0] = 1 * .75;
                // 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.itemOpacities[i] = data.itemOpacities[i-1] * options.opacityDecreaseFactor;
                }
                // 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
             *
             * Note that the calculation for the newTop when orientation is "Vertical" has been 
             * hardcoded to make it work in IE, for OEM specifically
             *
             */
            function setupCarousel() {
                // Fill in a data array with jQuery objects of all the images
                data.items = data.itemsContainer.children('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 (orientation == "horizontal") {
                                newLeft = (data.containerWidth / 2) - ($(this).width() / 2);
                                newTop = options.centerOffset;
                            } else {
                                newLeft = options.centerOffset;
                                newTop = 75; //115; //(data.containerHeight / 2) - ($(this).height() / 2);
                            }
                            $(this)
                                // Apply positioning and layering to the images
                                .css({
                                    left: newLeft,
                                    top: newTop,
                                    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,
                                    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() {
                // Get the center item and collections of the items that will be
                // to the left and to the right
                var $centerItem = data.itemsContainer.children('img').eq(options.startingItem - 1);
                var $previousItems = $centerItem.prevAll();
                var $nextItems = $centerItem.nextAll();

                // We will be rotating the carousel, so we set the animation queue to one
                data.carouselRotationsLeft = 1;

                var counter;
                counter = 1;
                // Move all the left side items to their proper positions
                $previousItems.each(function () {
                    for (i = 0; i < counter; i++) {
                        moveItem($(this),false);
                    }
                    counter++;
                });

                counter = 1;
                // Move all the right side items to their proper positions
                $nextItems.each(function () {
                    for (i = 0; i < counter; i++) {
                        moveItem($(this),true);
                    }
                    counter++;
                });
            }

            function setupSlider() {
                $("#slider").slider({
                    min:        1,
                    max:        data.items.length,
                    step:       1,
                    value:      options.startingItem,
                    animate:    true,
                    stop:      function () {
                        stopAnimations();
                        data.currentlyMoving = false;
                        var oldValue = $('#slider').slider('value');
                        var positionOfItem = data.items[oldValue-1].data().currentPosition;

                        if (positionOfItem < 0) {
                            rotateCarousel(true, Math.abs(positionOfItem));
                        } else if (positionOfItem > 0) {
                            rotateCarousel(false, positionOfItem);
                        }

                    }
                });

            }

            /**
             * 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);

                /** CALCULATE THE NEW WAVE SEPARATION OF THE ITEM **/
                    var waveSeparation = 0, centeringNumber
                    // number to center item on horizon (vertical or horizontal)
                    if (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 (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 (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('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('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, trigger the custom event
                if (newPosition == 0) {
                    options.movedToCenter($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;
                    }
                }
            }

            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;
                // Change direction if trying to move to right and already at right-most item
                if (direction == true && data.items[0].data().currentPosition == 0)
				{
					data.rotate_direction = false;
                    return false;
				}
                // Change direction if trying to move to left and already at left-most item
                if (direction == false && data.items[data.totalItems-1].data().currentPosition == 0)
				{
                   data.rotate_direction = true;
                   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 currrentPosition = $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 (currrentPosition >= ((options.flankingItems*-1)-1) && currrentPosition <= (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 {
                            if (direction == true) {
                                $item.data('currentPosition',currrentPosition+1);
                            } else {
                                $item.data('currentPosition',currrentPosition-1);
                            }
                        }
                    }
                }
            }
            
            /**
             * Function called to automate rotation of the carousel 
             * in the given direciton. Will check to make sure the carousel should
             * be able to move, and then adjust speed and move items
             */
            function for_rotateCarousel(direction, rotations) {

                // Check to see that a rotation is allowed 
                // (this will also reverse rotation when needed)
                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 currrentPosition = $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 (currrentPosition >= ((options.flankingItems*-1)-1) && currrentPosition <= (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 {

                            if (direction == true) {
                                $item.data('currentPosition',currrentPosition+1);
                            } else {
                                $item.data('currentPosition',currrentPosition-1);
                            }
					//change direction next time
					if(data.items[data.totalItems-1].data().currentPosition == 1 && data.rotate_direction == false)
                            data.rotate_direction = true;
					if(data.items[0].data().currentPosition == 0 && data.rotate_direction == true)
                            data.rotate_direction = false;
                        }		
                    }
                } 
            }

            /**
             * 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
             */
            $('img',data.containerIDTag).live("click", function () {
            
                clearInterval(data.intervalID);
            
                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));
                }
                // resume auto rotation
				auto_rotate();
            });

            $('#left').click(function () {
                rotateCarousel(false,1);
            });

            $('#right').click(function () {
                rotateCarousel(true,1);
            });

        });

    };

    $.fn.waterwheelCarousel.defaults = {
        startingItem:               1,      // item to place in the center at the start
        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
    };

})(jQuery);
