(function($) {

$.fn.scroller = function(settings) {
    function clamp(val, range) {
        if (range == 0)
            return 0;
        while (val < 0) // was val < range
            val += range;
        return val % range;
    }
    
    function sgn(n) {
        return (n == 0 ? 0 : (n < 0 ? -1 : 1));
    }
    
    var conf = $.extend({
        vertical: false,
        looping: false,
        crossfade: false,
        
        minItems: 1,
        initialItem: 0,
        
        scrollSpeed: 1.0,
        scrollAnimationInterval: 50,
        
        fadeDurationAuto: 2000,
        fadeDurationManual: 200,
        fadeInFirst: false,
        
        seamless: false,
        goByPage: false,
        
        naviAddSpaces: false,
        
        autoAdvanceInterval: -1,
        autoAdvanceDirection: 1,
        
        stopOnMouseOver: true,
        stopOnInteraction: true,
        
        enteringView: null,
        enteredView: null,
        leavingView: null,
        leftView: null
    }, settings);
    
    return $.each(this, function() {
        var root = $(this);
        root.addClass("ScrollerJS");
        
        var fireItemEvents = !!(conf.enteringView || conf.enteredView || conf.leavingView || conf.leftView);
        
        var containerEl = $(".ScrollerItems:first", this);
        if (containerEl.length == 0)
            containerEl = root;
        
        if (!conf.crossfade) {
            var areaEl = $(".ScrollerArea:first", this);
            if (areaEl.length == 0) {
                containerEl.css("overflow", "hidden");
                areaEl = $("<div class=\"ScrollerArea\" style=\"position: relative; overflow: hidden;\">");
                areaEl.height(containerEl.outerHeight()); // used to be root!
                areaEl.width(containerEl.outerWidth());
                
                containerEl.after(areaEl);
                areaEl.append(containerEl);
            }
            
            var containers = $(">*", areaEl);
        }
        
        var items = $(">*", containerEl);
        
        var itemCount = items.length;
        $(".ScrollerItemCount", this).text(itemCount);
        
        var index = clamp(conf.initialItem, itemCount), maxIndex = 0;
        
        if (itemCount == 0 || itemCount < conf.minItems) {
            conf.cirular = false;
            root.addClass("ScrollerNoItems");
            updateClasses();
            return;
        }
        
        if (!conf.crossfade) {
            var itemsSize = 0, areaSize = 0;
            
            if (conf.vertical)
                areaSize = areaEl.innerHeight(); // used to be containerEl
            else
                areaSize = areaEl.innerWidth();
                
            if (areaSize == 0) {
                root.addClass("ScrollerDisabled");
                updateClasses();
                return;
            }

            items.each(function() {
                if (conf.vertical)
                    this.size = $(this).outerHeight();
                else
                    this.size = $(this).outerWidth();
                
                this.position = itemsSize;
                itemsSize += this.size;
            });
            
            if (conf.vertical) {
                containerEl.height(itemsSize);
            } else {
                containerEl.width(itemsSize);
            }
            
            if (conf.looping) {
                var totalSize = itemsSize;
                do {
                    areaEl.append(containerEl.clone(true));
                    totalSize += itemsSize;
                } while (totalSize < areaSize + itemsSize);
                
                containers = $(">*", areaEl);
            } else if (itemsSize <= areaSize) {
                root.addClass("ScrollerDisabled");
                updateClasses();
                return;
            } else {
                items.each(function(i) {
                    if (this.position >= itemsSize - areaSize && maxIndex == 0)
                        maxIndex = i;
                });
            }
            
            containers.css("position", "absolute");
        }
        
        if (fireItemEvents) {
            containers.each(function() {
                this.items = $(">*", this);
            });
        }
        
        var navi = $(".ScrollerNavi", this);
        
        if (navi.length) {
            items.each(function(i) {
                if (conf.naviAddSpaces && i > 0)
                    navi.append(" ");
                navi.append($("<li />").append($("<a href=\"#\" />").text(i+1).click(function() {
                    if (conf.stopOnInteraction) {
                        stopAutoAdvance();
                        conf.autoAdvanceInterval = -1;
                    }
                    goToIndex(i);
                    return false;
                })));
            });
            
            navi.find(">li:first").addClass("ScrollerNaviFirst");
            navi.find(">li:last").addClass("ScrollerNaviLast");
        }
        
        var position = 0, target = 0, timer = null;
        
        if (conf.crossfade) {
            var goBySetting = function(d) {
            
                goByIndex(d);
            }
            
            var goByIndex = function(d) {
                var ni = index + d;
                goToIndex(conf.looping ? clamp(ni, itemCount) : Math.max(0, Math.min(ni, itemCount)));
                
            }
            
            var goToIndex = function(nindex) {
                if (nindex == index)
                    return;
                
                var prevIndex = index;
                index = nindex;
                
                if (prevIndex != -1) {
                    items.eq(prevIndex).css("zIndex", 1);
                }
                
                items.eq(index).css({"zIndex": 2, "visibility": "visible", "opacity": 0}).animate({"opacity": 1.0},
                    inhibitAutoAdvance ? conf.fadeDurationManual : conf.fadeDurationAuto, function() {
                        if (prevIndex != -1) {
                            items.eq(prevIndex).css("visibility", "hidden");
                        }
                        startAutoAdvance(null, true);
                });
                
                updateClasses();
            }
            
            var checkImagesLoaded = function() {
                var fail = false;
                items.each(function() {
                    if (this.imgs) {
                        $.each(this.imgs, function() {
                            if (!this.complete) {
                                fail = true;
                                return false;
                            }
                        });
                    }
                });
                
                if (fail) {
                    setTimeout(checkImagesLoaded, 200);
                    return;
                }
                
                if (conf.fadeInFirst) {
                    var ind = index;
                    index = -1;
                    goToIndex(ind);                    
                } else {
                    items.eq(index).css("visibility", "visible");
                    startAutoAdvance();
                }
            }
            
            items.each(function(i) {
                $(this).css({"visibility": "hidden", "position": "absolute", "display": "block", "zIndex": 1});
                this.imgs = $("img", this);
                checkImagesLoaded();
            });
        } else {
            var goBySetting = function(d) {
                if (conf.goByPage)
                    goByPage(d);
                else
                    goByIndex(d);                    
            }
            
            var goByPage = function(d) {
                var scrollBy = 0, nindex = index, i;
                
                for (i = index; ; i += d) {
                    var size = items[clamp(i, itemCount)].size;
                    if (scrollBy + size > areaSize)
                        break;
                    
                    scrollBy += size;
                    nindex += d;
                }
                
                goToIndex(nindex);
                
            }
            
            var goByIndex = function(d) {
                goToIndex(index + d);
            }
            
            var goToIndex = function(nindex) {
                if (conf.looping) {
                    var s = sgn(nindex - index), i = index;
                    
                    if (s == 0)
                        return;
                    
                    index = nindex;
                    
                    for (; i != nindex; i += s) {
                        target += s * items[clamp(i - (s < 0 ? 1 : 0), itemCount)].size;
                    }                    
                } else {
                    if (nindex < 0) nindex = 0;
                    if (nindex >= maxIndex) nindex = maxIndex;
                    index = nindex;
                    
                    target = Math.min(items[index].position, itemsSize - areaSize);
                }
                
                updateClasses();
                
                startAnimation();
            }
            
            var tick = function() {
                var left = target - position;
                var s = sgn(left);
                var d = Math.sqrt(Math.abs(left)) * conf.scrollSpeed;                
                
                if (d >= Math.abs(left) || target == position) {                
                    index = clamp(index, itemCount);
                    
                    position = target = items[index].position;
                    if (!conf.looping)
                        position = target = Math.min(position, itemsSize - areaSize);
                    
                    timer = null;
                    
                    startAutoAdvance(null, true);
                } else {
				
				conf.seamless ? position += s : position += s * d

                    
                    timer = setTimeout(tick, conf.scrollAnimationInterval);
                }
                
                updateElements(timer ? 0 : 2);
            }
            
            var startAnimation = function() {
                if (timer)
                    return;
                
                updateElements(1);
                
                timer = setTimeout(tick, conf.scrollAnimationInterval);
            }
            
            // Which events to fire? 0 = animation is progress, 1 = animation starting, 2 = animation ending
            var updateElements = function(events) {
                var pos = -clamp(position, itemsSize);
                
                containers.each(function() {
                    this.style[conf.vertical ? "top" : "left"] = Math.floor(pos) + "px";
                    
                    /*if (fireItemEvents && events > 0) {
                        doItemEvents(events);
                    }*/
                    
                    pos += itemsSize;
                });
            }
        }
        
        function updateClasses() {
            if (!conf.looping) {
                root[index == 0 ? "addClass" : "removeClass"]("ScrollBackDisabled");
                root[index == maxIndex ? "addClass" : "removeClass"]("ScrollForwardDisabled");
            }
            root.find(".ScrollerCurrentItem").text(clamp(index, itemCount) + 1);
            if (navi.length) {
                navi.find(">li").removeClass("ScrollerNaviActive").eq(clamp(index, itemCount)).addClass("ScrollerNaviActive");
            }
        }
        
        function doItemEvents(events) {
            var pos = -target;
            
            var a = "";
            containers.each(function() {
                this.items.each(function(i) {
                    var newInView = (pos > -items[i].size) && (pos < areaSize);
                    
                    if (events == 1) {
                        if (conf.enteringView && newInView && !this.inView)
                            conf.enteringView(this);
                        else if (conf.leavingView && !newInView && this.inView)
                            conf.leavingView(this);
                    } else if (events == 2) {
                        if (conf.enteredView && newInView && !this.inView)
                            conf.enteredView(this);
                        else if (conf.leftView && !newInView && this.inView)
                            conf.leftView(this);
                    }
                    
                    if (events == 2)
                        this.inView = newInView;
                        
                    pos += items[i].size;
                });
            });
        }

        var autoAdvanceTimer = null, inhibitAutoAdvance = false;
        
        function stopAutoAdvance() {
            inhibitAutoAdvance = true;
            
            if (autoAdvanceTimer) {
                clearTimeout(autoAdvanceTimer);
                autoAdvanceTimer = null;
            }
        }
        
        function startAutoAdvance(e, noForce) {
            if (!noForce) inhibitAutoAdvance = false;
            
            if (conf.autoAdvanceInterval >= 0 && !autoAdvanceTimer && !inhibitAutoAdvance)
                autoAdvanceTimer = setTimeout(autoAdvance, conf.autoAdvanceInterval);           
        }
        
        function autoAdvance() {
            autoAdvanceTimer = null;
            
            goBySetting(conf.autoAdvanceDirection);
        }
        
        if (!conf.crossfade) {
            updateElements(2);            
            startAutoAdvance();
        }
            
        updateClasses();
        
        if (conf.stopOnMouseOver)
            $(this).hover(stopAutoAdvance, startAutoAdvance);
                
        $(".ScrollerBack", root).click(function(e) {
            if (conf.stopOnInteraction) {
                stopAutoAdvance();
                conf.autoAdvanceInterval = -1;
            }
            goBySetting(-1);
            e.preventDefault();
        });
        $(".ScrollerForward", root).click(function(e) {
            if (conf.stopOnInteraction) {
                stopAutoAdvance();
                conf.autoAdvanceInterval = -1;
            }
            goBySetting(1);
            e.preventDefault();
        });
    });
};

})(jQuery);
