/**
 * Check if object is part of the DOM
 * @constructor
 * @param {Object} obj element to check
 */
function isDOMElement(obj) {
  return obj && typeof window !== 'undefined' && (obj === window || obj.nodeType);
}

/**
 * Helper function for extending objects
 */
function extend (object /*, objectN ... */) {
  if(arguments.length <= 0) {
    throw new Error('Missing arguments in extend function');
  }

  var result = object || {},
      key,
      i;

  for (i = 1; i < arguments.length; i++) {
    var replacement = arguments[i] || {};

    for (key in replacement) {
      // Recurse into object except if the object is a DOM element
      if(typeof result[key] === 'object' && ! isDOMElement(result[key])) {
        result[key] = extend(result[key], replacement[key]);
      }
      else {
        result[key] = result[key] || replacement[key];
      }
    }
  }

  return result;
}

/**
 * Helper function for normalizing tolerance option to object format
 */
function normalizeTolerance (t) {
  return t === Object(t) ? t : { down : t, up : t };
}

/**
 * UI enhancement for fixed headers.
 * Hides header when scrolling down
 * Shows header when scrolling up
 * @constructor
 * @param {DOMElement} elem the header element
 * @param {Object} options options for the widget
 */
function Headroom (elem, options) {
  options = extend(options, Headroom.options);

  this.lastKnownScrollY = 0;
  this.elem             = elem;
  this.tolerance        = normalizeTolerance(options.tolerance);
  this.classes          = options.classes;
  this.offset           = options.offset;
  this.scroller         = options.scroller;
  this.initialised      = false;
  this.onPin            = options.onPin;
  this.onUnpin          = options.onUnpin;
  this.onTop            = options.onTop;
  this.onNotTop         = options.onNotTop;
  this.onBottom         = options.onBottom;
  this.onNotBottom      = options.onNotBottom;
}
Headroom.prototype = {
  constructor : Headroom,

  /**
   * Initialises the widget
   */
  init : function() {
    if(!Headroom.cutsTheMustard) {
      return;
    }

    this.debouncer = new Debouncer(this.update.bind(this));
    this.elem.classList.add(this.classes.initial);

    // defer event registration to handle browser 
    // potentially restoring previous scroll position
    setTimeout(this.attachEvent.bind(this), 100);

    return this;
  },

  /**
   * Unattaches events and removes any classes that were added
   */
  destroy : function() {
    var classes = this.classes;

    this.initialised = false;
    this.elem.classList.remove(classes.unpinned, classes.pinned, classes.top, classes.notTop, classes.initial);
    this.scroller.removeEventListener('scroll', this.debouncer, false);
  },

  /**
   * Attaches the scroll event
   * @private
   */
  attachEvent : function() {
    if(!this.initialised){
      this.lastKnownScrollY = this.getScrollY();
      this.initialised = true;
      this.scroller.addEventListener('scroll', this.debouncer, false);

      this.debouncer.handleEvent();
    }
  },
  
  /**
   * Unpins the header if it's currently pinned
   */
  unpin : function() {
    var classList = this.elem.classList,
      classes = this.classes;
    
    if(classList.contains(classes.pinned) || !classList.contains(classes.unpinned)) {
      classList.add(classes.unpinned);
      classList.remove(classes.pinned);
      this.onUnpin && this.onUnpin.call(this);
    }
  },

  /**
   * Pins the header if it's currently unpinned
   */
  pin : function() {
    var classList = this.elem.classList,
      classes = this.classes;
    
    if(classList.contains(classes.unpinned)) {
      classList.remove(classes.unpinned);
      classList.add(classes.pinned);
      this.onPin && this.onPin.call(this);
    }
  },

  /**
   * Handles the top states
   */
  top : function() {
    var classList = this.elem.classList,
      classes = this.classes;
    
    if(!classList.contains(classes.top)) {
      classList.add(classes.top);
      classList.remove(classes.notTop);
      this.onTop && this.onTop.call(this);
    }
  },

  /**
   * Handles the not top state
   */
  notTop : function() {
    var classList = this.elem.classList,
      classes = this.classes;
    
    if(!classList.contains(classes.notTop)) {
      classList.add(classes.notTop);
      classList.remove(classes.top);
      this.onNotTop && this.onNotTop.call(this);
    }
  },

  bottom : function() {
    var classList = this.elem.classList,
      classes = this.classes;
    
    if(!classList.contains(classes.bottom)) {
      classList.add(classes.bottom);
      classList.remove(classes.notBottom);
      this.onBottom && this.onBottom.call(this);
    }
  },

  /**
   * Handles the not top state
   */
  notBottom : function() {
    var classList = this.elem.classList,
      classes = this.classes;
    
    if(!classList.contains(classes.notBottom)) {
      classList.add(classes.notBottom);
      classList.remove(classes.bottom);
      this.onNotBottom && this.onNotBottom.call(this);
    }
  },

  /**
   * Gets the Y scroll position
   * @see https://developer.mozilla.org/en-US/docs/Web/API/Window.scrollY
   * @return {Number} pixels the page has scrolled along the Y-axis
   */
  getScrollY : function() {
    return (this.scroller.pageYOffset !== undefined)
      ? this.scroller.pageYOffset
      : (this.scroller.scrollTop !== undefined)
        ? this.scroller.scrollTop
        : (document.documentElement || document.body.parentNode || document.body).scrollTop;
  },

  /**
   * Gets the height of the viewport
   * @see http://andylangton.co.uk/blog/development/get-viewport-size-width-and-height-javascript
   * @return {int} the height of the viewport in pixels
   */
  getViewportHeight : function () {
    return window.innerHeight
      || document.documentElement.clientHeight
      || document.body.clientHeight;
  },

  /**
   * Gets the physical height of the DOM element
   * @param  {Object}  elm the element to calculate the physical height of which
   * @return {int}     the physical height of the element in pixels
   */
  getElementPhysicalHeight : function (elm) {
    return Math.max(
      elm.offsetHeight,
      elm.clientHeight
    );
  },

  /**
   * Gets the physical height of the scroller element
   * @return {int} the physical height of the scroller element in pixels
   */
  getScrollerPhysicalHeight : function () {
    return (this.scroller === window || this.scroller === document.body)
      ? this.getViewportHeight()
      : this.getElementPhysicalHeight(this.scroller);
  },

  /**
   * Gets the height of the document
   * @see http://james.padolsey.com/javascript/get-document-height-cross-browser/
   * @return {int} the height of the document in pixels
   */
  getDocumentHeight : function () {
    var body = document.body,
      documentElement = document.documentElement;
  
    return Math.max(
      body.scrollHeight, documentElement.scrollHeight,
      body.offsetHeight, documentElement.offsetHeight,
      body.clientHeight, documentElement.clientHeight
    );
  },

  /**
   * Gets the height of the DOM element
   * @param  {Object}  elm the element to calculate the height of which
   * @return {int}     the height of the element in pixels
   */
  getElementHeight : function (elm) {
    return Math.max(
      elm.scrollHeight,
      elm.offsetHeight,
      elm.clientHeight
    );
  },

  /**
   * Gets the height of the scroller element
   * @return {int} the height of the scroller element in pixels
   */
  getScrollerHeight : function () {
    return (this.scroller === window || this.scroller === document.body)
      ? this.getDocumentHeight()
      : this.getElementHeight(this.scroller);
  },

  /**
   * determines if the scroll position is outside of document boundaries
   * @param  {int}  currentScrollY the current y scroll position
   * @return {bool} true if out of bounds, false otherwise
   */
  isOutOfBounds : function (currentScrollY) {
    var pastTop  = currentScrollY < 0,
      pastBottom = currentScrollY + this.getScrollerPhysicalHeight() > this.getScrollerHeight();
    
    return pastTop || pastBottom;
  },

  /**
   * determines if the tolerance has been exceeded
   * @param  {int} currentScrollY the current scroll y position
   * @return {bool} true if tolerance exceeded, false otherwise
   */
  toleranceExceeded : function (currentScrollY, direction) {
    return Math.abs(currentScrollY-this.lastKnownScrollY) >= this.tolerance[direction];
  },

  /**
   * determine if it is appropriate to unpin
   * @param  {int} currentScrollY the current y scroll position
   * @param  {bool} toleranceExceeded has the tolerance been exceeded?
   * @return {bool} true if should unpin, false otherwise
   */
  shouldUnpin : function (currentScrollY, toleranceExceeded) {
    var scrollingDown = currentScrollY > this.lastKnownScrollY,
      pastOffset = currentScrollY >= this.offset;

    return scrollingDown && pastOffset && toleranceExceeded;
  },

  /**
   * determine if it is appropriate to pin
   * @param  {int} currentScrollY the current y scroll position
   * @param  {bool} toleranceExceeded has the tolerance been exceeded?
   * @return {bool} true if should pin, false otherwise
   */
  shouldPin : function (currentScrollY, toleranceExceeded) {
    var scrollingUp  = currentScrollY < this.lastKnownScrollY,
      pastOffset = currentScrollY <= this.offset;

    return (scrollingUp && toleranceExceeded) || pastOffset;
  },

  /**
   * Handles updating the state of the widget
   */
  update : function() {
    var currentScrollY  = this.getScrollY(),
      scrollDirection = currentScrollY > this.lastKnownScrollY ? 'down' : 'up',
      toleranceExceeded = this.toleranceExceeded(currentScrollY, scrollDirection);

    if(this.isOutOfBounds(currentScrollY)) { // Ignore bouncy scrolling in OSX
      return;
    }

    if (currentScrollY <= this.offset ) {
      this.top();
    } else {
      this.notTop();
    }

    if(currentScrollY + this.getViewportHeight() >= this.getScrollerHeight()) {
      this.bottom();
    }
    else {
      this.notBottom();
    }

    if(this.shouldUnpin(currentScrollY, toleranceExceeded)) {
      this.unpin();
    }
    else if(this.shouldPin(currentScrollY, toleranceExceeded)) {
      this.pin();
    }

    this.lastKnownScrollY = currentScrollY;
  }
};
/**
 * Default options
 * @type {Object}
 */
Headroom.options = {
  tolerance : {
    up : 0,
    down : 0
  },
  offset : 0,
  scroller: window,
  classes : {
    pinned : 'headroom--pinned',
    unpinned : 'headroom--unpinned',
    top : 'headroom--top',
    notTop : 'headroom--not-top',
    bottom : 'headroom--bottom',
    notBottom : 'headroom--not-bottom',
    initial : 'headroom'
  }
};
Headroom.cutsTheMustard = typeof features !== 'undefined' && features.rAF && features.bind && features.classList;
