(function($) {

$.fn.scroller = function(settings) {
    function clamp(val, range) {
        while (val < range)
            val += range;
        return val % range;
    }
    
    function sgn(n) {
        return (n == 0 ? 0 : (n < 0 ? -1 : 1));
    }
    
    var conf = $.extend({
        vertical: false,
        circular: false,
        scrollSpeed: 1.0,
        autoScrollInterval: -1,
        autoScrollDirection: 1,
        stopOnInteraction: true,
        animationInterval: 50,
        scrollByPage: false,
        stopOnMouseOver: true,
        enteringView: null,
        enteredView: null,
        leavingView: null,
        leftView: null
    }, settings);
    
    $.each(this, function() {
        var root = $(this);
        
        var fireItemEvents = !!(conf.enteringView || conf.enteredView || conf.leavingView || conf.leftView);
        
        var containerEl = $(".ScrollerItems:first", this);
        if (containerEl.length == 0)
            containerEl = root;
        
        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()); // user to be root!
            areaEl.width(containerEl.outerWidth());
            
            containerEl.after(areaEl);
            areaEl.append(containerEl);
        }// else {
        //    areaEl = containerEl.parent();
        //}
        
        var containers = $(">*", areaEl);
        
        var items = $(">*", containerEl);
        
        var itemCount = items.length;
        $(".ScrollerItemCount", this).text(itemCount);
        
        var index = 0, maxIndex = 0;
        
        if (itemCount == 0 || (!conf.circular && itemCount == 1)) {
            root.addClass("ScrollerNoScroll");
            updateClasses();
            return;
        }
        
        var itemsSize = 0;
        
        var areaSize = 0;
        if (conf.vertical)
            areaSize = areaEl.innerHeight(); // used to be containerEl
        else
            areaSize = areaEl.innerWidth();
            
        if (areaSize == 0) {
            root.addClass("ScrollerNoScroll");
            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.circular) {
            var totalSize = itemsSize;
            do {
                areaEl.append(containerEl.clone(true));
                totalSize += itemsSize;
            } while (totalSize < areaSize + itemsSize);
            
            containers = $(">*", areaEl);
        } else if (itemsSize <= areaSize) {
            root.addClass("ScrollerNoScroll");
            updateClasses();
            return;
        } else {
            items.each(function(i) {
                if (this.position >= itemsSize - areaSize && maxIndex == 0)
                    maxIndex = i;
            });
        }
        
        if (fireItemEvents) {
            containers.each(function() {
                this.items = $(">*", this);
            });
        }
        
        containers.css("position", "absolute");
        
        var position = 0, target = 0, timer = null;
        
        function scrollBySetting(d) {
            if (conf.scrollByPage)
                scrollByPage(d);
            else
                scrollByIndex(d);
        }
        
        function scrollByPage(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;
            }
            
            scrollToIndex(nindex);
        }
        
        function scrollByIndex(d) {
            scrollToIndex(index + d);
        }
        
        function scrollToIndex(nindex) {
            if (conf.circular) {
                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();
        }
        
        function tick() {
            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.circular)
                    position = target = Math.min(position, itemsSize - areaSize);
                
                timer = null;
                
                startAutoScroll(null, true);
            } else {
                position += s * d;
                
                timer = setTimeout(tick, conf.animationInterval);
            }
            
            updateElements(timer ? 0 : 2);
        }
        
        function startAnimation() {
            if (timer)
                return;
            
            updateElements(1);
            timer = setTimeout(tick, conf.animationInterval);
        }
        
        // Which events to fire? 0 = animation is progress, 1 = animation starting, 2 = animation ending
        function updateElements(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.circular) {
                root[index == 0 ? "addClass" : "removeClass"]("ScrollBackDisabled");
                root[index == maxIndex ? "addClass" : "removeClass"]("ScrollForwardDisabled");
            }
            root.find(".ScrollerCurrentItem").text(clamp(index, itemCount) + 1);
        }
        
        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 autoScrollTimer = null, inhibitAutoScroll = false;
        
        function stopAutoScroll() {
            inhibitAutoScroll = true;
            
            if (autoScrollTimer) {
                clearTimeout(autoScrollTimer);
                autoScrollTimer = null;
            }
        }
        
        function startAutoScroll(e, noForce) {
            if (!noForce) inhibitAutoScroll = false;
            
            if (conf.autoScrollInterval >= 0 && !autoScrollTimer && !inhibitAutoScroll)
                autoScrollTimer = setTimeout(autoScroll, conf.autoScrollInterval);
        }
        
        function autoScroll() {
            autoScrollTimer = null;
            scrollBySetting(conf.autoScrollDirection);
        }
        
        updateElements(2);
        updateClasses();
        
        if (conf.stopOnMouseOver)
            $(this).hover(stopAutoScroll, startAutoScroll);
        
        startAutoScroll();
        
        $(".ScrollBack", root).click(function(e) {
            if (conf.stopOnInteraction) {
                stopAutoScroll();
                conf.autoScrollInterval = -1;
            }
            scrollBySetting(-1);
            e.preventDefault();
        });
        $(".ScrollForward", root).click(function(e) {
            if (conf.stopOnInteraction) {
                stopAutoScroll();
                conf.autoScrollInterval = -1;
            }
            scrollBySetting(1);
            e.preventDefault();
        });
    });
};

})(jQuery);