/**
 * jCarousel - Riding carousels with jQuery
 *   http://sorgalla.com/jcarousel/
 *
 * Copyright (c) 2006 Jan Sorgalla (http://sorgalla.com)
 * Dual licensed under the MIT (MIT-LICENSE.txt)
 * and GPL (GPL-LICENSE.txt) licenses.
 *
 * Built on top of the jQuery library
 *   http://jquery.com
 *
 * Inspired by the "Carousel Component" by Bill Scott
 *   http://billwscott.com/carousel/
 */

(function($) {
    /**
     * Creates a carousel for all matched elements.
     *
     * @example $("#mycarousel").jcarousel();
     * @before <ul id="mycarousel" class="jcarousel-skin-name"><li>First item</li><li>Second item</li></ul>
     * @result
     *
     * <div class="jcarousel-skin-name">
     *   <div class="jcarousel-container">
     *     <div disabled="disabled" class="jcarousel-prev jcarousel-prev-disabled"></div>
     *     <div class="jcarousel-next"></div>
     *     <div class="jcarousel-clip">
     *       <ul class="jcarousel-list">
     *         <li class="jcarousel-item-1">First item</li>
     *         <li class="jcarousel-item-2">Second item</li>
     *       </ul>
     *     </div>
     *   </div>
     * </div>
     *
     * @name jcarousel
     * @type jQuery
     * @param Hash o A set of key/value pairs to set as configuration properties.
     * @cat Plugins/jCarousel
     */
    $.fn.jcarousel=function(o) {
        return this.each(function() {
            new $jc(this, o);
        });
    };

    // Default configuration properties.
    var defaults={
        vertical: false,
        start: 1,
        offset: 1,
        size: null,
        scroll: 3,
        visible: null,
        animation: 'normal',
        easing: 'swing',
        auto: 0,
        wrap: null,
        initCallback: null,
        reloadCallback: null,
        itemLoadCallback: null,
        itemFirstInCallback: null,
        itemFirstOutCallback: null,
        itemLastInCallback: null,
        itemLastOutCallback: null,
        itemVisibleInCallback: null,
        itemVisibleOutCallback: null,
        buttonNextHTML: '<div></div>',
        buttonPrevHTML: '<div></div>',
        buttonNextEvent: 'click',
        buttonPrevEvent: 'click',
        buttonNextCallback: null,
        buttonPrevCallback: null
    };

    /**
     * The jCarousel object.
     *
     * @constructor
     * @name $.jcarousel
     * @param Object e The element to create the carousel for.
     * @param Hash o A set of key/value pairs to set as configuration properties.
     * @cat Plugins/jCarousel
     */
    $.jcarousel=function(e, o) {
        this.options  =$.extend({}, defaults, o||{});

        this.locked   =false;

        this.container=null;
        this.clip     =null;
        this.list     =null;
        this.buttonNext=null;
        this.buttonPrev=null;

        this.wh=!this.options.vertical?'width':'height';
        this.lt=!this.options.vertical?'left':'top';

        // Extract skin class
        var skin='', split=e.className.split(' ');

        for (var i=0; i<split.length; i++) {
            if(split[i].indexOf('jcarousel-skin') !=-1) {
                $(e).removeClass(split[i]);
                var skin=split[i];
               break;
            }
        }

        if(e.nodeName=='UL'||e.nodeName=='OL') {
            this.list=$(e);
            this.container=this.list.parent();

            if(this.container.hasClass('jcarousel-clip')) {
                if(!this.container.parent().hasClass('jcarousel-container'))
                    this.container=this.container.wrap('<div></div>');

                this.container=this.container.parent();
            } else if(!this.container.hasClass('jcarousel-container'))
                this.container=this.list.wrap('<div></div>').parent();
        } else {
            this.container=$(e);
            this.list=$(e).find('>ul,>ol,div>ul,div>ol');
        }

        if(skin !=''&&this.container.parent()[0].className.indexOf('jcarousel-skin')==-1)
        	this.container.wrap('<div class=" '+ skin+'"></div>');

        this.clip=this.list.parent();

        if(!this.clip.length||!this.clip.hasClass('jcarousel-clip'))
            this.clip=this.list.wrap('<div></div>').parent();

        this.buttonPrev=$('.jcarousel-prev', this.container);

        if(this.buttonPrev.size()==0&&this.options.buttonPrevHTML !=null)
            this.buttonPrev=this.clip.before(this.options.buttonPrevHTML).prev();

        this.buttonPrev.addClass(this.className('jcarousel-prev'));

        this.buttonNext=$('.jcarousel-next', this.container);

        if(this.buttonNext.size()==0&&this.options.buttonNextHTML !=null)
            this.buttonNext=this.clip.before(this.options.buttonNextHTML).prev();

        this.buttonNext.addClass(this.className('jcarousel-next'));

        this.clip.addClass(this.className('jcarousel-clip'));
        this.list.addClass(this.className('jcarousel-list'));
        this.container.addClass(this.className('jcarousel-container'));

        var di=this.options.visible !=null?Math.ceil(this.clipping() / this.options.visible):null;
        var li=this.list.children('li');

        var self=this;

        if(li.size()>0) {
            var wh=0, i=this.options.offset;
            li.each(function() {
                self.format(this, i++);
                wh +=self.dimension(this, di);
            });

            this.list.css(this.wh, wh+'px');

            // Only set if not explicitly passed as option
            if(!o||o.size===undefined)
                this.options.size=li.size();
        }

        // For whatever reason, .show() does not work in Safari...
        this.container.css('display', 'block');
        this.buttonNext.css('display', 'block');
        this.buttonPrev.css('display', 'block');

        this.funcNext =function() { self.next(); };
        this.funcPrev =function() { self.prev(); };
        this.funcResize=function() { self.reload(); };

        if(this.options.initCallback !=null)
            this.options.initCallback(this, 'init');

        if($.browser.safari) {
            this.buttons(false, false);
            $(window).bind('load', function() { self.setup(); });
        } else
            this.setup();
    };

    // Create shortcut for internal use
    var $jc=$.jcarousel;

    $jc.fn=$jc.prototype={
        jcarousel: '0.2.3'
    };

    $jc.fn.extend=$jc.extend=$.extend;

    $jc.fn.extend({
        /**
         * Setups the carousel.
         *
         * @name setup
         * @type undefined
         * @cat Plugins/jCarousel
         */
        setup: function() {
            this.first   =null;
            this.last    =null;
            this.prevFirst=null;
            this.prevLast=null;
            this.animating=false;
            this.timer   =null;
            this.tail    =null;
            this.inTail  =false;

            if(this.locked)
                return;

            this.list.css(this.lt, this.pos(this.options.offset)+'px');
            var p=this.pos(this.options.start);
            this.prevFirst=this.prevLast=null;
            this.animate(p, false);

            $(window).unbind('resize', this.funcResize).bind('resize', this.funcResize);
        },

        /**
         * Clears the list and resets the carousel.
         *
         * @name reset
         * @type undefined
         * @cat Plugins/jCarousel
         */
        reset: function() {
            this.list.empty();

            this.list.css(this.lt, '0px');
            this.list.css(this.wh, '10px');

            if(this.options.initCallback !=null)
                this.options.initCallback(this, 'reset');

            this.setup();
        },

        /**
         * Reloads the carousel and adjusts positions.
         *
         * @name reload
         * @type undefined
         * @cat Plugins/jCarousel
         */
        reload: function() {
            if(this.tail !=null&&this.inTail)
                this.list.css(this.lt, $jc.intval(this.list.css(this.lt))+this.tail);

            this.tail =null;
            this.inTail=false;

            if(this.options.reloadCallback !=null)
                this.options.reloadCallback(this);

            if(this.options.visible !=null) {
                var self=this;
                var di=Math.ceil(this.clipping() / this.options.visible), wh=0, lt=0;
                $('li', this.list).each(function(i) {
                    wh +=self.dimension(this, di);
                    if(i+1<self.first)
                        lt=wh;
                });

                this.list.css(this.wh, wh+'px');
                this.list.css(this.lt, -lt+'px');
            }

            this.scroll(this.first, false);
        },

        /**
         * Locks the carousel.
         *
         * @name lock
         * @type undefined
         * @cat Plugins/jCarousel
         */
        lock: function() {
            this.locked=true;
            this.buttons();
        },

        /**
         * Unlocks the carousel.
         *
         * @name unlock
         * @type undefined
         * @cat Plugins/jCarousel
         */
        unlock: function() {
            this.locked=false;
            this.buttons();
        },

        /**
         * Sets the size of the carousel.
         *
         * @name size
         * @type undefined
         * @param Number s The size of the carousel.
         * @cat Plugins/jCarousel
         */
        size: function(s) {
            if(s !=undefined) {
                this.options.size=s;
                if(!this.locked)
                    this.buttons();
            }

            return this.options.size;
        },

        /**
         * Checks whether a list element exists for the given index (or index range).
         *
         * @name get
         * @type bool
         * @param Number i The index of the (first) element.
         * @param Number i2 The index of the last element.
         * @cat Plugins/jCarousel
         */
        has: function(i, i2) {
            if(i2==undefined||!i2)
                i2=i;

            if(this.options.size !==null&&i2>this.options.size)
            	i2=this.options.size;

            for (var j=i; j <=i2; j++) {
                var e=this.get(j);
                if(!e.length||e.hasClass('jcarousel-item-placeholder'))
                    return false;
            }

            return true;
        },

        /**
         * Returns a jQuery object with list element for the given index.
         *
         * @name get
         * @type jQuery
         * @param Number i The index of the element.
         * @cat Plugins/jCarousel
         */
        get: function(i) {
            return $('.jcarousel-item-'+i, this.list);
        },

        /**
         * Adds an element for the given index to the list.
         * If the element already exists, it updates the inner html.
         * Returns the created element as jQuery object.
         *
         * @name add
         * @type jQuery
         * @param Number i The index of the element.
         * @param String s The innerHTML of the element.
         * @cat Plugins/jCarousel
         */
        add: function(i, s) {
            var e=this.get(i), old=0, add=0;

            if(e.length==0) {
                var c, e=this.create(i), j=$jc.intval(i);
                while (c=this.get(--j)) {
                    if(j <=0||c.length) {
                        j <=0?this.list.prepend(e):c.after(e);
                       break;
                    }
                }
            } else
                old=this.dimension(e);

            e.removeClass(this.className('jcarousel-item-placeholder'));
            typeof s=='string'?e.html(s):e.empty().append(s);

            var di=this.options.visible !=null?Math.ceil(this.clipping() / this.options.visible):null;
            var wh=this.dimension(e, di) - old;

            if(i>0&&i<this.first)
                this.list.css(this.lt, $jc.intval(this.list.css(this.lt)) - wh+'px');

            this.list.css(this.wh, $jc.intval(this.list.css(this.wh))+wh+'px');

            return e;
        },

        /**
         * Removes an element for the given index from the list.
         *
         * @name remove
         * @type undefined
         * @param Number i The index of the element.
         * @cat Plugins/jCarousel
         */
        remove: function(i) {
            var e=this.get(i);

            // Check if item exists and is not currently visible
            if(!e.length||(i >=this.first&&i <=this.last))
                return;

            var d=this.dimension(e);

            if(i<this.first)
                this.list.css(this.lt, $jc.intval(this.list.css(this.lt))+d+'px');

            e.remove();

            this.list.css(this.wh, $jc.intval(this.list.css(this.wh)) - d+'px');
        },

        /**
         * Moves the carousel forwards.
         *
         * @name next
         * @type undefined
         * @cat Plugins/jCarousel
         */
        next: function() {
            this.stopAuto();

            if(this.tail !=null&&!this.inTail)
                this.scrollTail(false);
            else
                this.scroll(((this.options.wrap=='both'||this.options.wrap=='last')&&this.options.size !=null&&this.last==this.options.size)?1:this.first+this.options.scroll);
        },

        /**
         * Moves the carousel backwards.
         *
         * @name prev
         * @type undefined
         * @cat Plugins/jCarousel
         */
        prev: function() {
            this.stopAuto();

            if(this.tail !=null&&this.inTail)
                this.scrollTail(true);
            else
                this.scroll(((this.options.wrap=='both'||this.options.wrap=='first')&&this.options.size !=null&&this.first==1)?this.options.size:this.first - this.options.scroll);
        },

        /**
         * Scrolls the tail of the carousel.
         *
         * @name scrollTail
         * @type undefined
         * @param Bool b Whether scroll the tail back or forward.
         * @cat Plugins/jCarousel
         */
        scrollTail: function(b) {
            if(this.locked||this.animating||!this.tail)
                return;

            var pos=$jc.intval(this.list.css(this.lt));

            !b?pos -=this.tail:pos +=this.tail;
            this.inTail=!b;

            // Save for callbacks
            this.prevFirst=this.first;
            this.prevLast=this.last;

            this.animate(pos);
        },

        /**
         * Scrolls the carousel to a certain position.
         *
         * @name scroll
         * @type undefined
         * @param Number i The index of the element to scoll to.
         * @param Bool a Flag indicating whether to perform animation.
         * @cat Plugins/jCarousel
         */
        scroll: function(i, a) {
            if(this.locked||this.animating)
                return;

            this.animate(this.pos(i), a);
        },

        /**
         * Prepares the carousel and return the position for a certian index.
         *
         * @name pos
         * @type Number
         * @param Number i The index of the element to scoll to.
         * @cat Plugins/jCarousel
         */
        pos: function(i) {
            if(this.locked||this.animating)
                return;

            i=$jc.intval(i);
            if(this.options.wrap !='circular')
                i=i<1?1:(this.options.size&&i>this.options.size?this.options.size:i);

            var back=this.first>i;
            var pos=$jc.intval(this.list.css(this.lt));

            // Create placeholders, new list width/height
            // and new list position
            var f=this.options.wrap !='circular'&&this.first <=1?1:this.first;
            var c=back?this.get(f):this.get(this.last);
            var j=back?f:f - 1;
            var e=null, l=0, p=false, d=0;

            while (back?--j >=i:++j<i) {
                e=this.get(j);
                p=!e.length;
                if(e.length==0) {
                    e=this.create(j).addClass(this.className('jcarousel-item-placeholder'));
                    c[back?'before':'after' ](e);
                }

                c=e;
                d=this.dimension(e);

                if(p)
                    l +=d;

                if(this.first !=null&&(this.options.wrap=='circular'||(j >=1&&(this.options.size==null||j <=this.options.size))))
                    pos=back?pos+d:pos - d;
            }

            // Calculate visible items
            var clipping=this.clipping();
            var cache=[];
            var visible=0, j=i, v=0;
            var c=this.get(i - 1);

            while (++visible) {
                e=this.get(j);
                p=!e.length;
                if(e.length==0) {
                    e=this.create(j).addClass(this.className('jcarousel-item-placeholder'));
                    // This should only happen on a next scroll
                    c.length==0?this.list.prepend(e):c[back?'before':'after' ](e);
                }

                c=e;
                var d=this.dimension(e);
                if(d==0) {
                    alert('jCarousel: No width/height set for items. This will cause an infinite loop. Aborting...');
                    return 0;
                }

                if(this.options.wrap !='circular'&&this.options.size !==null&&j>this.options.size)
                    cache.push(e);
                else if(p)
                    l +=d;

                v +=d;

                if(v >=clipping)
                   break;

                j++;
            }

             // Remove out-of-range placeholders
            for (var x=0; x<cache.length; x++)
                cache[x].remove();

            // Resize list
            if(l>0) {
                this.list.css(this.wh, this.dimension(this.list)+l+'px');

                if(back) {
                    pos -=l;
                    this.list.css(this.lt, $jc.intval(this.list.css(this.lt)) - l+'px');
                }
            }

            // Calculate first and last item
            var last=i+visible - 1;
            if(this.options.wrap !='circular'&&this.options.size&&last>this.options.size)
                last=this.options.size;

            if(j>last) {
                visible=0, j=last, v=0;
                while (++visible) {
                    var e=this.get(j--);
                    if(!e.length)
                       break;
                    v +=this.dimension(e);
                    if(v >=clipping)
                       break;
                }
            }

            var first=last - visible+1;
            if(this.options.wrap !='circular'&&first<1)
                first=1;

            if(this.inTail&&back) {
                pos +=this.tail;
                this.inTail=false;
            }

            this.tail=null;
            if(this.options.wrap !='circular'&&last==this.options.size&&(last - visible+1) >=1) {
                var m=$jc.margin(this.get(last), !this.options.vertical?'marginRight':'marginBottom');
                if((v - m)>clipping)
                    this.tail=v - clipping - m;
            }

            // Adjust position
            while (i-->first)
                pos +=this.dimension(this.get(i));

            // Save visible item range
            this.prevFirst=this.first;
            this.prevLast=this.last;
            this.first   =first;
            this.last    =last;

            return pos;
        },

        /**
         * Animates the carousel to a certain position.
         *
         * @name animate
         * @type undefined
         * @param mixed p Position to scroll to.
         * @param Bool a Flag indicating whether to perform animation.
         * @cat Plugins/jCarousel
         */
        animate: function(p, a) {
            if(this.locked||this.animating)
                return;

            this.animating=true;

            var self=this;
            var scrolled=function() {
                self.animating=false;

                if(p==0)
                    self.list.css(self.lt,  0);

                if(self.options.wrap=='both'||self.options.wrap=='last'||self.options.size==null||self.last<self.options.size)
                    self.startAuto();

                self.buttons();
                self.notify('onAfterAnimation');
            };

            this.notify('onBeforeAnimation');

            // Animate
            if(!this.options.animation||a==false) {
                this.list.css(this.lt, p+'px');
                scrolled();
            } else {
                var o=!this.options.vertical?{'left': p}:{'top': p};
                this.list.animate(o, this.options.animation, this.options.easing, scrolled);
            }
        },

        /**
         * Starts autoscrolling.
         *
         * @name auto
         * @type undefined
         * @param Number s Seconds to periodically autoscroll the content.
         * @cat Plugins/jCarousel
         */
        startAuto: function(s) {
            if(s !=undefined)
                this.options.auto=s;

            if(this.options.auto==0)
                return this.stopAuto();

            if(this.timer !=null)
                return;

            var self=this;
            this.timer=setTimeout(function() { self.next(); }, this.options.auto * 1000);
        },

        /**
         * Stops autoscrolling.
         *
         * @name stopAuto
         * @type undefined
         * @cat Plugins/jCarousel
         */
        stopAuto: function() {
            if(this.timer==null)
                return;

            clearTimeout(this.timer);
            this.timer=null;
        },

        /**
         * Sets the states of the prev/next buttons.
         *
         * @name buttons
         * @type undefined
         * @cat Plugins/jCarousel
         */
        buttons: function(n, p) {
            if(n==undefined||n==null) {
                var n=!this.locked&&this.options.size !==0&&((this.options.wrap&&this.options.wrap !='first')||this.options.size==null||this.last<this.options.size);
                if(!this.locked&&(!this.options.wrap||this.options.wrap=='first')&&this.options.size !=null&&this.last >=this.options.size)
                    n=this.tail !=null&&!this.inTail;
            }

            if(p==undefined||p==null) {
                var p=!this.locked&&this.options.size !==0&&((this.options.wrap&&this.options.wrap !='last')||this.first>1);
                if(!this.locked&&(!this.options.wrap||this.options.wrap=='last')&&this.options.size !=null&&this.first==1)
                    p=this.tail !=null&&this.inTail;
            }

            var self=this;

            this.buttonNext[n?'bind':'unbind'](this.options.buttonNextEvent, this.funcNext)[n?'removeClass':'addClass'](this.className('jcarousel-next-disabled')).attr('disabled', n?false:true);
            this.buttonPrev[p?'bind':'unbind'](this.options.buttonPrevEvent, this.funcPrev)[p?'removeClass':'addClass'](this.className('jcarousel-prev-disabled')).attr('disabled', p?false:true);

            if(this.buttonNext.length>0&&(this.buttonNext[0].jcarouselstate==undefined||this.buttonNext[0].jcarouselstate !=n)&&this.options.buttonNextCallback !=null) {
                this.buttonNext.each(function() { self.options.buttonNextCallback(self, this, n); });
                this.buttonNext[0].jcarouselstate=n;
            }

            if(this.buttonPrev.length>0&&(this.buttonPrev[0].jcarouselstate==undefined||this.buttonPrev[0].jcarouselstate !=p)&&this.options.buttonPrevCallback !=null) {
                this.buttonPrev.each(function() { self.options.buttonPrevCallback(self, this, p); });
                this.buttonPrev[0].jcarouselstate=p;
            }
        },

        notify: function(evt) {
            var state=this.prevFirst==null?'init':(this.prevFirst<this.first?'next':'prev');

            // Load items
            this.callback('itemLoadCallback', evt, state);

            if(this.prevFirst !==this.first) {
                this.callback('itemFirstInCallback', evt, state, this.first);
                this.callback('itemFirstOutCallback', evt, state, this.prevFirst);
            }

            if(this.prevLast !==this.last) {
                this.callback('itemLastInCallback', evt, state, this.last);
                this.callback('itemLastOutCallback', evt, state, this.prevLast);
            }

            this.callback('itemVisibleInCallback', evt, state, this.first, this.last, this.prevFirst, this.prevLast);
            this.callback('itemVisibleOutCallback', evt, state, this.prevFirst, this.prevLast, this.first, this.last);
        },

        callback: function(cb, evt, state, i1, i2, i3, i4) {
            if(this.options[cb]==undefined||(typeof this.options[cb] !='object'&&evt !='onAfterAnimation'))
                return;

            var callback=typeof this.options[cb]=='object'?this.options[cb][evt]:this.options[cb];

            if(!$.isFunction(callback))
                return;

            var self=this;

            if(i1===undefined)
                callback(self, state, evt);
            else if(i2===undefined)
                this.get(i1).each(function() { callback(self, this, i1, state, evt); });
            else {
                for (var i=i1; i <=i2; i++)
                    if(i !==null&&!(i >=i3&&i <=i4))
                        this.get(i).each(function() { callback(self, this, i, state, evt); });
            }
        },

        create: function(i) {
            return this.format('<li></li>', i);
        },

        format: function(e, i) {
            var $e=$(e).addClass(this.className('jcarousel-item')).addClass(this.className('jcarousel-item-'+i));
            $e.attr('jcarouselindex', i);
            return $e;
        },

        className: function(c) {
            return c+' '+c+(!this.options.vertical?'-horizontal':'-vertical');
        },

        dimension: function(e, d) {
            var el=e.jquery !=undefined?e[0]:e;

            var old=!this.options.vertical ?
                el.offsetWidth+$jc.margin(el, 'marginLeft')+$jc.margin(el, 'marginRight') :
                el.offsetHeight+$jc.margin(el, 'marginTop')+$jc.margin(el, 'marginBottom');

            if(d==undefined||old==d)
                return old;

            var w=!this.options.vertical ?
                d - $jc.margin(el, 'marginLeft') - $jc.margin(el, 'marginRight') :
                d - $jc.margin(el, 'marginTop') - $jc.margin(el, 'marginBottom');

            $(el).css(this.wh, w+'px');

            return this.dimension(el);
        },

        clipping: function() {
            return !this.options.vertical ?
                this.clip[0].offsetWidth - $jc.intval(this.clip.css('borderLeftWidth')) - $jc.intval(this.clip.css('borderRightWidth')) :
                this.clip[0].offsetHeight - $jc.intval(this.clip.css('borderTopWidth')) - $jc.intval(this.clip.css('borderBottomWidth'));
        },

        index: function(i, s) {
            if(s==undefined)
                s=this.options.size;

            return Math.round((((i-1) / s) - Math.floor((i-1) / s)) * s)+1;
        }
    });

    $jc.extend({
        /**
         * Gets/Sets the global default configuration properties.
         *
         * @name defaults
         * @descr Gets/Sets the global default configuration properties.
         * @type Hash
         * @param Hash d A set of key/value pairs to set as configuration properties.
         * @cat Plugins/jCarousel
         */
        defaults: function(d) {
            return $.extend(defaults, d||{});
        },

        margin: function(e, p) {
            if(!e)
                return 0;

            var el=e.jquery !=undefined?e[0]:e;

            if(p=='marginRight'&&$.browser.safari) {
                var old={'display': 'block', 'float': 'none', 'width': 'auto'}, oWidth, oWidth2;

                $.swap(el, old, function() { oWidth=el.offsetWidth; });

                old['marginRight']=0;
                $.swap(el, old, function() { oWidth2=el.offsetWidth; });

                return oWidth2 - oWidth;
            }

            return $jc.intval($.css(el, p));
        },

        intval: function(v) {
            v=parseInt(v);
            return isNaN(v)?0:v;
        }
    });

})(jQuery);
