$.extend($.fn, {
    /* Turns a UL into a carousel.
     * options:
     *   - nextControl: jQuery target or selector to act as the 'next slide' button
     *   - prevControl: jQuery target or selector to act as the 'previous slide' button
     *   - multipleVisibleFrames: if we are managing multiple visible frames, set to true
     *   - populateDotNav: jQuery target or selector into which to populate clickable nav dots
     *   - dotFactory: a function that should return the HTML for one dot
    */
    carousel: function(opts){
        var $self = this; // carousel itself
        var $slides = $self.children("li");
        var visiblePixels = $self.width();
        var widths = $.map($slides, function(el){ return $(el).outerWidth(); });
        var target = 0;
        var avgSlideSize = function(){
            var total = 0;
            $.each(widths, function(){
                total+=this;
            });
            return total / widths.length;
        }();

        if ($slides.size() <= 1) return false; // nothing to do with one slide

        $.extend({
            multipleVisibleFrames: false
        }, opts);

        // if we've got multiple visible slides, we need to move the slides themselves
        // otherwise it's way better to just move the parent container
        var $toSlide = $slides.first();
        if (!opts.multipleVisibleFrames)
            $toSlide = $toSlide.parent();

        // these will just no-op if the values of these opts aren't found by jQuery
        var $nextControl = $(opts.nextControl);
        var $prevControl = $(opts.prevControl);
        $nextControl.click(function(){ slide("next"); });
        $prevControl.click(function(){ slide("prev"); }).hide(0);

        // create nav dots for each slide, if we're handed the target and the dot factory
        if (opts.populateDotNav && opts.dotFactory){
            $(opts.populateDotNav).html(
                $.map($slides, function(el){
                    return opts.dotFactory();
                }).join("") // we're just building the html
            );

            // add click handlers to the dots and make the first dot active
            var $dots = $(opts.populateDotNav).children();
            $dots.click(function(){
                $dots.removeClass("active");
                $(this).addClass("active");
                var pos = $dots.index(this);
                slideTo(pos);
            }).first().addClass("active");
        }

        // slides to a position
        function slideTo(pos){
            var steps = target - pos;
            var dir = steps < 0 ? "next" : "prev";
            for (i=0;i<Math.abs(steps);i++)
            {
                slide(dir);
            }
        }

        // just moves forward or backwards by one slide (dir can be "prev" or "next")
        function slide(dir){
            $nextControl.add($prevControl).show(0);
            if (!evalNav(dir)) return false;

            // we should be copacetic enough now to actually slide
            if (dir === "prev") target--;

            $toSlide.animate({
                "margin-left": ( dir === "next" ? "-" : "+" ) + "=" + widths[target]
            }, 200);

            if (dir === "next") target++;

            evalNav(dir);
        }

        // determines whether we can move forward or backwards and whether the nav
        // visibility needs to be updated based on that decision
        function evalNav(dir){
            // don't slide out-of-bounds
            if (dir === "prev" && target === 0){
                $prevControl.hide(0);
                return false;
            }

            // in general we don't want to show empty slide slots, so if it seems on average that we're gonna if we slide, bail.
            // if we aren't handling maultiple visible frames, we don't need to do that fancy footwork
            var visibleOkay = opts.multipleVisibleFrames && (dir ==="next" && (widths.length - target) * avgSlideSize < visiblePixels);

            if (dir === "next" && target === widths.length - 1 ||
                visibleOkay){
                $nextControl.hide(0);
                return false;
            }

            return true;
        }
    }
});

