text: /**
   * @namespace PositionSticky
   * @author Ahmet Katrancı <ahmet@katranci.co.uk>
   */
  var Container   = require('../src/Container');
  var Placeholder = require('../src/Placeholder');
  var Sticky      = require('../src/Sticky');
  var rAF         = require('raf');
  
  var PositionSticky = {
  
    /**
     * @constant
     */
    POS_SCHEME_STATIC:   100,
  
    /**
     * @constant
     */
    POS_SCHEME_FIXED:    200,
  
    /**
     * @constant
     */
    POS_SCHEME_ABSOLUTE: 300,
  
    /**
     * Creates an instance of PositionSticky
     *
     * @param element
     * @param options
     * @returns {PositionSticky}
     * @static
     * @public
     */
    create: function(element, options) {
      if (typeof options === 'undefined') {
        options = {};
      }
      return Object.create(PositionSticky)._init(element, options);
    },
  
    /**
     * Constructor method
     *
     * @param element {HTMLElement}
     * @param options {Object}
     * @returns {PositionSticky}
     * @instance
     * @private
     */
    _init: function(element, options) {
      this.constructor = PositionSticky;
      this._window = window;
      this._sticky = Sticky.create(element);
      this._container = Container.create(element.parentNode);
      this._placeholder = null;
      this._posScheme = PositionSticky.POS_SCHEME_STATIC;
      this._isTicking = false;
      this._threshold = null;
      this._options = options;
      this._leftPositionWhenAbsolute = null;
      this._leftPositionWhenFixed = null;
      this._latestKnownScrollY = this._window.pageYOffset;
  
      this._setOffsetTop();
      this._setOffsetBottom();
      this._calcThreshold();
      this._setLeftPositionWhenAbsolute();
      this._setLeftPositionWhenFixed();
      this._createPlaceholder();
      this._subscribeToWindowScroll();
  
      return this;
    },
  
    /**
     * Sets the distance that the sticky element will have from the top of viewport
     * when it becomes sticky
     *
     * @instance
     * @private
     */
    _setOffsetTop: function() {
      if (typeof this._options.offsetTop === 'number' && this._options.offsetTop >= 0) {
        this.offsetTop = this._options.offsetTop;
      } else {
        this.offsetTop = this._container.borderTopWidth + this._container.paddingTop;
      }
    },
  
    /**
     * Sets the amount to subtract in #_canStickyFitInContainer and also sets the
     * distance that the sticky element will have from the bottom of its container
     * when it is positioned absolutely
     *
     * @instance
     * @private
     */
    _setOffsetBottom: function() {
      this.offsetBottom = this._container.borderBottomWidth + this._container.paddingBottom;
    },
  
    /**
     * Calculates the point where the sticky behaviour should start
     *
     * @instance
     * @private
     */
    _calcThreshold: function() {
      this._threshold = this._getStickyDistanceFromDocumentTop() - this.offsetTop;
    },
  
    /**
     * Gets the element's distance from its offset parent's left
     * and subtracts any horizontal margins and saves it
     *
     * @instance
     * @private
     */
    _setLeftPositionWhenAbsolute: function() {
      var marginLeft = parseInt(this._window.getComputedStyle(this._sticky.element).marginLeft, 10);
      this._leftPositionWhenAbsolute = this._sticky.element.offsetLeft - marginLeft;
    },
  
    /**
     * Gets the element's distance from document left and saves it
     *
     * @instance
     * @private
     *
     * @todo Write a test that is covering when the page is scrolled
     */
    _setLeftPositionWhenFixed: function() {
      var marginLeft = parseInt(this._window.getComputedStyle(this._sticky.element).marginLeft, 10);
      this._leftPositionWhenFixed = this._window.pageXOffset + this._sticky.element.getBoundingClientRect().left - marginLeft;
    },
  
    /**
     * Creates the placeholder that will be used in place of the element
     * when the element is positioned absolutely or fixed
     *
     * @instance
     * @private
     */
    _createPlaceholder: function() {
      this._placeholder = Placeholder.create(this._sticky);
    },
  
    /**
     * Attaches #_onScroll method to Window.onscroll event
     *
     * @instance
     * @private
     */
    _subscribeToWindowScroll: function() {
      this._window.addEventListener('scroll', this._onScroll.bind(this));
    },
  
    /**
     * Debounces the scroll event
     *
     * @see [Debouncing Scroll Events]{@link http://www.html5rocks.com/en/tutorials/speed/animations/#debouncing-scroll-events}
     * @instance
     * @private
     *
     * @todo Don't run _update when container is not visible
     */
    _onScroll: function() {
      if (!this._isTicking) {
        this._latestKnownScrollY = this._window.pageYOffset;
        rAF(this._update.bind(this));
        this._isTicking = true;
      }
    },
  
    /**
     * @returns {boolean}
     * @instance
     * @private
     */
    _isStatic: function() {
      return this._posScheme === PositionSticky.POS_SCHEME_STATIC;
    },
  
    /**
     * @instance
     * @private
     */
    _makeStatic: function() {
      this._sticky.element.style.position = 'static';
      this._placeholder.element.style.display = 'none';
      this._posScheme = PositionSticky.POS_SCHEME_STATIC;
    },
  
    /**
     * @returns {boolean}
     * @instance
     * @private
     */
    _isFixed: function() {
      return this._posScheme === PositionSticky.POS_SCHEME_FIXED;
    },
  
    /**
     * @instance
     * @private
     */
    _makeFixed: function() {
      this._sticky.element.style.bottom = null;
      this._sticky.element.style.position = 'fixed';
      this._sticky.element.style.top = this.offsetTop + 'px';
      this._sticky.element.style.left = this._leftPositionWhenFixed + 'px';
      this._placeholder.element.style.display = 'block';
      this._posScheme = PositionSticky.POS_SCHEME_FIXED;
    },
  
    /**
     * @returns {boolean}
     * @instance
     * @private
     */
    _isAbsolute: function() {
      return this._posScheme === PositionSticky.POS_SCHEME_ABSOLUTE;
    },
  
    /**
     * @instance
     * @private
     */
    _makeAbsolute: function() {
      this._sticky.element.style.top = null;
      this._sticky.element.style.position = 'absolute';
      this._sticky.element.style.bottom = this._container.paddingBottom + 'px';
      this._sticky.element.style.left = this._leftPositionWhenAbsolute + 'px';
      this._placeholder.element.style.display = 'block';
      this._posScheme = PositionSticky.POS_SCHEME_ABSOLUTE;
    },
  
    /**
     * This is the main method that runs on every animation frame during scroll.
     * It starts with checking whether the element is within the static range.
     * If not, it checks whether the element is within the fixed range.
     * Otherwise, it positions the element absolutely.
     *
     * @instance
     * @private
     */
    _update: function() {
      this._isTicking = false;
  
      if (this._isBelowThreshold()) {
        if (!this._isStatic()) {
          this._makeStatic();
        }
      } else if (this._canStickyFitInContainer()) {
        if (!this._isFixed()) {
          this._makeFixed();
        }
      } else {
        if (!this._isAbsolute()) {
          this._makeAbsolute();
        }
      }
    },
  
    /**
     * Returns true when the page hasn't been scrolled to the threshold point yet.
     * Otherwise, returns false.
     *
     * @returns {boolean}
     * @instance
     * @private
     */
    _isBelowThreshold: function() {
      if (this._latestKnownScrollY < this._threshold) {
        return true;
      }
      return false;
    },
  
    /**
     * Checks whether the element can fit inside the visible portion of the container or not
     *
     * @returns {boolean}
     * @instance
     * @private
     */
    _canStickyFitInContainer: function() {
      return this._getAvailableSpaceInContainer() >= this._sticky.boundingBoxHeight;
    },
  
    /**
     * Calculates the height of the visible portion of the container
     * that can be used to fit the sticky element
     *
     * @returns {number}
     * @instance
     * @private
     */
    _getAvailableSpaceInContainer: function() {
      return this._container.element.getBoundingClientRect().bottom - this.offsetBottom - this.offsetTop;
    },
  
    /**
     * Calculates sticky element's total offset from the document top.
     * It uses placeholder if it is called when the sticky element is
     * not static (e.g. through #refresh)
     *
     * @returns {number}
     * @instance
     * @private
     */
    _getStickyDistanceFromDocumentTop: function() {
      var element = (this._isStatic() ? this._sticky.element : this._placeholder.element);
      var totalOffsetTop = this._latestKnownScrollY + element.getBoundingClientRect().top;
      return totalOffsetTop;
    },
  
    /**
     * Re-measures the cached positions/dimensions that are used during scroll
     *
     * @instance
     * @public
     */
    refresh: function() {
      this._calcThreshold();
      this._sticky.refresh();
      this._placeholder.refresh();
    }
  
  };
  
  module.exports = PositionSticky;