/**
 * this class uses jQuery
 *
 * pass in an element selected by jQuery and the Scroller does the rest
 * optionally add some options
 *
 * options:
 *    - orientation: vertical, horizontal
 *    - aimation_length: amount of time (in milis) it takes for one 'scroll'
 *    - animation_pause_length: amount of time (in milis) the scroller pauses in between scrolls
 *    - initial_delay: amount of time to wait before starting the scroller
 *    - auto_size: automatically size the scrollables' widths/heights to fit within container
 *    - auto_size_count: number of scrollables to see at a time (must use auto_size option)
 *
 * $Id: scroller.js,v 1.16 2010/12/13 19:33:49 gschwart Exp $
 */

function Scroller (el, options)
{
    var self = this;

    // clone elements to other end of the scrollables list so animation is fluid
    self.cycle = function (direction, cycles)
    {
        cycles = cycles || 1;

        if (direction == 'prev')
        {
            self.inner.children().slice(-cycles).clone().prependTo(self.inner);
            //for(var i = 0; i < cycles; i++)
            //{
            //    self.inner.children().eq(self.inner.children().size() - 1).detach().prependTo(self.inner);
            //}
        }
        else if (direction == 'next')
        {
            self.inner.children().slice(0, cycles).clone().appendTo(self.inner);
            //for(var i = 0; i < cycles; i++)
            //{
            //    self.inner.children().eq(0).detach().appendTo(self.inner);
            //}
        }

        self._clone_count = cycles;
    }

    // remove unneeded cloned elements (takes same arguments as self.cycle())
    self.after_cycle = function (direction, cycles)
    {
        cycles = cycles || 1;

        if (direction == 'prev')
        {
            self.inner.children().slice(-cycles).remove();
        }
        else if (direction == 'next')
        {
            self.inner.children().slice(0, cycles).remove();
        }

        self._clone_count = 0;
    }

    // returns true number of scrollables
    // made into a function because with all the cloning above, this gets slightly complicated
    self.get_num_items = function ()
    {
        self._clone_count = self._clone_count || 0;

        return self.inner.children().size() - self._clone_count;
    }

    // index has changed, notify all registered callbacks
    self.push_index = function ()
    {
        // assume self.options.index_update is an array of functions
        for (var i = 0; i < self.options.index_update.length; i++)
        {
            self.options.index_update[i](self.item_index);
        }
    }

    // update and push new index
    self.update_index = function (new_index)
    {
        self.item_index = new_index;

        self.push_index();
    }

    // arbitrarily move and push new index
    self.move_index = function (direction)
    {
        if (direction == 'next')
        {
            self.item_index++;
        }
        else if (direction == 'prev')
        {
            self.item_index--;
        }

        if (self.item_index < 0)
            self.item_index += self.num_items;

        self.item_index = self.item_index % self.num_items;

        self.push_index();
    }

    // generate any scroll action (scrolls through only one item)
    self.scroll_action = function (direction, fast)
    {
        // all calculations must be done in-action since resize can happen after queue'd but before execution
        return function (callback)
        {
            var animation;
            var inner_css;
            if (self.options.orientation == 'vertical')
            {
                if (direction == 'next')
                {
                    animation = {top: '-' + self.item_height + 'px'};
                    inner_css = {top: '0px'};
                }
                else if (direction == 'prev')
                {
                    animation = {top: 0};
                    inner_css = {top: '-' + self.item_height + 'px'};
                }
            }
            else if (self.options.orientation == 'horizontal')
            {
                if (direction == 'next')
                {
                    animation = {left: '-' + self.item_width + 'px'};
                    inner_css = {left: '0px'};
                }
                else if (direction == 'prev')
                {
                    animation = {left: 0};
                    inner_css = {left: '-' + self.item_width + 'px'};
                }
            }

            var animation_length = self.options.animation_length;

            if (fast)
            {
                animation_length = animation_length / 2;
            }

            // actually do stuff
            self.cycle(direction);

            if (direction == 'prev')
            {
                self.inner.css(inner_css);
            }

            self.inner.animate(
                animation,
                animation_length,
                'swing',
                function () {
                    self.after_cycle(direction);

                    if (direction == 'next')
                    {
                        self.inner.css(inner_css);
                    }

                    self.move_index(direction);

                    callback();
                }
            );
        }
    }

    // generate a jump action (jump to any index)
    self.jump_action = function (jump_index)
    {
        if (jump_index < 0 || jump_index >= self.num_items)
        {
            // out of bounds
            return null;
        }

        // all calculations must be done in-action since item_index hasn't been updated and resize can happen after queue'd but before execution
        return function (callback)
        {
            if (jump_index == self.item_index)
            {
                // already here!
                callback();
                return;
            }

            var jump_forward = jump_index - self.item_index;
            if (jump_forward < 0)
                jump_forward += self.num_items;

            var jump_backward = self.item_index - jump_index;
            if (jump_backward < 0)
                jump_backward += self.num_items;

            var direction;
            var jump_count;
            if (jump_forward < jump_backward)
            {
                // moving forward
                direction  = 'next';
                jump_count = jump_forward;
            }
            else
            {
                // moving backward
                direction  = 'prev';
                jump_count = jump_backward;
            }

            var animation;
            var inner_css;
            if (self.options.orientation == 'vertical')
            {
                if (direction == 'next')
                {
                    animation = {top: '-' + (self.item_height * jump_count) + 'px'};
                    inner_css = {top: '0px'};
                }
                else
                {
                    animation = {top: 0};
                    inner_css = {top: '-' + (self.item_height * jump_count) + 'px'};
                }
            }
            else if (self.options.orientation == 'horizontal')
            {
                if (direction == 'next')
                {
                    animation = {left: '-' + (self.item_width * jump_count) + 'px'};
                    inner_css = {left: '0px'};
                }
                else
                {
                    animation = {left: 0};
                    inner_css = {left: '-' + (self.item_width * jump_count) + 'px'};
                }
            }

            var animation_length = (self.options.animation_length / 3) * jump_count;

            // actually do stuff
            self.cycle(direction, jump_count);

            if (direction == 'prev')
            {
                self.inner.css(inner_css);
            }

            self.inner.animate(
                animation,
                animation_length,
                'swing',
                function () {
                    self.after_cycle(direction, jump_count);

                    if (direction == 'next')
                    {
                        self.inner.css(inner_css);
                    }

                    self.update_index(jump_index);

                    callback();
                }
            );
        }
    }

    // resize code
    self._resize = function ()
    {
        self.container_height   = $(self.el).innerHeight();
        self.container_width    = $(self.el).innerWidth();
        self.num_items          = self.get_num_items();

        // auto_size code
        if (self.options.auto_size)
        {
            var max_auto_size_count = self.options.auto_size_count >= self.num_items ? self.num_items : self.options.auto_size_count;

            if (self.options.direction == 'vertical')
            {
                self.item_width     = self.inner.children().eq(0).outerWidth(true);
                self.item_height    = (self.container_height / max_auto_size_count);

                self.inner.children().each(function () {
                    $(this).height( self.item_height - ($(this).outerHeight(true) - $(this).height()) );
                });
            }
            else
            {
                self.item_width     = (self.container_width / max_auto_size_count);
                self.item_height    = self.inner.children().eq(0).outerHeight(true);

                self.inner.children().each(function () {
                    $(this).width( self.item_width - ($(this).outerWidth(true) - $(this).width()) );
                });
            }
        }
        else
        {
            self.item_height    = self.inner.children().eq(0).outerHeight(true);
            self.item_width     = self.inner.children().eq(0).outerWidth(true);
        }

        // resize the inner div so it can contain all scrollables on one "line"
        // add a little extra since we clone the elements for visual purposes and they all need to be on one "line"
        if (self.options.direction == 'vertical')
        {
            self.inner.css({
                'height': Math.ceil(self.item_height * self.num_items * 2),
                'width':  Math.ceil(self.item_width)
            });
        }
        else
        {
            self.inner.css({
                'width':  Math.ceil(self.item_width * self.num_items * 2),
                'height': Math.ceil(self.item_height)
            });
        }

        //if (self.options.direction == 'vertical')
        //{
        //    console.log('new dimensions -- items:'+self.num_items+' scrollable:'+self.item_height+' container:'+self.container_height);
        //}
        //else
        //{
        //    console.log('new dimensions -- items:'+self.num_items+' scrollable:'+self.item_width+' container:'+self.container_width);
        //}
    }

    // front-end
    self.start_scrolling = function ()
    {
        self.queue.queue_recurring(new Action('scroll', self.scroll_action('next', false)));
        self.queue.queue_recurring(new Action('pause', function (callback) {setTimeout(callback, self.options.animation_pause_length);}));
        self.queue.start();
    }

    self.stop_scrolling = function ()
    {
        self.queue.clear_recurring_queue();
        self.queue.stop();
    }

    self.prev = function ()
    {
        self.queue.clear_recurring_queue();
        self.queue.quash('pause');
        self.queue.queue(new Action('prev', self.scroll_action('prev', true)));
        self.queue.start();
    }

    self.next = function ()
    {
        self.queue.clear_recurring_queue();
        self.queue.quash('pause');
        self.queue.queue(new Action('next', self.scroll_action('next', true)));
        self.queue.start();
    }

    self.jump = function (jump_index)
    {
        self.queue.clear_recurring_queue();
        self.queue.quash('pause');
        self.queue.queue(new Action('jump', self.jump_action(jump_index)));
        self.queue.start();
    }

    // "constructor"
        self.el = el;
        self.queue = new ActionQueue();

        // pull in options
            self.options = options || {};

            var default_options = {
                'initial_delay':            3000,
                'animation_length':         3000,
                'animation_pause_length':   3000,
                'orientation':              'vertical',
                'index_update':             [],
                'auto_size':                false,
                'auto_size_count':          1
            };

            for (var opt in default_options)
            {
                if( typeof(self.options[opt]) == "undefined" )
                {
                    self.options[opt] = default_options[opt];
                }
            }

        self.item_index = 0;

        var scrollables = $(self.el).children();

        $(self.el).css({
            'position': 'relative',
            'overflow': 'hidden'
        });

        // make an inner div and move all scrollables in there
        self.inner = $('<div></div>').css('position', 'absolute').appendTo(self.el);
        scrollables.each(function () {
            $(this).detach().appendTo(self.inner);
        });
        self.inner.children().show();

        // size everything just right
        self._resize();
        if (self.options.auto_size)
        {
            // scroller must survive resizes
            $(window).resize(function () { self._resize(); });
        }

        //console.log(self);

        setTimeout(function () { self.start_scrolling() }, self.options.initial_delay);
}

