/* Copyright (c) 2006 Kelvin Luck (kelvin AT kelvinluck DOT com || http://www.kelvinluck.com)
 * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 
 * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
 * 
 * See http://kelvinluck.com/assets/jquery/jScrollPane/
 * $Id: jScrollPane.js 19 2008-11-13 06:00:09Z kelvin.luck $
 */

/**
 * Replace the vertical scroll bars on any matched elements with a fancy
 * styleable (via CSS) version. With JS disabled the elements will
 * gracefully degrade to the browsers own implementation of overflow:auto.
 * If the mousewheel plugin has been included on the page then the scrollable areas will also
 * respond to the mouse wheel.
 *
 * @example jQuery(".scroll-pane").jScrollPane();
 *
 * @name jScrollPane
 * @type jQuery
 * @param Object  settings hash with options, described below.
 *                      scrollbarWidth -  The width of the generated scrollbar in pixels
 *                      scrollbarMargin   -  The amount of space to leave on the side of the scrollbar in pixels
 *                      wheelSpeed     -  The speed the pane will scroll in response to the mouse wheel in pixels
 *                      showArrows     -  Whether to display arrows for the user to scroll with
 *                      arrowSize      -  The height of the arrow buttons if showArrows=true
 *                      animateTo      -  Whether to animate when calling scrollTo and scrollBy
 *                      dragMinHeight  -  The minimum height to allow the drag bar to be
 *                      dragMaxHeight  -  The maximum height to allow the drag bar to be
 *                      animateInterval   -  The interval in milliseconds to update an animating scrollPane (default 100)
 *                      animateStep    -  The amount to divide the remaining scroll distance by when animating (default 3)
 *                      maintainPosition- Whether you want the contents of the scroll pane to maintain it's position when you re-initialise it - so it doesn't scroll as you add more content (default true)
 *                      scrollbarOnLeft   -  Display the scrollbar on the left side?  (needs stylesheet changes, see examples.html)
 *                      reinitialiseOnImageLoad - Whether the jScrollPane should automatically re-initialise itself when any contained images are loaded
 * @return jQuery
 * @cat Plugins/jScrollPane
 * @author Kelvin Luck (kelvin AT kelvinluck DOT com || http://www.kelvinluck.com)
 */
