/** ===========================================================================
 * jCan - a jQuery image carousel
 * Copyright 2008 suprfish <http://suprfish.wordpress.com>
 *
 * Licensed under the MIT License (../MIT-LICENSE.txt)
=========================================================================== **/

var nufn = function() {};

(function()
{
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // Default configuration.
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    var defs = {
        buttons: ['&larr;', '&rarr;'],
        circular: false,
        easing: null,
        fluid: true,
        item: [null, null],
        itemClick: nufn,
        itemHover: [nufn, nufn],
        onjCan: [nufn, nufn],
        onSize: [nufn, nufn],  
        skin: null,
        spacing: 4,
        speed: 'normal',
        step: [1, 1],
        wrap: [null, null],
        vertical: false
    };

    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // jQuery extension.
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~    
    $.fn.jCan = function(cfg)
    {
        return this.each(function()
        { 
            new $.jCan(this, cfg); 
        });
    };

    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // The jCan object.
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    $.jCan = function(e, cfg)
    {
        this.list = $(e);
        this.cfg = $.extend({}, defs, cfg || {});
        
        if ($.isFunction(this.cfg.onjCan[0]))
        	this.cfg.onjCan[0]();
        
        // CSS values that need to be set depend on whether the can is vertical.
        if (this.cfg.vertical) {
            this.wh = ['height', 'width'];
            this.ltrb = ['top', 'bottom'];
        } else {
            this.wh = ['width', 'height'];
            this.ltrb = ['left', 'right'];
        }

        var id = this.list.attr('id');
        var cl = this.cfg.skin;
        
        if (id == null) id = '';
        if (cl == null) cl = '';
        
        // Wrap the list and move the list's ID to the wrap; add the skin also.
        this.list.wrap('<div id="'+ id +'" class="jCan '+ cl +'"></div>');
        this.list.removeAttr('id');
        this.list.removeClass('jCan');
        
        this.wrap = this.list.parent('.jCan');
        this.items = this.list.children('li');
        this.items.remove();
       
        // Adjust the items_size if there are less items than the set spacing.
        this.items_size = this.items.size();
        
        if (this.items_size < this.cfg.spacing)
            this.items_size = this.cfg.spacing;
        
        // Step cannot be greater than half the number of items if circular,
        // for various reasons. Automatically adjusted here...
        if (this.cfg.circular) {
			if (this.cfg.step[0] > (this.items_size * .5))
				this.cfg.step[0] = Math.floor(this.items_size * .5);
			
			if (this.cfg.step[1] > (this.items_size * .5))
				this.cfg.step[1] = Math.floor(this.items_size * .5);
        }
        
        var item;
        
        // Put the items back, add the various event bindings.
        for (i = 0; i < this.cfg.spacing; i++) {
        	item = this.items[i];
            this.list.append(item);
            
            $(item).click(this.cfg.itemClick);
            $(item).hover(this.cfg.itemHover[0], this.cfg.itemHover[1]);
        }
        
        var self = this;
        
        // Window resize bindings, if fluid.
        if (this.cfg.fluid)
        	$(window)
        		.unbind('resize', function() { self.size() })
        		.bind(  'resize', function() { self.size() });
        
        // The index is an array of the two extremes of the items that are
        // currently visible. (i.e. [0] 1 2 3 [4]).
        this.index = [0, this.cfg.spacing - 1];
        
        this.btns();
        this.size();
        
        if ($.isFunction(this.cfg.onjCan[1]))
        	this.cfg.onjCan[1]();
    };

    //
    // Set up the 'previous' and 'next' buttons.
    // ------------------------------------------------------------------------
    $.jCan.prototype.btns = function()
    {
    	// Set up the buttons if they haven't already been.
        if (this.btnP == undefined || this.btnN == undefined ) 
        {
        	var btn = this.cfg.buttons;
        	var p = '<div class="prev">'+ btn[0] +'</div>';
        	var n = '<div class="next">'+ btn[1] +'</div>';
            
            this.wrap.prepend(p + '\n' + n);

            this.btnP = this.wrap.children('div.prev');
            this.btnN = this.wrap.children('div.next');
            
            if (this.cfg.circular == false) {
                this.btnP.addClass('off');
                
                if (this.items_size <= this.cfg.spacing)
                    this.btnN.addClass('off');
            }

            this.btnP.css(this.ltrb[0], 0);
            this.btnN.css(this.ltrb[1], 0);
            
            this.btnP.css(this.wh[1], '100%');
            this.btnN.css(this.wh[1], '100%');
            
            var self = this;
            this.btnP.click( function() { self.prev(); } );
            this.btnN.click( function() { self.next(); } ); 
            
            return;
        } 
        
        // Otherwise, handle the button states (on/off).
        if (this.cfg.circular == false) {
            if (this.index[0] == 0)
                this.btnP.addClass('off');
            else
                this.btnP.removeClass('off');
            
            if (this.index[1] == this.items_size - 1)
                this.btnN.addClass('off');
            else
                this.btnN.removeClass('off');
        }
    };

    //
    // Control the 'previous' button's functionality.
    // ------------------------------------------------------------------------
    $.jCan.prototype.prev = function() 
    {
        if (this.cfg.circular || this.index[0] >= (this.cfg.step[0]))
            this.spin(this.cfg.step[0] * -1);
        else
            this.spin(this.index[0]);
        
        this.btns();
    };

    //
    // Control the 'next' button's functionality.
    // ------------------------------------------------------------------------
    $.jCan.prototype.next = function() 
    {  	
        if (this.cfg.circular || this.index[1] <= (this.items_size - this.cfg.step[1] - 1))
            this.spin(this.cfg.step[1]);
        else
            this.spin(this.items_size - this.index[1] - 1);
        
        this.btns();
    };

    //
    // Size all elements when the window is resized.
    // ------------------------------------------------------------------------
    $.jCan.prototype.size = function()
    {
        if ($.isFunction(this.cfg.onSize[0]))
        	this.cfg.onSize[0]();
        
        // Dynamic wrap width/height configuring.
        if (this.cfg.wrap[0] != null)
        	this.wrap.css('width', this.cfg.wrap[0]);
        if (this.cfg.wrap[1] != null)
        	this.wrap.css('height', this.cfg.wrap[1]);
        
        // Dynamic items width/height configuring.
        if (this.cfg.item[0] != null)
        	this.items.css('width', this.cfg.item[0]);
        if (this.cfg.item[1] != null)
        	this.items.css('height', this.cfg.item[1]);
        
        // Padding, width, height, and border are possible variables that can
        // affect the total width/height of an item.
        if (this.wh[0] == 'width') {
            this.item_wh = this.items.width();
            this.wrap_wh = this.wrap.width();
        } else {
            this.item_wh = this.items.height();
            this.wrap_wh = this.wrap.height();
        }

		//! TODO: Grab padding, border, add to width/height?
		
        this.item_wh = parseFloat(this.item_wh);
        this.wrap_wh = parseFloat(this.wrap_wh);

		// Even margins are set on each item so that they are equally spaced
		// across the wrap width/height.
        this.item_m = (this.wrap_wh / this.cfg.spacing - this.item_wh) * 5;
        this.item_m = Math.round(this.item_m) / 10;
        
        this.item_d = this.item_wh + (this.item_m * 2);
        
        this.items.css('margin-' + this.ltrb[0], this.item_m);
        this.items.css('margin-' + this.ltrb[1], this.item_m);
        
        var whval = this.item_d * (this.cfg.spacing + this.cfg.step[1]);
        this.list.css(this.wh[0], whval);
        
        if ($.isFunction(this.cfg.onSize[1]))
        	this.cfg.onSize[1]();
    };

    //
    // "Spin" the image 'can.'
    //
    // @param step
    //   Number of images to rotate past.
    // ------------------------------------------------------------------------
    $.jCan.prototype.spin = function(step)
    {
        if (step == 0 || this.animating) 
            return;
        
        this.animating = true;
        
        // "Circular", helper function for calculating positions.
        function c(i)
        {
            if (i < min) return max + i + 1;
            if (i > max) return i - max - 1;
            return i;
        }
        
        // Minimum and maximum possible image position indexes.
        var min = 0;
        var max = this.items_size - 1;
        
        // Forwards (1/true) or backwards (0/false).
        var direction = (step > 0) + 0;
        
        // Delta; going backwards 1 is subtracted, forwards 1 is added.
        var d = direction ? 1 : -1;
        
        // First and last images to become visible.
        var start = c(this.index[direction] + d);
        var end = c(start + step - d);
       
        // All the images that need to be added and removed.
        var add = [], rm = [];

        do {
            add.push(this.items[start]);
            rm.push(this.items[c(start + (-1 * d * this.cfg.spacing))]);
            
            start = c(start + d);
            
            this.index[0] = c(this.index[0] + d);
            this.index[1] = c(this.index[1] + d);
        } 
        while (start != c(end + d));
        
        // The actual animation is calculated; when going backwards, the new
        // images are added and hidden by setting a negative position value of
        // their w/h value on the list; when going forwards, the new images are
        // hidden by the extra w/h that was calculated by size().
        var css, fn, self = this;
        
        if (direction) {
        	css = -1 * step * this.item_d;
            this.list.append(add);
        } else {
        	css = 0;
            this.list.css(this.ltrb[0], step * this.item_d);
            this.list.prepend(add.reverse());
        }
        
        $(add).click(this.cfg.itemClick);
        $(add).hover(this.cfg.itemHover[0], this.cfg.itemHover[1]);
        
        css = this.cfg.vertical ? {top:  css} : {left: css};   
        fn = function() 
        {
        	$(rm).remove();
        	$(this).css(self.ltrb[0], 0);
        	self.animating = false;
		};
        
        this.list.animate(css, this.cfg.speed, this.cfg.easing, fn);
    };
})
(jQuery);
