/**
  * Js file for the EasySlide class.
  * Recently only tested on firefox 2-3.0.3, IE 7 and webkit.
  * 
  * :IMPORTANT: At the moment this class depends on all list elements to have the same width.
  * :TODO: When introducing support for various element widths, we need to calculate the correct acceleration for each element relative to the given speed.
  *
  * @description Provides a comfortable and easy to use interface to slide through a list with several options to control behaviour.
  * @requires mootools 1.2
  * @see EasySlide.Example.html
  * @author Thorsten Schmitt-Rink <schmittrink@gmail.com>
  * @copyright 2008 Thorsten Schmitt-Rink
  */
  
/**
  * You can register callbacks for following events:
  * @see EasySlide.EVENT
  */
  
var EasySlide = new Class
({
    /**
       * Options:
       * @required int speed The time in ms for a item to take to run over the screen once.
       * @required int elPerScreen How many items fit on the screen. This information is need to provide correct behaviour.
       * @optional int space The distance in px left between the single items. Default is 0.
       * @optional string mode { @see EasySlide.ROTATION }
       * @optional string pathMode { @see EasySlide.PATHMODE }
       * @optional mixed startPos Either an EasySlide constant { @see EasySlide.STARTPOS } or an index of the desired element.
       */
    Implements: [Options],
    
    /**
       * @var HtmlUlElement element The root element of our slider's list.
       */
    element: null,
    
    /**
       * @var int width The width that was calculated for the slider list. 
       * :NOTICE: (The li's don't float unless the list is wide enough, otherwise they break) 
       */
    slideModifier: $H({attributeName: 'width', value: 0}),
    
    /**
       * @var bool isSliding Indicates whether we are currently in the proccess of sliding or not.
       */
    isSliding: false,
    
    /**
       * @var Array listItems Array of hashes with the following properties:
       * { HtmlLiElement:element, int:index }
       */
    listItems: [],
    
    /**
       * @var Hash current Infos on the item that is currently first in the slider's list.
       */
    current: null,
    
    /**
       * @var string direction The current direction we are sliding to.
       */
    direction: null,
    
    /**
       * @var boolean loop Indicates whether to keep on sliding after finishing the current slide.
       */
    loop: false,
    
     /**
       * @var Hash targetItem When the slideTo method is called, this property contains the target we want to reach. 
       */
    targetItem: $H(),
    
    /**
      * @var Hash eventListeners A hash holding the callback functions for all registered events.
      */
    eventListeners: $H(),
   
    /**
      * Initialize and build the slider.
      *
      * @param HtmlUlElement sliderList
      * @param Hash options
      *
      * @access public
      */
    initialize: function(sliderList, options)
    {
        this.element = sliderList;
        this.setOptions(options);
        this.element.set('tween', {duration: this.get('speed'), transition: Fx.Transitions.linear, onComplete: this.onComplete.bind(this)});
        this.slideModifier.set('attributeName', (EasySlide.ALIGN.HORIZONTAL === this.get('align', EasySlide.ALIGN.HORIZONTAL)) ? 'width' : 'height');
    },
       
    /**
       * Determine the item which should be first in the list
       * and then build the slider's list according to the resulting order.
       *
       * @access public
       */
    bootStrap: function()
    {
        var items = this.element.getElements('li');
        var startPos = this.get('startPos', EasySlide.STARTPOS.FIRST);
        var startItemIndex = (EasySlide.STARTPOS.LAST === startPos) ? items.length - 1 : (EasySlide.STARTPOS.FIRST === startPos) ? 0 : startPos;
        this.buildSlider(startItemIndex, items);        
        var event = $H({type: 'EasySlideEvent', event: EasySlide.EVENT.BUILD_COMPLETE, src: this, current: this.current, initialIndex: startItemIndex});
        this.fireEvent(EasySlide.EVENT.BUILD_COMPLETE, event);
    },
    
    /**
      * Slide into the given direction with a range of one element.
      * To slide a range wider than one item set loop to true or use the slideTo() method.
      * 
      * @access public
      *
      * @param string The direction in which to slide. @see { EasySlide.DIRECTION }
      */
    slide: function(direction)
    {
        if (true === this.isSliding || (this.listItems.length <= this.get('elPerScreen'))) return false;
        
        if ((EasySlide.ROTATION.KARUSSEL === this.get('mode')) || this.isValidDirection(direction))
        {
            this.isSliding = true;
            this.direction = direction;
            var coords = this.current.get('element').getCoordinates();
            var targetValue = 0;
            var attributeName = this.slideModifier.get('attributeName');
            
            if (EasySlide.DIRECTION.L2R === direction || EasySlide.DIRECTION.T2B === direction)
            {
                this.current = this.listItems.pop();
                this.listItems.unshift(this.current);
                this.current.get('element').inject(this.element, 'top');
                this.element.get('tween').set('width' === attributeName ? 'left' : 'top', - (this.get('space', 0) + coords[attributeName]));
            }
            else
            {
                targetValue -= coords[attributeName] + this.get('space', 0);
            }
            this.fireEvent(EasySlide.EVENT.SLIDE_START, $H({type: 'EasySlideEvent', name: EasySlide.EVENT.SLIDE_START, src: this, direction: this.direction}));
            this.element.get('tween').start('width' === this.slideModifier.get('attributeName') ? 'left' : 'top', targetValue);
            return true;
        }
        
        this.fireEvent(EasySlide.EVENT.SLIDE_STOP, $H({type: 'EasySlideEvent', name: EasySlide.EVENT.SLIDE_STOP, src: this, direction: this.direction}));
        this.loop = false;
        return false;
    },
    
    /**
      * Method to use when you want to slide to a certain item in the list.
      *
      * @access public
      *
      * @param int index The index of the item you want to slide to. Must be the initial list position of the desired element.
      */
    slideTo: function(index)
    {
        if (this.current.get('index') === index) return;
        this.set('seekMode', EasySlide.SEEKMODE.TOFIRST);
        this.slideToIndex(index);
    },
    
    /**
      * Method to use when you want to slide a certain item in the list just until it is visible.
      *
      * @access public
      *
      * @param int index The index of the item you want to slide to. Must be the initial list position of the desired element.
      */
    slideIntoScreen: function(index)
    {
        if (this.isItemVisible(index)) return;
        this.set('seekMode', EasySlide.SEEKMODE.TOVISIBLE);
        this.slideToIndex(index);
    },
    
    /**
      * Check if the requested direction is inside the valid list's boundry.
      *
      * @access public
      *
      * @param string direction The direction to validate.
      *
      * @return boolean
      */
    isValidDirection: function(direction)
    {
        if (direction === EasySlide.DIRECTION.L2R || direction === EasySlide.DIRECTION.T2B)
        {
            return (0 < this.current.get('index'));
        }
        else
        {
            return ((this.current.get('index') + this.get('elPerScreen')) < this.listItems.length);
        }
    },
    
    /**
      * Check if the item at the given index is currently visible or not.
      *
      * @access public
      *
      * @param int index The initial list position of the item you want to check,
      */
    isItemVisible: function(initialIndex)
    {
        var target = this.findItemByInitialIndex(initialIndex);
        return (this.listItems.indexOf(target) < (this.get('elPerScreen')));
    },
    
    /**
      * See if we have a neighbor to our right.
      *
      * @access public
      *
      * @return boolean
      */
    hasNext: function()
    {
        return ((this.current.get('index') + this.get('elPerScreen')) < this.listItems.length);
    },
    
    /**
      * See if we have a neighbor to our left.
      *
      * @access public
      *
      * @return boolean
      */
    hasPrev: function()
    {
        return (0 < this.current.get('index'));
    },
    
    /**
      * Returns the currently first positioned element of the list.
      *
      * @access public
      *
      * @return Hash
      */
    getCurrentItem: function()
    {
        return this.current;
    },
    
    /**
      * Returns the item at the given index.
      *
      * @access public
      *
      * @param int index
      *
      * @return HtmlLiElement
      */
    getItemAt: function(index)
    {
        return $pick(this.listItems[index], null);
    },
    
    /**
      * Return the item with the initial list position matching the given index.
      *
      * @access public
      *
      * @param int index The searched elements index.
      *
      * @return object Json object containing the found item and it's current position in the list.
      */
    findItemByInitialIndex: function(index)
    {
        var checkItem = function(item)
        {
            return (item.get('index') === index); 
        }
        return $pick(this.listItems.filter(checkItem)[0], null);
    },
    
    /**
      * Return the slider's items.
      *
      * @access public
      *
      * @return Array
      */
    getItems: function()
    {
        return this.listItems;
    },
    
    /**
      * Register a callack to the given slider event.
      *
      * @access public
      *
      * @param string event_name The name of the event to register.
      * @param function callback The function to call when the given event is triggered.
      *
      * @return The given callback function.
      */
    addEvent: function(eventName, callback)
    {
        if (!this.eventListeners.has(eventName))
        {
            this.eventListeners.set(eventName, $A([callback])); 
            return callback;
        }
        this.eventListeners.get(eventName).push(callback);
        return callback;
    },
    
    /**
      * Remove a listener from the slider's listener list.
      *
      * @access public
      *
      * @param string event_name The name of the event to remove.
      * @param function callback The function called when the given event is triggered.
      */
    removeEvent: function(eventName, callback)
    {
        if (!this.eventListeners.has(eventName)) return false;
        return this.eventListeners.get(eventName).erase(callback);
    },
    
    /**
      * Return the value for an option by the given key.
      *
      * @access public
      *
      * @param string key
      * @param mixed defaultValue
      *
      * @return mixed Null if no value for the given key was found.
      */
    get: function(key, defaultValue)
    {
        return $pick(this.options[key], defaultValue, null);
    },
    
    /**
      * Return the value for an option by the given key.
      *
      * @access public
      *
      * @param string key
      * @param mixed value
      */
    set: function(key, value)
    {
        this.options[key] = value;
    },
    
    /**
      * Fetch all our items from the list and build the slider.
      *
      * @access protected
      *
      * @param initialItemIndex We will rebuild the list order so that the item at the given index is first in the resulting list.
      */
    buildSlider: function(initialItemIndex, listItems)
    {
        var buffer = [];        
        /**
            * Define a function we'll use to initialize our list items...
            */
        var initializeListItem = function(element, pos)
        {
            var item = this.registerItemEvents(this.initItem(element, pos));            
            /**
                * We have to mak sure that we correctly resort the list, if we're past an initial item index, so that
                * that the at the given index is displayed as first with the others following correct order.
                */
            if ((this.get('elPerScreen') + pos < listItems.length) && pos < initialItemIndex - 1)
            {
                /**
                      * As long as the desired initial item has not been reached we buffer the list items 
                      * we have initialized in order to append them at the end.
                      */
                element.inject(this.element);
                buffer.push(item);
            }
            else
            {
                /**
                      * As soon as the desired element is reached we start to build our slider list.
                      */
                this.listItems.push(item);
                this.current = $pick(this.current, item);
            }
        }.bind(this);        
        /**
            * ... and call it on every item in order o build our list.
            */
        $each(listItems, initializeListItem);        
        /**
            * We have to set the width for the list, that we calculated while we were buildung the list,
            * in order to make sure that we won't break the floating of the list elements.
            */
        this.element.setStyle(this.slideModifier.get('attributeName'), this.slideModifier.get('value'));
        /**
            * Then make sure we append our buffered elements.
            */
        this.listItems.extend(buffer);
    },
    
    /**
      * Initialize and add the given item to our slider.
      *
      * @param HtmlLiElement The list element to init.
      * @param int The initial position of the given element.
      * 
      * @access protected
      *
      * @return Hash
      */
    initItem: function(element, pos)
    {
        var coords = element.getCoordinates();
        var slideRange = this.slideModifier.get('value');
        var styleProp = 'height' === this.slideModifier.get('attributeName') ? 'height' : 'width';
        var marginStyle = 'margin-' + (styleProp === 'height' ? 'bottom' : 'right');
        element.setStyle(marginStyle, this.get('space', 0));
        slideRange += coords[styleProp] + this.get('space', 0);
        this.slideModifier.set('value', slideRange);
        return $H({index: pos, element: element});
    },
    
    /**
       * Register some events to the given list item.
       *
       * @param Hash item The hash representing the list item to register the events on.
       *
       * @access protected
       *
       * @return Hash
       */
    registerItemEvents: function(item)
    {
        var propagateClick = function (event, item) { this.fireEvent(EasySlide.EVENT.ITEM_CLICKED, $H({ type: 'EasySlideEvent', event: EasySlide.EVENT.ITEM_CLICKED, srcEvent: event, src: item})); }.bindWithEvent(this, item);
        var propagateItemEnter = function (event, item) { this.fireEvent(EasySlide.EVENT.ITEM_MOUSEENTER, $H({ type: 'EasySlideEvent', event: EasySlide.EVENT.ITEM_MOUSEENTER, srcEvent: event, src: item})); }.bindWithEvent(this, item);
        var propagateItemLeave = function (event, item) { this.fireEvent(EasySlide.EVENT.ITEM_MOUSELEAVE, $H({type: 'EasySlideEvent', event: EasySlide.EVENT.ITEM_MOUSELEAVE, srcEvent: event, src: item})); }.bindWithEvent(this, item)
        item.get('element').addEvents({ 'click': propagateClick.bindWithEvent(this, item), 'mouseenter': propagateItemEnter.bindWithEvent(this, item), 'mouseleave': propagateItemLeave.bindWithEvent(this, item) });
        return item;
    }, 
    
    /**
      * Internal method wrapped by slideIntoScreen() and SlideTo().
      * Does the actual sliding.
      *
      * @access protected
      *
      * @param int initialIndex The item to slide to.
      */
    slideToIndex: function(initialIndex)
    {
        initialIndex = (initialIndex < 0) ? 0 : (initialIndex > this.listItems.length - 1) ? this.listItems.length - 1 : initialIndex;
        var targetItem = this.findItemByInitialIndex(initialIndex);        
        if (!$chk(targetItem)) return;
        this.targetItem = targetItem;
        this.loop = true;
        var dir = this.determineDirection(targetItem);
        this.slide(dir);
    },
    
    /**
      * Method called each time an item has finished sliding through the screen.
      * We resort our listItems, if the direction was right to left, in order to provide a clean initial state for the next call to slide.
      * Then we check, if we are currently sliding towards a target and if necessary, check if we have already reached it.
      * Finally we unlock our slider for upcoming slide requests and check if we are in a loop to continue sliding if reuqired.
      * 
      * @access protected
      */
    onComplete: function()
    {
        if  (EasySlide.DIRECTION.R2L === this.direction || EasySlide.DIRECTION.B2T === this.direction)
        {
            this.listItems.push(this.listItems.shift());
            this.current.get('element').inject(this.element);
            var styleName = 'width' === this.slideModifier.get('attributeName') ? 'left' : 'top';
            this.element.get('tween').set(styleName, 0);
            this.current = this.listItems[0];
        }        
        this.checkTarget();
        this.isSliding = false;        
        this.fireEvent(EasySlide.EVENT.SLIDE_PROGRESS, $H({type: 'EasySlideEvent', name: EasySlide.EVENT.SLIDE_PROGRESS, src: this.current, direction: this.direction}));
            
        if (true === this.loop)
        {
            this.slide(this.direction);
            return;
        }
        this.fireEvent(EasySlide.EVENT.SLIDE_STOP, $H({type: 'EasySlideEvent', name: EasySlide.EVENT.SLIDE_STOP, src: this, direction: this.direction}));
    },
    
    /**
      * Check if we are sliding towards a certain target and if it has been reached.
      *
      * @access protected
      */
    checkTarget: function()
    {        
        if (!$pick(this.targetItem, false)) return;
          
        if ((this.get('seekMode') === EasySlide.SEEKMODE.TOFIRST && this.isTargetHit())
        || (this.get('seekMode') === EasySlide.SEEKMODE.TOVISIBLE && this.isItemVisible(this.targetItem.get('index'))))
        {
            this.fireEvent(EasySlide.EVENT.TARGET_REACHED, $H({type: 'EasySlideEvent', name: EasySlide.EVENT.TARGET_REACHED, src: this, direction: this.direction, target: this.targetItem}));
            this.targetItem = null;
            this.loop = false;
        }      
    },
    
    /**
      * Determine whether the current target has been reached or not.
      *
      * @access protected
      *
      * @return boolean
      */
    isTargetHit: function()
    {
        return (this.targetItem.get('index') === this.current.get('index'));
    },
    
    /**
      * Find out what direction we need to slide to in order to reach the given element.
      * Depending on the value set for options.pathMode the direction following will be returned:
      * - initialOrder: The direction will be determined considering the initial order of the list items.
      * - fastestPath: The direction will be returned by checking the fastest way to reach target. 
      * 
      * @access protected
      *
      * @param object The element we want to reach.
      *
      * @return string The direction to take.
      */
    determineDirection: function(targetItem)
    {
        var direction = (this.current.get('index') >= targetItem.get('index')) ? EasySlide.DIRECTION.L2R : EasySlide.DIRECTION.R2L;
        
        if (this.get('mode') === EasySlide.ROTATION.KARUSELL && this.get('pathMode') === EasySlide.SEEKMODE.FASTPATH)
        {
            var leftDiff = this.listItems.length - this.listIetms.indexOf(targetItem);            
            direction = (leftDiff < this.listItems.indexOf(targetItem)) ? EasySlide.DIRECTION.L2R : EasySlide.DIRECTION.R2L;
        }
        
        if (EasySlide.ALIGN.VERTICAL === this.get('align'))
        {
            direction = (direction === EasySlide.DIRECTION.L2R) ? EasySlide.DIRECTION.T2B : EasySlide.DIRECTION.B2T;  
        }
        return direction;
    },
    
    /**
      * Notify all listeners registered to the given event.
      *
      * @access protected
      *
      * @param string event_name The name of the event to fire.
      * @param object The event to pass the to the registered event callbacks. 
      */
    fireEvent: function(eventName, event)
    {
        if (!this.eventListeners.has(eventName)) return;
        var handleCallback = function(callbackFunc){ callbackFunc(event); }
        $each(this.eventListeners.get(eventName), handleCallback);
    }
});
/**
  * Options you can use to influence the slide behaviour.
  */
