‘use strict’;

!function($) {

/**

* Sticky module.
* @module foundation.sticky
* @requires foundation.util.triggers
* @requires foundation.util.mediaQuery
*/

class Sticky {

/**
 * Creates a new instance of a sticky thing.
 * @class
 * @param {jQuery} element - jQuery object to make sticky.
 * @param {Object} options - options object passed when creating the element programmatically.
 */
constructor(element, options) {
  this.$element = element;
  this.options = $.extend({}, Sticky.defaults, this.$element.data(), options);

  this._init();

  Foundation.registerPlugin(this, 'Sticky');
}

/**
 * Initializes the sticky element by adding classes, getting/setting dimensions, breakpoints and attributes
 * @function
 * @private
 */
_init() {
  var $parent = this.$element.parent('[data-sticky-container]'),
      id = this.$element[0].id || Foundation.GetYoDigits(6, 'sticky'),
      _this = this;

  if (!$parent.length) {
    this.wasWrapped = true;
  }
  this.$container = $parent.length ? $parent : $(this.options.container).wrapInner(this.$element);
  this.$container.addClass(this.options.containerClass);

  this.$element.addClass(this.options.stickyClass).attr({ 'data-resize': id, 'data-mutate': id });
  if (this.options.anchor !== '') {
      $('#' + _this.options.anchor).attr({ 'data-mutate': id });
  }

  this.scrollCount = this.options.checkEvery;
  this.isStuck = false;
  $(window).one('load.zf.sticky', function(){
    //We calculate the container height to have correct values for anchor points offset calculation.
    _this.containerHeight = _this.$element.css("display") == "none" ? 0 : _this.$element[0].getBoundingClientRect().height;
    _this.$container.css('height', _this.containerHeight);
    _this.elemHeight = _this.containerHeight;
    if(_this.options.anchor !== ''){
      _this.$anchor = $('#' + _this.options.anchor);
    }else{
      _this._parsePoints();
    }

    _this._setSizes(function(){
      var scroll = window.pageYOffset;
      _this._calc(false, scroll);
      //Unstick the element will ensure that proper classes are set.
      if (!_this.isStuck) {
        _this._removeSticky((scroll >= _this.topPoint) ? false : true);
      }
    });
    _this._events(id.split('-').reverse().join('-'));
  });
}

/**
 * If using multiple elements as anchors, calculates the top and bottom pixel values the sticky thing should stick and unstick on.
 * @function
 * @private
 */
_parsePoints() {
  var top = this.options.topAnchor == "" ? 1 : this.options.topAnchor,
      btm = this.options.btmAnchor== "" ? document.documentElement.scrollHeight : this.options.btmAnchor,
      pts = [top, btm],
      breaks = {};
  for (var i = 0, len = pts.length; i < len && pts[i]; i++) {
    var pt;
    if (typeof pts[i] === 'number') {
      pt = pts[i];
    } else {
      var place = pts[i].split(':'),
          anchor = $(`#${place[0]}`);

      pt = anchor.offset().top;
      if (place[1] && place[1].toLowerCase() === 'bottom') {
        pt += anchor[0].getBoundingClientRect().height;
      }
    }
    breaks[i] = pt;
  }

  this.points = breaks;
  return;
}

/**
 * Adds event handlers for the scrolling element.
 * @private
 * @param {String} id - psuedo-random id for unique scroll event listener.
 */
_events(id) {
  var _this = this,
      scrollListener = this.scrollListener = `scroll.zf.${id}`;
  if (this.isOn) { return; }
  if (this.canStick) {
    this.isOn = true;
    $(window).off(scrollListener)
             .on(scrollListener, function(e) {
               if (_this.scrollCount === 0) {
                 _this.scrollCount = _this.options.checkEvery;
                 _this._setSizes(function() {
                   _this._calc(false, window.pageYOffset);
                 });
               } else {
                 _this.scrollCount--;
                 _this._calc(false, window.pageYOffset);
               }
            });
  }

  this.$element.off('resizeme.zf.trigger')
               .on('resizeme.zf.trigger', function(e, el) {
                  _this._eventsHandler(id);
  });

  this.$element.on('mutateme.zf.trigger', function (e, el) {
      _this._eventsHandler(id);
  });

  if(this.$anchor) {
    this.$anchor.on('mutateme.zf.trigger', function (e, el) {
        _this._eventsHandler(id);
    });
  }
}

/**
 * Handler for events.
 * @private
 * @param {String} id - psuedo-random id for unique scroll event listener.
 */
_eventsHandler(id) {
     var _this = this,
      scrollListener = this.scrollListener = `scroll.zf.${id}`;

     _this._setSizes(function() {
     _this._calc(false);
     if (_this.canStick) {
       if (!_this.isOn) {
         _this._events(id);
       }
     } else if (_this.isOn) {
       _this._pauseListeners(scrollListener);
     }
   });
}

/**
 * Removes event handlers for scroll and change events on anchor.
 * @fires Sticky#pause
 * @param {String} scrollListener - unique, namespaced scroll listener attached to `window`
 */
_pauseListeners(scrollListener) {
  this.isOn = false;
  $(window).off(scrollListener);

  /**
   * Fires when the plugin is paused due to resize event shrinking the view.
   * @event Sticky#pause
   * @private
   */
   this.$element.trigger('pause.zf.sticky');
}

/**
 * Called on every `scroll` event and on `_init`
 * fires functions based on booleans and cached values
 * @param {Boolean} checkSizes - true if plugin should recalculate sizes and breakpoints.
 * @param {Number} scroll - current scroll position passed from scroll event cb function. If not passed, defaults to `window.pageYOffset`.
 */
_calc(checkSizes, scroll) {
  if (checkSizes) { this._setSizes(); }

  if (!this.canStick) {
    if (this.isStuck) {
      this._removeSticky(true);
    }
    return false;
  }

  if (!scroll) { scroll = window.pageYOffset; }

  if (scroll >= this.topPoint) {
    if (scroll <= this.bottomPoint) {
      if (!this.isStuck) {
        this._setSticky();
      }
    } else {
      if (this.isStuck) {
        this._removeSticky(false);
      }
    }
  } else {
    if (this.isStuck) {
      this._removeSticky(true);
    }
  }
}

/**
 * Causes the $element to become stuck.
 * Adds `position: fixed;`, and helper classes.
 * @fires Sticky#stuckto
 * @function
 * @private
 */
_setSticky() {
  var _this = this,
      stickTo = this.options.stickTo,
      mrgn = stickTo === 'top' ? 'marginTop' : 'marginBottom',
      notStuckTo = stickTo === 'top' ? 'bottom' : 'top',
      css = {};

  css[mrgn] = `${this.options[mrgn]}em`;
  css[stickTo] = 0;
  css[notStuckTo] = 'auto';
  this.isStuck = true;
  this.$element.removeClass(`is-anchored is-at-${notStuckTo}`)
               .addClass(`is-stuck is-at-${stickTo}`)
               .css(css)
               /**
                * Fires when the $element has become `position: fixed;`
                * Namespaced to `top` or `bottom`, e.g. `sticky.zf.stuckto:top`
                * @event Sticky#stuckto
                */
               .trigger(`sticky.zf.stuckto:${stickTo}`);
  this.$element.on("transitionend webkitTransitionEnd oTransitionEnd otransitionend MSTransitionEnd", function() {
    _this._setSizes();
  });
}

/**
 * Causes the $element to become unstuck.
 * Removes `position: fixed;`, and helper classes.
 * Adds other helper classes.
 * @param {Boolean} isTop - tells the function if the $element should anchor to the top or bottom of its $anchor element.
 * @fires Sticky#unstuckfrom
 * @private
 */
_removeSticky(isTop) {
  var stickTo = this.options.stickTo,
      stickToTop = stickTo === 'top',
      css = {},
      anchorPt = (this.points ? this.points[1] - this.points[0] : this.anchorHeight) - this.elemHeight,
      mrgn = stickToTop ? 'marginTop' : 'marginBottom',
      notStuckTo = stickToTop ? 'bottom' : 'top',
      topOrBottom = isTop ? 'top' : 'bottom';

  css[mrgn] = 0;

  css['bottom'] = 'auto';
  if(isTop) {
    css['top'] = 0;
  } else {
    css['top'] = anchorPt;
  }

  this.isStuck = false;
  this.$element.removeClass(`is-stuck is-at-${stickTo}`)
               .addClass(`is-anchored is-at-${topOrBottom}`)
               .css(css)
               /**
                * Fires when the $element has become anchored.
                * Namespaced to `top` or `bottom`, e.g. `sticky.zf.unstuckfrom:bottom`
                * @event Sticky#unstuckfrom
                */
               .trigger(`sticky.zf.unstuckfrom:${topOrBottom}`);
}

/**
 * Sets the $element and $container sizes for plugin.
 * Calls `_setBreakPoints`.
 * @param {Function} cb - optional callback function to fire on completion of `_setBreakPoints`.
 * @private
 */
_setSizes(cb) {
  this.canStick = Foundation.MediaQuery.is(this.options.stickyOn);
  if (!this.canStick) {
    if (cb && typeof cb === 'function') { cb(); }
  }
  var _this = this,
      newElemWidth = this.$container[0].getBoundingClientRect().width,
      comp = window.getComputedStyle(this.$container[0]),
      pdngl = parseInt(comp['padding-left'], 10),
      pdngr = parseInt(comp['padding-right'], 10);

  if (this.$anchor && this.$anchor.length) {
    this.anchorHeight = this.$anchor[0].getBoundingClientRect().height;
  } else {
    this._parsePoints();
  }

  this.$element.css({
    'max-width': `${newElemWidth - pdngl - pdngr}px`
  });

  var newContainerHeight = this.$element[0].getBoundingClientRect().height || this.containerHeight;
  if (this.$element.css("display") == "none") {
    newContainerHeight = 0;
  }
  this.containerHeight = newContainerHeight;
  this.$container.css({
    height: newContainerHeight
  });
  this.elemHeight = newContainerHeight;

  if (!this.isStuck) {
    if (this.$element.hasClass('is-at-bottom')) {
      var anchorPt = (this.points ? this.points[1] - this.$container.offset().top : this.anchorHeight) - this.elemHeight;
      this.$element.css('top', anchorPt);
    }
  }

  this._setBreakPoints(newContainerHeight, function() {
    if (cb && typeof cb === 'function') { cb(); }
  });
}

/**
 * Sets the upper and lower breakpoints for the element to become sticky/unsticky.
 * @param {Number} elemHeight - px value for sticky.$element height, calculated by `_setSizes`.
 * @param {Function} cb - optional callback function to be called on completion.
 * @private
 */
_setBreakPoints(elemHeight, cb) {
  if (!this.canStick) {
    if (cb && typeof cb === 'function') { cb(); }
    else { return false; }
  }
  var mTop = emCalc(this.options.marginTop),
      mBtm = emCalc(this.options.marginBottom),
      topPoint = this.points ? this.points[0] : this.$anchor.offset().top,
      bottomPoint = this.points ? this.points[1] : topPoint + this.anchorHeight,
      // topPoint = this.$anchor.offset().top || this.points[0],
      // bottomPoint = topPoint + this.anchorHeight || this.points[1],
      winHeight = window.innerHeight;

  if (this.options.stickTo === 'top') {
    topPoint -= mTop;
    bottomPoint -= (elemHeight + mTop);
  } else if (this.options.stickTo === 'bottom') {
    topPoint -= (winHeight - (elemHeight + mBtm));
    bottomPoint -= (winHeight - mBtm);
  } else {
    //this would be the stickTo: both option... tricky
  }

  this.topPoint = topPoint;
  this.bottomPoint = bottomPoint;

  if (cb && typeof cb === 'function') { cb(); }
}

/**
 * Destroys the current sticky element.
 * Resets the element to the top position first.
 * Removes event listeners, JS-added css properties and classes, and unwraps the $element if the JS added the $container.
 * @function
 */
destroy() {
  this._removeSticky(true);

  this.$element.removeClass(`${this.options.stickyClass} is-anchored is-at-top`)
               .css({
                 height: '',
                 top: '',
                 bottom: '',
                 'max-width': ''
               })
               .off('resizeme.zf.trigger')
               .off('mutateme.zf.trigger');
  if (this.$anchor && this.$anchor.length) {
    this.$anchor.off('change.zf.sticky');
  }
  $(window).off(this.scrollListener);

  if (this.wasWrapped) {
    this.$element.unwrap();
  } else {
    this.$container.removeClass(this.options.containerClass)
                   .css({
                     height: ''
                   });
  }
  Foundation.unregisterPlugin(this);
}

}

Sticky.defaults = {

/**
 * Customizable container template. Add your own classes for styling and sizing.
 * @option
 * @type {string}
 * @default '&lt;div data-sticky-container&gt;&lt;/div&gt;'
 */
container: '<div data-sticky-container></div>',
/**
 * Location in the view the element sticks to. Can be `'top'` or `'bottom'`.
 * @option
 * @type {string}
 * @default 'top'
 */
stickTo: 'top',
/**
 * If anchored to a single element, the id of that element.
 * @option
 * @type {string}
 * @default ''
 */
anchor: '',
/**
 * If using more than one element as anchor points, the id of the top anchor.
 * @option
 * @type {string}
 * @default ''
 */
topAnchor: '',
/**
 * If using more than one element as anchor points, the id of the bottom anchor.
 * @option
 * @type {string}
 * @default ''
 */
btmAnchor: '',
/**
 * Margin, in `em`'s to apply to the top of the element when it becomes sticky.
 * @option
 * @type {number}
 * @default 1
 */
marginTop: 1,
/**
 * Margin, in `em`'s to apply to the bottom of the element when it becomes sticky.
 * @option
 * @type {number}
 * @default 1
 */
marginBottom: 1,
/**
 * Breakpoint string that is the minimum screen size an element should become sticky.
 * @option
 * @type {string}
 * @default 'medium'
 */
stickyOn: 'medium',
/**
 * Class applied to sticky element, and removed on destruction. Foundation defaults to `sticky`.
 * @option
 * @type {string}
 * @default 'sticky'
 */
stickyClass: 'sticky',
/**
 * Class applied to sticky container. Foundation defaults to `sticky-container`.
 * @option
 * @type {string}
 * @default 'sticky-container'
 */
containerClass: 'sticky-container',
/**
 * Number of scroll events between the plugin's recalculating sticky points. Setting it to `0` will cause it to recalc every scroll event, setting it to `-1` will prevent recalc on scroll.
 * @option
 * @type {number}
 * @default -1
 */
checkEvery: -1

};

/**

* Helper function to calculate em values
* @param Number {em} - number of em's to calculate into pixels
*/

function emCalc(em) {

return parseInt(window.getComputedStyle(document.body, null).fontSize, 10) * em;

}

// Window exports Foundation.plugin(Sticky, ‘Sticky’);

}(jQuery);