// SmoothScroll v1.2.1
// Licensed under the terms of the MIT license.

// People involved
//  - Balazs Galambosi (maintainer)  
//  - Patrick Brunner  (original idea)
//  - Michael Herf     (Pulse Algorithm)
//  - Justin Force     (Resurect)

// Scroll Variables (tweakable)
var framerate = 150; // [Hz]
var animtime  = 800; // [px]
var stepsize  = 80; // [px]

// Pulse (less tweakable)
// ratio of "tail" to "acceleration"
var pulseAlgorithm = true;
var pulseScale     = 8;
var pulseNormalize = 1;

// Acceleration
var acceleration   = true;
var accelDelta     = 10;  // 20
var accelMax       = 1;   // 1

// Keyboard Settings
var keyboardsupport = true;  // option
var disableKeyboard = false; // other reasons
var arrowscroll     = 50;    // [px]

// Excluded pages
var exclude = "";
var disabled = false;

// Other Variables
var frame = false;
var direction = { x: 0, y: 0 };
var initdone  = false;
var fixedback = true;
var root = document.documentElement;
var activeElement;

var key = { left: 37, up: 38, right: 39, down: 40, spacebar: 32, pageup: 33, pagedown: 34, end: 35, home: 36 };

/**
 * Sets up scrolls array, determines if frames are involved.
 */
function init() {

  if (!document.body) return;

  var body = document.body;
  var html = document.documentElement;
  var windowHeight = window.innerHeight;
  var scrollHeight = body.scrollHeight;

  // check compat mode for root element
  root = (document.compatMode.indexOf('CSS') >= 0) ? html : body;
  activeElement = body;

  initdone = true;

  // Checks if this script is running in a frame
  if (top != self) {
    frame = true;
  }

  /**
   * This fixes a bug where the areas left and right to
   * the content does not trigger the onmousewheel event
   * on some pages. e.g.: html, body { height: 100% }
   */
  else if (scrollHeight > windowHeight &&
           (body.offsetHeight <= windowHeight ||
            html.offsetHeight <= windowHeight)) {

    // DOMChange (throttle): fix height
    var pending = false;
    var refresh = function() {
      if (!pending && html.scrollHeight != document.height) {
        pending = true; // add a new pending action
        setTimeout(function(){
          html.style.height = document.height + 'px';
          pending = false;
        }, 500); // act rarely to stay fast
      }
    };
    html.style.height = '';
    setTimeout(refresh, 10);
    addEvent("DOMNodeInserted", refresh);
    addEvent("DOMNodeRemoved",  refresh);

    // clearfix
    if (root.offsetHeight <= windowHeight) {
      var underlay = document.createElement("div");
      underlay.style.clear = "both";
      body.appendChild(underlay);
    }
  }

  // gmail performance fix
  if (document.URL.indexOf("mail.google.com") > -1) {
    var s = document.createElement("style");
    s.innerHTML = ".iu { visibility: hidden }";
    (document.getElementsByTagName("head")[0] || html).appendChild(s);
  }
  // disable fixed background
  if (!fixedback && !disabled) {
    body.style.backgroundAttachment = "scroll";
    html.style.backgroundAttachment = "scroll";
  }
}


/************************************************
 * SCROLLING
 ************************************************/

var que = [];
var pending = false;
var lastScroll = +new Date;

/**
 * Pushes scroll actions to the scrolling queue.
 */