EasySlide.ROTATION = 
{
    /**
      * Endless rotation in both directions.
      */
    KARUSSEL: 1, 
    /**
      * The slider will use the first and last item as boundries for sliding.
      */
    SINGLE: 2
};
/**
  * Controlls the way we decide which direction to take when told to slide to an
  * item that is not directly next to us.
  */
EasySlide.PATHMODE = 
{
    /**
      * Requires EasySlide.ROTATION.KARUSSEL
      * Prefer the shorter path prior to the correct order. 
      */
    FASTPATH: 4, 
    /**
      * Always choose the direction so that we slide the items in the correct order.
      */
    ORDERED: 8
};
/**
  * Control whether we want the slider to slide until a searched item is on position one
  * or only until it is visible.
  */
EasySlide.SEEKMODE = 
{
    /**
      * Stop sliding as soon as the item is visible.
      */
    TOVISIBLE: 16, 
    /**
      * Slide untill the searched item is at position one.
      */
    TOFIRST: 32
};
/**
 * Directions you can pass to EasySlide when using slide().
 */
EasySlide.DIRECTION = {L2R: 'L2R', R2L: 'R2L', T2B: 'T2B', B2T: 'B2T'};
/**
  * Determines whether we are running a horizontal or vertically aligned slider.
  */
EasySlide.ALIGN = {VERTICAL: 'vertical', HORIZONTAL: 'horizontal'};
/**
 * Tell EasySlide to start on the first or the last item.
 */
EasySlide.STARTPOS = {FIRST: 64, LAST: 128};
/**
  * Events you can register for.
  */
EasySlide.EVENT = 
{
    /**
      * Fired when an item was clicked.
      */
    ITEM_CLICKED: 1, 
    /**
      * Fired when the mouse enters a slider item.
      */
    ITEM_MOUSEENTER: 2, 
    /**
      * Fired when the mouse leaves a slider item.
      */
    ITEM_MOUSELEAVE: 3, 
    /**
      * Fired when the slider finished building.
      */
    BUILD_COMPLETE: 4, 
    /**
      * Fired everytime the slider starts to slide.
      */
    SLIDE_START: 5, 
    /**
      * Fired every time the slider stops to slide.
      */
    SLIDE_STOP: 6, 
    /**
      * Fired everytime an item has finished sliding.
      */
    SLIDE_PROGRESS: 7,
    /**
      * Fired when an item is reached that was selected by a call to slideTo() or slideIntoScreen().
      */
    TARGET_REACHED: 8
};
