/* 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 33 2008-12-10 22:55:28Z 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)
 */

(function($) {

$.jScrollPane = {
  active : []
};
$.fn.jScrollPane = function(settings)
{
  settings = $.extend({}, $.fn.jScrollPane.defaults, settings);

  var rf = function() { return false; };

  return this.each(
    function()
    {
      var $this = $(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 ($(this).parent().is('.jScrollPaneContainer')) {
        var currentScrollPosition = settings.maintainPosition ? $this.position().top : 0;
        var $c = $(this).parent();
        var paneWidth = $c.innerWidth();
        var paneHeight = $c.outerHeight();
        var trackHeight = paneHeight;
        $('>.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(
          $('<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
        $(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($.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(
          $('<div></div>').attr({'className':'jScrollPaneTrack'}).css({'width':settings.scrollbarWidth+'px'}).append(
            $('<div></div>').attr({'className':'jScrollPaneDrag'}).css({'width':settings.scrollbarWidth+'px'}).append(
              $('<div></div>').attr({'className':'jScrollPaneDragTop'}).css({'width':settings.scrollbarWidth+'px'}),
              $('<div></div>').attr({'className':'jScrollPaneDragBottom'}).css({'width':settings.scrollbarWidth+'px'})
            )
          )
        );

        var $track = $('>.jScrollPaneTrack', $container);
        var $drag = $('>.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)
          {
            $('html').unbind('mouseup', onArrowMouseUp);
            currentArrowButton.removeClass('jScrollActiveArrowButton');
            clearInterval(currentArrowInterval);
          };
          var onArrowMouseDown = function() {
            $('html').bind('mouseup', onArrowMouseUp);
            currentArrowButton.addClass('jScrollActiveArrowButton');
            currentArrowInc = 0;
            whileArrowButtonDown();
            currentArrowInterval = setInterval(whileArrowButtonDown, 100);
          };
          $container
            .append(
              $('<a></a>')
                .attr({'href':'javascript:;', 'className':'jScrollArrowUp'})
                .css({'width':settings.scrollbarWidth+'px'})
                .html('Scroll up')
                .bind('mousedown', function()
                {
                  currentArrowButton = $(this);
                  currentArrowDirection = -1;
                  onArrowMouseDown();
                  this.blur();
                  return false;
                })
                .bind('click', rf),
              $('<a></a>')
                .attr({'href':'javascript:;', 'className':'jScrollArrowDown'})
                .css({'width':settings.scrollbarWidth+'px'})
                .html('Scroll down')
                .bind('mousedown', function()
                {
                  currentArrowButton = $(this);
                  currentArrowDirection = 1;
                  onArrowMouseDown();
                  this.blur();
                  return false;
                })
                .bind('click', rf)
            );
          var $upArrow = $('>.jScrollArrowUp', $container);
          var $downArrow = $('>.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 = $(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;
          $('html').bind('mouseup', onStopDrag).bind('mousemove', updateScroll);
          if ($.browser.msie) {
            $('html').bind('dragstart', ignoreNativeDrag).bind('selectstart', ignoreNativeDrag);
          }
          return false;
        };
        var onStopDrag = function()
        {
          $('html').unbind('mouseup', onStopDrag).unbind('mousemove', updateScroll);
          dragMiddle = percentInView*paneHeight/2;
          if ($.browser.msie) {
            $('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 = 39;

        $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);
          $('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;
          $('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 = $(pos, $this);
            if (!$e.length) return;
            pos = $e.offset().top - $this.offset().top;
          }
          $container.scrollTop(0);
          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 $e = $(this);

            // loop through parents adding the offset top of any elements that are relatively positioned between
            // the focused element and the jScrollPaneContainer so we can get the true distance from the top
            // of the focused element to the top of the scrollpane...
            var eleTop = 0;

            while ($e[0] != $this[0]) {
              eleTop += $e.position().top;
              $e = $e.offsetParent();
            }

            var viewportTop = -parseInt($pane.css('top')) || 0;
            var maxVisibleEleTop = viewportTop + paneHeight;
            var eleInView = eleTop > viewportTop && eleTop < maxVisibleEleTop;
            if (!eleInView) {
              var destPos = eleTop - settings.scrollbarMargin;
              if (eleTop > viewportTop) { // element is below viewport - scroll so it is at bottom.
                destPos += $(this).height() + 15 + settings.scrollbarMargin - paneHeight;
              }
              scrollTo(destPos);
            }
          }
        )


        if (location.hash) {
          scrollTo(location.hash);
        }

        // 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');
              if (h.substr(0, 1) == '#') {
                scrollTo(h);
              }
            }
          }
        );

        $.jScrollPane.active.push($this[0]);

      } else {
        $this.css(
          {
            'height':paneHeight+'px',
            'width':paneWidth-this.originalSidePaddingTotal+'px',
            'padding':this.originalPadding
          }
        );
        // remove from active list?
        $this.parent().unbind('mousewheel');
      }

    }
  )
};

$.fn.jScrollPane.defaults = {
  scrollbarWidth : 9,
  scrollbarMargin : 0,
  wheelSpeed : 18,
  showArrows : false,
  arrowSize : 6,
  animateTo : false,
  dragMinHeight : 1,
  dragMaxHeight : 99999,
  animateInterval : 100,
  animateStep: 3,
  maintainPosition: true,
  scrollbarOnLeft: false,
  reinitialiseOnImageLoad: false
};

// clean up the scrollTo expandos
$(window)
  .bind('unload', function() {
    var els = $.jScrollPane.active;
    for (var i=0; i<els.length; i++) {
      els[i].scrollTo = els[i].scrollBy = null;
    }
  }
);

})(jQuery);