(function($) {
    var clamp = function(val, range) {
        if (range == 0)
            return 0;
        while (val < 0) // was val < range
            val += range;
        return val % range;
    }

    var sgn = function(n) {
        return (n == 0 ? 0 : (n < 0 ? -1 : 1));
    }

    $.fn.scroller = function(settingsOrCommand) {
        var args = arguments;
        if (typeof (settingsOrCommand) == "string") {
            return $.each(this, function() {
                var iface = $(this).data("jquery-scroller-interface");
                if (iface && iface[settingsOrCommand])
                    return iface[settingsOrCommand].apply(null, $.makeArray(args).slice(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,
            interactionRestartDelay: 10000,

            enteringView: null,
            enteredView: null,
            leavingView: null,
            leftView: null
        }, settingsOrCommand);

        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, itemsMaxOppSize = 0, areaSize = 0;

                items.each(function() {
                    if (conf.vertical) {
                        this.size = $(this).outerHeight();
                        var oppSize = $(this).outerWidth();
                    } else {
                        this.size = $(this).outerWidth();
                        var oppSize = $(this).outerHeight();
                    }

                    this.position = itemsSize;
                    itemsSize += this.size;
                    if (oppSize > itemsMaxOppSize)
                        itemsMaxOppSize = oppSize;
                });

                var containers = $(">*", areaEl);

                if (conf.vertical) {
                    containerEl.height(itemsSize);
                    containerEl.width(itemsMaxOppSize);
                } else {
                    containerEl.width(itemsSize);
                    containerEl.height(itemsMaxOppSize);
                }

                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);
                }

                if (conf.vertical)
                    areaSize = areaEl.innerHeight(); // used to be containerEl
                else
                    areaSize = areaEl.innerWidth();

                if (areaSize == 0) {
                    root.addClass("ScrollerDisabled");
                    updateClasses();
                    return;
                }

                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, interactionTimer = 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, instant) {
                    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();

                    if (instant) {
                        position = target;
                        updateElements(2);
                    } else {
                        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 && 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);
            }

            function notifyInteraction() {
                if (conf.interactionRestartDelay < 0)
                    return;

                stopAutoAdvance();

                if (interactionTimer)
                    clearTimeout(interactionTimer);

                interactionTimer = setTimeout(restartAutoAdvanceAfterInteraction, conf.interactionRestartDelay);
            }

            function restartAutoAdvanceAfterInteraction() {
                interactionTimer = null;

                startAutoAdvance();
            }

            if (!conf.crossfade) {
                updateElements(2);
                startAutoAdvance();
            }

            updateClasses();

            if (conf.stopOnMouseOver)
                $(this).hover(stopAutoAdvance, startAutoAdvance);

            $(".ScrollerBack", root).click(function(e) {
                if (conf.stopOnInteraction)
                    notifyInteraction();
                goBySetting(-1);
                e.preventDefault();
            });
            $(".ScrollerForward", root).click(function(e) {
                if (conf.stopOnInteraction)
                    notifyInteraction();
                goBySetting(1);
                e.preventDefault();
            });

            root.data("jquery-scroller-interface", {
                "gotoindex": goToIndex,
                "notifyinteraction": notifyInteraction
            });
        });
    };

})(jQuery);