function scrollArray(elem, left, top, delay) {

  delay || (delay = 1000);
  directionCheck(left, top);

  if (acceleration) {
    var now = +new Date;
    var elapsed = now - lastScroll;
    if (elapsed < accelDelta) {
      var factor = (1 + (30 / elapsed)) / 2;
      if (factor > 1) {
        factor = Math.min(factor, accelMax);
        left *= factor;
        top  *= factor;
      }
    }
    lastScroll = +new Date;
  }

  // push a scroll command
  que.push({
    x: left,
    y: top,
    lastX: (left < 0) ? 0.99 : -0.99,
    lastY: (top  < 0) ? 0.99 : -0.99,
    start: +new Date
  });

  // don't act if there's a pending queue
  if (pending) {
    return;
  }

  var scrollWindow = (elem === document.body);

  var step = function() {

    var now = +new Date;
    var scrollX = 0;
    var scrollY = 0;

    for (var i = 0; i < que.length; i++) {

      var item = que[i];
      var elapsed  = now - item.start;
      var finished = (elapsed >= animtime);

      // scroll position: [0, 1]
      var position = (finished) ? 1 : elapsed / animtime;

      // easing [optional]
      if (pulseAlgorithm) {
        position = pulse(position);
      }

      // only need the difference
      var x = (item.x * position - item.lastX) >> 0;
      var y = (item.y * position - item.lastY) >> 0;

      // add this to the total scrolling
      scrollX += x;
      scrollY += y;

      // update last values
      item.lastX += x;
      item.lastY += y;

      // delete and step back if it's over
      if (finished) {
        que.splice(i, 1); i--;
      }
    }

    // scroll left and top
    if (scrollWindow) {
      window.scrollBy(scrollX, scrollY)
    }
    else {
      if (scrollX) elem.scrollLeft += scrollX;
      if (scrollY) elem.scrollTop  += scrollY;
    }

    // clean up if there's nothing left to do
    if (!left && !top) {
      que = [];
    }

    if (que.length) {
      requestFrame(step, elem, (delay / framerate + 1));
    } else {
      pending = false;
    }
  }

  // start a new queue of actions
  requestFrame(step, elem, 0);
  pending = true;
}


/***********************************************
 * EVENTS
 ***********************************************/

/**
 * Mouse wheel handler.
 * @param {Object} event
 */
function wheel(event) {

  if (!initdone) {
    init();
  }

  var target = event.target;
  var overflowing = overflowingAncestor(target);

  // use default if there's no overflowing
  // element or default action is prevented
  if (!overflowing || event.defaultPrevented ||
      isNodeName(activeElement, "embed") ||
        (isNodeName(target, "embed") && /\.pdf/i.test(target.src))) {
    return true;
  }

  var deltaX = event.wheelDeltaX || 0;
  var deltaY = event.wheelDeltaY || 0;

  // use wheelDelta if deltaX/Y is not available
  if (!deltaX && !deltaY) {
    deltaY = event.wheelDelta || 0;
  }

  // scale by step size
  // delta is 120 most of the time
  // synaptics seems to send 1 sometimes
  if (Math.abs(deltaX) > 1.2) {
    deltaX *= stepsize / 120;
  }
  if (Math.abs(deltaY) > 1.2) {
    deltaY *= stepsize / 120;
  }

  scrollArray(overflowing, -deltaX, -deltaY);
  event.preventDefault();
}

/**
 * Keydown event handler.
 * @param {Object} event
 */
function keydown(event) {

  var target   = event.target;
  var modifier = event.ctrlKey || event.altKey || event.metaKey ||
    (event.shiftKey && event.keyCode !== key.spacebar);

  // do nothing if user is editing text
  // or using a modifier key (except shift)
  // or in a dropdown
  if ( /input|textarea|select|embed/i.test(target.nodeName) ||
      target.isContentEditable ||
        event.defaultPrevented   ||
          modifier ) {
    return true;
  }
  // spacebar should trigger button press
  if (isNodeName(target, "button") &&
      event.keyCode === key.spacebar) {
    return true;
  }

  var shift, x = 0, y = 0;
  var elem = overflowingAncestor(activeElement);
  var clientHeight = elem.clientHeight;

  if (elem == document.body) {
    clientHeight = window.innerHeight;
  }

  switch (event.keyCode) {
    case key.up:
      y = -arrowscroll;
    break;
    case key.down:
      y = arrowscroll;
    break;
    case key.spacebar: // (+ shift)
      shift = event.shiftKey ? 1 : -1;
    y = -shift * clientHeight * 0.9;
    break;
    case key.pageup:
      y = -clientHeight * 0.9;
    break;
    case key.pagedown:
      y = clientHeight * 0.9;
    break;
    case key.home:
      y = -elem.scrollTop;
    break;
    case key.end:
      var damt = elem.scrollHeight - elem.scrollTop - clientHeight;
    y = (damt > 0) ? damt+10 : 0;
    break;
    case key.left:
      x = -arrowscroll;
    break;
    case key.right:
      x = arrowscroll;
    break;
    default:
      return true; // a key we don't care about
  }

  scrollArray(elem, x, y);
  event.preventDefault();
}