jQuery.jScrollPane = {
   active : []
};
jQuery.fn.jScrollPane = function(settings)
{
   settings = jQuery.extend({}, jQuery.fn.jScrollPane.defaults, settings);

   var rf = function() { return false; };
   
   return this.each(
      function()
      {
         var $this = jQuery(this);
         // Switch the element's overflow to hidden to ensure we get the size of the element without the scrollbars [http://plugins.jquery.com/node/1208]
         $this.css('overflow', 'hidden');
         var paneEle = this;
         
         if (jQuery(this).parent().is('.jScrollPaneContainer')) {
            var currentScrollPosition = settings.maintainPosition ? $this.offset({relativeTo:jQuery(this).parent()[0]}).top : 0;
            var $c = jQuery(this).parent();
            var paneWidth = $c.innerWidth();
            var paneHeight = $c.outerHeight();
            var trackHeight = paneHeight;
            jQuery('>.jScrollPaneTrack, >.jScrollArrowUp, >.jScrollArrowDown', $c).remove();
            $this.css({'top':0});
         } else {
            var currentScrollPosition = 0;
            this.originalPadding = $this.css('paddingTop') + ' ' + $this.css('paddingRight') + ' ' + $this.css('paddingBottom') + ' ' + $this.css('paddingLeft');
            this.originalSidePaddingTotal = (parseInt($this.css('paddingLeft')) || 0) + (parseInt($this.css('paddingRight')) || 0);
            var paneWidth = $this.innerWidth();
            var paneHeight = $this.innerHeight();
            var trackHeight = paneHeight;
            $this.wrap(
               jQuery('<div></div>').attr(
                  {'className':'jScrollPaneContainer'}
               ).css(
                  {
                     'height':paneHeight+'px', 
                     'width':paneWidth+'px'
                  }
               )
            );
            // deal with text size changes (if the jquery.em plugin is included)
            // and re-initialise the scrollPane so the track maintains the
            // correct size
            jQuery(document).bind(
               'emchange', 
               function(e, cur, prev)
               {
                  $this.jScrollPane(settings);
               }
            );
            
         }
         
         if (settings.reinitialiseOnImageLoad) {
            // code inspired by jquery.onImagesLoad: http://plugins.jquery.com/project/onImagesLoad
            // except we re-initialise the scroll pane when each image loads so that the scroll pane is always up to size...
            // TODO: Do I even need to store it in $.data? Is a local variable here the same since I don't pass the reinitialiseOnImageLoad when I re-initialise?
            var $imagesToLoad = $.data(paneEle, 'jScrollPaneImagesToLoad') || $('img', $this);
            var loadedImages = [];
            
            if ($imagesToLoad.length) {
               $imagesToLoad.each(function(i, val) {
                  $(this).bind('load', function() {
                     if(jQuery.inArray(i, loadedImages) == -1){ //don't double count images
                        loadedImages.push(val); //keep a record of images we've seen
                        $imagesToLoad = $.grep($imagesToLoad, function(n, i) {
                           return n != val;
                        });
                        $.data(paneEle, 'jScrollPaneImagesToLoad', $imagesToLoad);
                        settings.reinitialiseOnImageLoad = false;
                        $this.jScrollPane(settings); // re-initialise
                     }
                  }).each(function(i, val) {
                     if(this.complete || this.complete===undefined) { 
                        //needed for potential cached images
                        this.src = this.src; 
                     } 
                  });
               });
            };
         }

         var p = this.originalSidePaddingTotal;
         
         var cssToApply = {
            'height':'auto',
            'width':paneWidth - settings.scrollbarWidth - settings.scrollbarMargin - p + 'px'
         }

         if(settings.scrollbarOnLeft) {
            cssToApply.paddingLeft = settings.scrollbarMargin + settings.scrollbarWidth + 'px';
         } else {
            cssToApply.paddingRight = settings.scrollbarMargin + 'px';
         }

         $this.css(cssToApply);

         var contentHeight = $this.outerHeight();
         var percentInView = paneHeight / contentHeight;

         if (percentInView < .99) {
            var $container = $this.parent();
            $container.append(
               jQuery('<div></div>').attr({'className':'jScrollPaneTrack'}).css({'width':settings.scrollbarWidth+'px'}).append(
                  jQuery('<div></div>').attr({'className':'jScrollPaneDrag'}).css({'width':settings.scrollbarWidth+'px'}).append(
                     jQuery('<div></div>').attr({'className':'jScrollPaneDragTop'}).css({'width':settings.scrollbarWidth+'px'}),
                     jQuery('<div></div>').attr({'className':'jScrollPaneDragBottom'}).css({'width':settings.scrollbarWidth+'px'})
                  )
               )
            );
            
            var $track = jQuery('>.jScrollPaneTrack', $container);
            var $drag = jQuery('>.jScrollPaneTrack .jScrollPaneDrag', $container);
            
            if (settings.showArrows) {
               
               var currentArrowButton;
               var currentArrowDirection;
               var currentArrowInterval;
               var currentArrowInc;
               var whileArrowButtonDown = function()
               {
                  if (currentArrowInc > 4 || currentArrowInc%4==0) {
                     positionDrag(dragPosition + currentArrowDirection * mouseWheelMultiplier);
                  }
                  currentArrowInc ++;
               };
               var onArrowMouseUp = function(event)
               {
                  jQuery('html').unbind('mouseup', onArrowMouseUp);
                  currentArrowButton.removeClass('jScrollActiveArrowButton');
                  clearInterval(currentArrowInterval);
                  //console.log($(event.target));
                  //currentArrowButton.parent().removeClass('jScrollArrowUpClicked jScrollArrowDownClicked');
               };
               var onArrowMouseDown = function() {
                  //console.log(direction);
                  //currentArrowButton = $(this);
                  jQuery('html').bind('mouseup', onArrowMouseUp);
                  currentArrowButton.addClass('jScrollActiveArrowButton');
                  currentArrowInc = 0;
                  whileArrowButtonDown();
                  currentArrowInterval = setInterval(whileArrowButtonDown, 100);
               };
               $container
                  .append(
                     jQuery('<a></a>')
                        .attr({'href':'javascript:;', 'className':'jScrollArrowUp'})
                        .css({'width':settings.scrollbarWidth+'px'})
                        .html('Scroll up')
                        .bind('mousedown', function()
                        {
                           currentArrowButton = jQuery(this);
                           currentArrowDirection = -6;
                           onArrowMouseDown();
                           this.blur();
                           return false;
                        })
                        .bind('click', rf),
                     jQuery('<a></a>')
                        .attr({'href':'javascript:;', 'className':'jScrollArrowDown'})
                        .css({'width':settings.scrollbarWidth+'px'})
                        .html('Scroll down')
                        .bind('mousedown', function()
                        {
                           currentArrowButton = jQuery(this);
                           currentArrowDirection = 6;
                           onArrowMouseDown();
                           this.blur();
                           return false;
                        })
                        .bind('click', rf)
                  );
               var $upArrow = jQuery('>.jScrollArrowUp', $container);
               var $downArrow = jQuery('>.jScrollArrowDown', $container);
               if (settings.arrowSize) {
                  trackHeight = paneHeight - settings.arrowSize - settings.arrowSize;
                  $track
                     .css({'height': trackHeight+'px', top:settings.arrowSize+'px'})
               } else {
                  var topArrowHeight = $upArrow.height();
                  settings.arrowSize = topArrowHeight;
                  trackHeight = paneHeight - topArrowHeight - $downArrow.height();
                  $track
                     .css({'height': trackHeight+'px', top:topArrowHeight+'px'})
               }
            }
            
            var $pane = jQuery(this).css({'position':'absolute', 'overflow':'visible'});
            
            var currentOffset;
            var maxY;
            var mouseWheelMultiplier;
            // store this in a seperate variable so we can keep track more accurately than just updating the css property..
            var dragPosition = 0;
            var dragMiddle = percentInView*paneHeight/2;
            
            // pos function borrowed from tooltip plugin and adapted...
            var getPos = function (event, c) {
               var p = c == 'X' ? 'Left' : 'Top';
               return event['page' + c] || (event['client' + c] + (document.documentElement['scroll' + p] || document.body['scroll' + p])) || 0;
            };
            
            var ignoreNativeDrag = function() { return false; };
            
            var initDrag = function()
            {
               ceaseAnimation();
               currentOffset = $drag.offset(false);
               currentOffset.top -= dragPosition;
               maxY = trackHeight - $drag[0].offsetHeight;
               mouseWheelMultiplier = 2 * settings.wheelSpeed * maxY / contentHeight;
            };
            
            var onStartDrag = function(event)
            {
               initDrag();
               dragMiddle = getPos(event, 'Y') - dragPosition - currentOffset.top;
               jQuery('html').bind('mouseup', onStopDrag).bind('mousemove', updateScroll);
               if (jQuery.browser.msie) {
                  jQuery('html').bind('dragstart', ignoreNativeDrag).bind('selectstart', ignoreNativeDrag);
               }
               return false;
            };
            var onStopDrag = function()
            {
               jQuery('html').unbind('mouseup', onStopDrag).unbind('mousemove', updateScroll);
               dragMiddle = percentInView*paneHeight/2;
               if (jQuery.browser.msie) {
                  jQuery('html').unbind('dragstart', ignoreNativeDrag).unbind('selectstart', ignoreNativeDrag);
               }
            };
            var positionDrag = function(destY)
            {
               destY = destY < 0 ? 0 : (destY > maxY ? maxY : destY);
               dragPosition = destY;
               $drag.css({'top':destY+'px'});
               var p = destY / maxY;
               $pane.css({'top':((paneHeight-contentHeight)*p) + 'px'});
               $this.trigger('scroll');
               if (settings.showArrows) {
                  $upArrow[destY == 0 ? 'addClass' : 'removeClass']('disabled');
                  $downArrow[destY == maxY ? 'addClass' : 'removeClass']('disabled');
               }
            };
            var updateScroll = function(e)
            {
               positionDrag(getPos(e, 'Y') - currentOffset.top - dragMiddle);
            };
            
            var dragH = Math.max(Math.min(percentInView*(paneHeight-settings.arrowSize*2), settings.dragMaxHeight), settings.dragMinHeight);
            
            $drag.css(
               {'height':dragH+'px'}
            ).bind('mousedown', onStartDrag);
            
            var trackScrollInterval;
            var trackScrollInc;
            var trackScrollMousePos;
            var doTrackScroll = function()
            {
               if (trackScrollInc > 8 || trackScrollInc%4==0) {
                  positionDrag((dragPosition - ((dragPosition - trackScrollMousePos) / 2)));
               }
               trackScrollInc ++;
            };
            var onStopTrackClick = function()
            {
               clearInterval(trackScrollInterval);
               jQuery('html').unbind('mouseup', onStopTrackClick).unbind('mousemove', onTrackMouseMove);
            };
            var onTrackMouseMove = function(event)
            {
               trackScrollMousePos = getPos(event, 'Y') - currentOffset.top - dragMiddle;
            };
            var onTrackClick = function(event)
            {
               initDrag();
               onTrackMouseMove(event);
               trackScrollInc = 0;
               jQuery('html').bind('mouseup', onStopTrackClick).bind('mousemove', onTrackMouseMove);
               trackScrollInterval = setInterval(doTrackScroll, 100);
               doTrackScroll();
            };
            
            $track.bind('mousedown', onTrackClick);
            
            $container.bind(
               'mousewheel',
               function (event, delta) {
                  initDrag();
                  ceaseAnimation();
                  var d = dragPosition;
                  positionDrag(dragPosition - delta * mouseWheelMultiplier);
                  var dragOccured = d != dragPosition;
                  return !dragOccured;
               }
            );

            var _animateToPosition;
            var _animateToInterval;
            function animateToPosition()
            {
               var diff = (_animateToPosition - dragPosition) / settings.animateStep;
               if (diff > 1 || diff < -1) {
                  positionDrag(dragPosition + diff);
               } else {
                  positionDrag(_animateToPosition);
                  ceaseAnimation();
               }
            }
            var ceaseAnimation = function()
            {
               if (_animateToInterval) {
                  clearInterval(_animateToInterval);
                  delete _animateToPosition;
               }
            };
            var scrollTo = function(pos, preventAni)
            {
               if (typeof pos == "string") {
                  $e = jQuery(pos, this);
                  if (!$e.length) return;
                  pos = $e.offset().top - $this.offset().top;
               }
               ceaseAnimation();
               var destDragPosition = -pos/(paneHeight-contentHeight) * maxY;
               if (preventAni || !settings.animateTo) {
                  positionDrag(destDragPosition);
               } else {
                  _animateToPosition = destDragPosition;
                  _animateToInterval = setInterval(animateToPosition, settings.animateInterval);
               }
            };
            $this[0].scrollTo = scrollTo;
            
            $this[0].scrollBy = function(delta)
            {
               var currentPos = -parseInt($pane.css('top')) || 0;
               scrollTo(currentPos + delta);
            };
            
            initDrag();
            
            scrollTo(-currentScrollPosition, true);
         
            // Deal with it when the user tabs to a link or form element within this scrollpane
            $('*', this).bind(
               'focus',
               function(event)
               {
                  var eleTop = $(this).position().top;
                  var viewportTop = -parseInt($pane.css('top')) || 0;
                  var maxVisibleEleTop = viewportTop + paneHeight;
                  var eleInView = eleTop > viewportTop && eleTop < maxVisibleEleTop;
                  if (!eleInView) {
                     $container.scrollTop(0);
                     var destPos = eleTop - settings.scrollbarMargin;
                     if (eleTop > viewportTop) { // element is below viewport - scroll so it is at bottom.
                        destPos += $(this).height() + 40+ settings.scrollbarMargin - paneHeight;
                     }
                     scrollTo(destPos);
                  }
               }
            )
            
            
            if (location.hash) {
               // the timeout needs to be longer in IE when not loading from cache...
               setTimeout(function() {
                  $(location.hash, $this).trigger('focus');
               }, $.browser.msie ? 100 : 0);
            }
            
            // use event delegation to listen for all clicks on links and hijack them if they are links to
            // anchors within our content...
            $(document).bind(
               'click',
               function(e)
               {
                  $target = $(e.target);
                  if ($target.is('a')) {
                     var h = $target.attr('href');
                     console.log(h);
                     if (h.substr(0, 1) == '#') {
                        $linkedEle = $(h, $this);
                        console.log($linkedEle);
                        if ($linkedEle.length) {
                           $linkedEle.trigger('focus');
                           return false;
                        }
                     }
                  }
               }
            );
            
            jQuery.jScrollPane.active.push($this[0]);
            
         } else {
            $this.css(
               {
                  'height':paneHeight+'px',
                  'width':paneWidth-this.originalSidePaddingTotal+'px',
                  'padding':this.originalPadding
               }
            );
            // remove from active list?
         }
         
      }
   )
};

jQuery.fn.jScrollPane.defaults = {
   scrollbarWidth : 40,
   scrollbarMargin : 0,
   wheelSpeed : 18,
   showArrows : false,
   arrowSize : 0,
   animateTo : true,
   dragMinHeight : 50,
   dragMaxHeight : 99999,
   animateInterval : 100,
   animateStep: 3,
   maintainPosition: true,
   scrollbarOnLeft: false,
   reinitialiseOnImageLoad: false
};

// clean up the scrollTo expandos
jQuery(window)
   .bind('unload', function() {
      var els = jQuery.jScrollPane.active; 
      for (var i=0; i<els.length; i++) {
         els[i].scrollTo = els[i].scrollBy = null;
      }
   }
);