/*
 * $Log: scroller.js,v $
 * Revision 1.16  2010/12/13 19:33:49  gschwart
 * increased inner div size to allow scrolling with only 1 scrollable
 *
 * Revision 1.15  2010/12/13 19:22:07  gschwart
 * took out some debug statements
 *
 * Revision 1.14  2010/12/13 19:14:21  gschwart
 * added auto_size feature allowing you to exactly fit the scrollable objects into a sized container
 *
 * Revision 1.13  2010/12/07 22:45:08  gschwart
 * added "initial_delay" option
 *
 * Revision 1.12  2010/12/02 21:42:19  gschwart
 * scroller shows all children after load so they can be initially hidden
 *
 * Revision 1.11  2010/04/19 18:59:43  gschwart
 * after implementing ActionQueue, we used it here
 * scroller can now queue actions like a pro... no more quick cuts instead of scrolls
 * also, scroller can now scroll to an arbitrary index in one motion
 * also implemented a 'index callback' so something can know when a scroller scrolls and what index it is on
 *
 * Revision 1.10  2010/04/06 16:42:09  gschwart
 * modified scroller to use our new Action Queue
 *
 * Revision 1.9  2009/12/05 03:40:02  jmuller
 * Add initial support for next and previous slide.
 *
 * Revision 1.8  2009/12/02 21:42:54  gschwart
 * fixed some bugs
 *
 * Revision 1.7  2009/12/02 20:57:03  gschwart
 * added the ability to stop and start the scroller
 *
 * Revision 1.6  2009/12/02 19:51:28  gschwart
 * inverted how we did the scroller
 * now it keeps the original scroller element as the outer-most element (so css rules can still apply)
 * also removed the items_displayed feature which allowed you to control how many items show at once.  this will now have to be controlled manually by setting the size of the outer element yourself
 * i think its a good idea to leave as much power as possible to the script user
 *
 * Revision 1.5  2009/05/28 21:10:32  gschwart
 * re-spaced things
 *
 * Revision 1.4  2009/04/30 18:20:06  jmuller
 * Add log lines.
 *
 */

