/**
 * tools.scrollable 1.1.2 - Scroll your HTML with eye candy.
 * 
 * Copyright (c) 2009 Tero Piirainen
 * http://flowplayer.org/tools/scrollable.html
 *
 * Dual licensed under MIT and GPL 2+ licenses
 * http://www.opensource.org/licenses
 *
 * Launch  : March 2008
 * Date: ${date}
 * Revision: ${revision} 
 */
(function($) { 
        
    // static constructs
    $.tools = $.tools || {};
    
    $.tools.scrollable = {
        version: '1.1.2',
        
        conf: {
            
            // basics
            size: 5,
            vertical: false,
            speed: 400,
            keyboard: true,        
            
            // by default this is the same as size
            keyboardSteps: null, 
            
            // other
            disabledClass: 'disabled',
            hoverClass: null,        
            clickable: true,
            activeClass: 'active', 
            easing: 'swing',
            loop: false,
            
            items: '.items',
            item: null,
            
            // navigational elements            
            prev: '.prev',
            next: '.next',
            prevPage: '.prevPage',
            nextPage: '.nextPage', 
            api: false
            
            // CALLBACKS: onBeforeSeek, onSeek, onReload
        } 
    };
                
    var current;        
    
    // constructor
    function Scrollable(root, conf) {   
        
        // current instance
        var self = this, $self = $(this),
             horizontal = !conf.vertical,
             wrap = root.children(),
             index = 0,
             forward;  
        
        
        if (!current) { current = self; }
        
        // bind all callbacks from configuration
        $.each(conf, function(name, fn) {
            if ($.isFunction(fn)) { $self.bind(name, fn); }
        });
        
        if (wrap.length > 1) { wrap = $(conf.items, root); }
        
        // navigational items can be anywhere when globalNav = true
        function find(query) {
            var els = $(query);
            return conf.globalNav ? els : root.parent().find(query);    
        }
        
        // to be used by plugins
        root.data("finder", find);
        
        // get handle to navigational elements
        var prev = find(conf.prev),
             next = find(conf.next),
             prevPage = find(conf.prevPage),
             nextPage = find(conf.nextPage);

        
        // methods
        $.extend(self, {
            
            getIndex: function() {
                return index;    
            },
            
            getClickIndex: function() {
                var items = self.getItems(); 
                return items.index(items.filter("." + conf.activeClass));    
            },
    
            getConf: function() {
                return conf;    
            },
            
            getSize: function() {
                return self.getItems().size();    
            },
    
            getPageAmount: function() {
                return Math.ceil(this.getSize() / conf.size);     
            },
            
            getPageIndex: function() {
                return Math.ceil(index / conf.size);    
            },

            getNaviButtons: function() {
                return prev.add(next).add(prevPage).add(nextPage);    
            },
            
            getRoot: function() {
                return root;    
            },
            
            getItemWrap: function() {
                return wrap;    
            },
            
            getItems: function() {
                return wrap.children(conf.item);    
            },
            
            getVisibleItems: function() {
                return self.getItems().slice(index, index + conf.size);    
            },
            
            /* all seeking functions depend on this */        
            seekTo: function(i, time, fn) {

                if (i < 0) { i = 0; }                
                
                // nothing happens
                if (index === i) { return self; }                
                
                // function given as second argument
                if ($.isFunction(time)) {
                    fn = time;
                }

                // seeking exceeds the end                 
                if (i > self.getSize() - conf.size) { 
                    return conf.loop ? self.begin() : this.end(); 
                }                 

                var item = self.getItems().eq(i);                    
                if (!item.length) { return self; }                
                
                // onBeforeSeek
                var e = $.Event("onBeforeSeek");

                $self.trigger(e, [i]);                
                if (e.isDefaultPrevented()) { return self; }                
                
                // get the (possibly altered) speed
                if (time === undefined || $.isFunction(time)) { time = conf.speed; }
                
                function callback() {
                    if (fn) { fn.call(self, i); }
                    $self.trigger("onSeek", [i]);
                }
                
                if (horizontal) {
                    wrap.animate({left: -item.position().left}, time, conf.easing, callback);                    
                } else {
                    wrap.animate({top: -item.position().top}, time, conf.easing, callback);                            
                }
                
                
                current = self;
                index = i;                
                
                // onStart
                e = $.Event("onStart");
                $self.trigger(e, [i]);                
                if (e.isDefaultPrevented()) { return self; }                
    
                
                /* default behaviour */
                
                // prev/next buttons disabled flags
                prev.add(prevPage).toggleClass(conf.disabledClass, i === 0);
                next.add(nextPage).toggleClass(conf.disabledClass, i >= self.getSize() - conf.size);
                
                return self; 
            },            
            
                
            move: function(offset, time, fn) {
                forward = offset > 0;
                return this.seekTo(index + offset, time, fn);
            },
            
            next: function(time, fn) {
                return this.move(1, time, fn);    
            },
            
            prev: function(time, fn) {
                return this.move(-1, time, fn);    
            },
            
            movePage: function(offset, time, fn) {
                forward = offset > 0;
                var steps = conf.size * offset;
                
                var i = index % conf.size;
                if (i > 0) {
                     steps += (offset > 0 ? -i : conf.size - i);
                }
                
                return this.move(steps, time, fn);        
            },
            
            prevPage: function(time, fn) {
                return this.movePage(-1, time, fn);
            },  
    
            nextPage: function(time, fn) {
                return this.movePage(1, time, fn);
            },            
            
            setPage: function(page, time, fn) {
                return this.seekTo(page * conf.size, time, fn);
            },            
            
            begin: function(time, fn) {
                forward = false;
                return this.seekTo(0, time, fn);    
            },
            
            end: function(time, fn) {
                forward = true;
                var to = this.getSize() - conf.size;
                return to > 0 ? this.seekTo(to, time, fn) : self;    
            },
            
            reload: function() {                
                $self.trigger("onReload");
                return self;
            },            
            
            focus: function() {
                current = self;
                return self;
            },
            
            click: function(i) {
                
                var item = self.getItems().eq(i), 
                     klass = conf.activeClass,
                     size = conf.size;            
                
                // check that i is sane
                if (i < 0 || i >= self.getSize()) { return self; }
                
                // size == 1                            
                if (size == 1) {
                    if (conf.loop) { return self.next(); }
                    
                    if (i === 0 || i == self.getSize() -1)  { 
                        forward = (forward === undefined) ? true : !forward;     
                    }
                    return forward === false  ? self.prev() : self.next(); 
                } 
                
                // size == 2
                if (size == 2) {
                    if (i == index) { i--; }
                    self.getItems().removeClass(klass);
                    item.addClass(klass);                    
                    return self.seekTo(i, time, fn);
                }                
        
                if (!item.hasClass(klass)) {                
                    self.getItems().removeClass(klass);
                    item.addClass(klass);
                    var delta = Math.floor(size / 2);
                    var to = i - delta;
        
                    // next to last item must work
                    if (to > self.getSize() - size) { 
                        to = self.getSize() - size; 
                    }
        
                    if (to !== i) {
                        return self.seekTo(to);        
                    }
                }
                
                return self;
            },
            
            // bind / unbind
            bind: function(name, fn) {
                $self.bind(name, fn);
                return self;    
            },    
            
            unbind: function(name) {
                $self.unbind(name);
                return self;    
            }            
            
        });
        
        // callbacks    
        $.each("onBeforeSeek,onStart,onSeek,onReload".split(","), function(i, ev) {
            self[ev] = function(fn) {
                return self.bind(ev, fn);    
            };
        });  
            
            
        // prev button        
        prev.addClass(conf.disabledClass).click(function() {
            self.prev(); 
        });
        

        // next button
        next.click(function() { 
            self.next(); 
        });
        
        // prev page button
        nextPage.click(function() { 
            self.nextPage(); 
        });
        
        if (self.getSize() < conf.size) {
            next.add(nextPage).addClass(conf.disabledClass);    
        }
        

        // next page button
        prevPage.addClass(conf.disabledClass).click(function() { 
            self.prevPage(); 
        });        
        
        
        // hover
        var hc = conf.hoverClass, keyId = "keydown." + Math.random().toString().substring(10); 
            
        self.onReload(function() { 

            // hovering
            if (hc) {
                self.getItems().hover(function()  {
                    $(this).addClass(hc);        
                }, function() {
                    $(this).removeClass(hc);    
                });                        
            }
            
            // clickable
            if (conf.clickable) {
                self.getItems().each(function(i) {
                    $(this).unbind("click.scrollable").bind("click.scrollable", function(e) {
                        if ($(e.target).is("a")) { return; }    
                        return self.click(i);
                    });
                });
            }                
            
            // keyboard            
            if (conf.keyboard) {                
                
                // keyboard works on one instance at the time. thus we need to unbind first
                $(document).unbind(keyId).bind(keyId, function(evt) {

                    // do nothing with CTRL / ALT buttons
                    if (evt.altKey || evt.ctrlKey) { return; }
                    
                    // do nothing for unstatic and unfocused instances
                    if (conf.keyboard != 'static' && current != self) { return; }
                    
                    var s = conf.keyboardSteps;                
                                        
                    if (horizontal && (evt.keyCode == 37 || evt.keyCode == 39)) {                    
                        self.move(evt.keyCode == 37 ? -s : s);
                        return evt.preventDefault();
                    }    
                    
                    if (!horizontal && (evt.keyCode == 38 || evt.keyCode == 40)) {
                        self.move(evt.keyCode == 38 ? -s : s);
                        return evt.preventDefault();
                    }
                    
                    return true;
                    
                });
                
            } else  {
                $(document).unbind(keyId);    
            }                

        });
        
        self.reload(); 
        
    } 

        
    // jQuery plugin implementation
    $.fn.scrollable = function(conf) { 
            
        // already constructed --> return API
        var el = this.eq(typeof conf == 'number' ? conf : 0).data("scrollable");
        if (el) { return el; }         
 
        var globals = $.extend({}, $.tools.scrollable.conf);
        conf = $.extend(globals, conf);
        
        conf.keyboardSteps = conf.keyboardSteps || conf.size;
        
        this.each(function() {            
            el = new Scrollable($(this), conf);
            $(this).data("scrollable", el);    
        });
        
        return conf.api ? el: this; 
        
    };
            
    
})(jQuery);