/**
 * Mousedown event only for updating activeElement
 */
function mousedown(event) {
  activeElement = event.target;
}


/***********************************************
 * OVERFLOW
 ***********************************************/

var cache = {}; // cleared out every once in while
setInterval(function(){ cache = {}; }, 10 * 1000);

var uniqueID = (function() {
  var i = 0;
  return function (el) {
    return el.uniqueID || (el.uniqueID = i++);
  };
})();

function setCache(elems, overflowing) {
  for (var i = elems.length; i--;)
  cache[uniqueID(elems[i])] = overflowing;
  return overflowing;
}

function overflowingAncestor(el) {
  var elems = [];
  var rootScrollHeight = root.scrollHeight;
  do {
    var cached = cache[uniqueID(el)];
    if (cached) {
      return setCache(elems, cached);
    }
    elems.push(el);
    if (rootScrollHeight === el.scrollHeight) {
      if (!frame || root.clientHeight + 10 < rootScrollHeight) {
        return setCache(elems, document.body); // scrolling root in WebKit
      }
    } else if (el.clientHeight + 10 < el.scrollHeight) {
      overflow = getComputedStyle(el, "").getPropertyValue("overflow-y");
      if (overflow === "scroll" || overflow === "auto") {
        return setCache(elems, el);
      }
    }
  } while (el = el.parentNode);
}


/***********************************************
 * HELPERS
 ***********************************************/

function addEvent(type, fn, bubble) {
  window.addEventListener(type, fn, (bubble||false));
}

function removeEvent(type, fn, bubble) {
  window.removeEventListener(type, fn, (bubble||false));
}

function isNodeName(el, tag) {
  return (el.nodeName||"").toLowerCase() === tag.toLowerCase();
}

function directionCheck(x, y) {
  x = (x > 0) ? 1 : -1;
  y = (y > 0) ? 1 : -1;
  if (direction.x !== x || direction.y !== y) {
    direction.x = x;
    direction.y = y;
    que = [];
    lastScroll = 0;
  }
}

var requestFrame = (function(){
  return  window.requestAnimationFrame       ||
    window.webkitRequestAnimationFrame ||
    function(callback, element, delay){
    window.setTimeout(callback, delay || (1000/60));
  };
})();

/***********************************************
 * PULSE
 ***********************************************/

/**
 * Viscous fluid with a pulse for part and decay for the rest.
 * - Applies a fixed force over an interval (a damped acceleration), and
 * - Lets the exponential bleed away the velocity over a longer interval
 * - Michael Herf, http://stereopsis.com/stopping/
 */
function pulse_(x) {
  var val, start, expx;
  // test
  x = x * pulseScale;
  if (x < 1) { // acceleartion
    val = x - (1 - Math.exp(-x));
  } else {     // tail
    // the previous animation ended here:
    start = Math.exp(-1);
    // simple viscous drag
    x -= 1;
    expx = 1 - Math.exp(-x);
    val = start + (expx * (1 - start));
  }
  return val * pulseNormalize;
}

function pulse(x) {
  if (x >= 1) return 1;
  if (x <= 0) return 0;

  if (pulseNormalize == 1) {
    pulseNormalize /= pulse_(1);
  }
  return pulse_(x);
}

addEvent("mousedown", mousedown);
addEvent("mousewheel", wheel);
addEvent("load", init);
