/*
 * HC-Sticky
 * =========
 * Version: 2.3.0
 * Author: Some Web Media
 * Author URL: https://github.com/somewebmedia
 * Plugin URL: https://github.com/somewebmedia/hc-sticky
 * Description: JavaScript library that makes any element on your page visible while you scroll
 * License: MIT
 */

( function( global, factory ) {
  if ( typeof module === 'object' && typeof module.exports === 'object' ) {
    if ( global.document ) {
      module.exports = factory( global );
    }
    else {
      throw new Error( 'HC-Sticky requires a browser to run.' );
    }
  }
  else if ( typeof define === 'function' && define.amd ) {
    define( 'hcSticky', [], factory( global ) );
  }
  else {
    factory( global );
  }
} )( typeof window !== 'undefined' ? window : this, ( window ) => {
  const document = window.document;

  const DEFAULT_OPTIONS = {
    top: 0,
    bottom: 0,
    bottomEnd: 0,
    innerTop: 0,
    innerSticker: null,
    stickyClass: 'sticky',
    stickTo: null,
    followScroll: true,
    responsive: null,
    mobileFirst: false,
    onStart: null,
    onStop: null,
    onBeforeResize: null,
    onResize: null,
    resizeDebounce: 100,
    disable: false
  };

  const deprecated = ( () => {
    const pluginName = 'HC Sticky';

    return ( what, instead, type ) => {
      console.warn(
        '%c' + pluginName + ':'
        + '%c ' + type
        + "%c '"+ what + "'"
        + '%c is now deprecated and will be removed. Use'
        + "%c '" + instead + "'"
        + '%c instead.',
        'color: #fa253b',
        'color: default',
        'color: #5595c6',
        'color: default',
        'color: #5595c6',
        'color: default'
      );
    };
  } )();

  const hcSticky = function( elem, userSettings ) {
    userSettings = userSettings || {};

    // use querySeletor if string is passed
    if ( typeof elem === 'string' ) {
      elem = document.querySelector( elem );
    }

    // check if element exist
    if ( ! elem ) return false;

    if ( userSettings.queries ) {
      deprecated( 'queries', 'responsive', 'option' );
    }

    if ( userSettings.queryFlow ) {
      deprecated( 'queryFlow', 'mobileFirst', 'option' );
    }

    let STICKY_OPTIONS = {};
    const Helpers = hcSticky.Helpers;
    const elemParent = elem.parentNode;

    // parent can't be static
    if ( Helpers.getStyle( elemParent, 'position' ) === 'static' ) {
      elemParent.style.position = 'relative';
    }

    const setOptions = ( options ) => {
      options = options || {};

        // nothing to set
      if ( Helpers.isEmptyObject( options ) && ! Helpers.isEmptyObject( STICKY_OPTIONS ) ) {
        return;
      }

      // extend options
      STICKY_OPTIONS = Object.assign( {}, DEFAULT_OPTIONS, STICKY_OPTIONS, options );
    };

    const resetOptions = ( options ) => {
      STICKY_OPTIONS = Object.assign( {}, DEFAULT_OPTIONS, options || {} );
    };

    const getOptions = ( option ) => {
      return option ? STICKY_OPTIONS[option] : Object.assign( {}, STICKY_OPTIONS );
    };

    const isDisabled = () => STICKY_OPTIONS.disable;

    const applyQueries = () => {
      const mediaQueries = STICKY_OPTIONS.responsive || STICKY_OPTIONS.queries;

      if ( mediaQueries ) {
        const window_width = window.innerWidth;

        // reset settings
        resetOptions( userSettings );

        if ( STICKY_OPTIONS.mobileFirst ) {
          for ( const width in mediaQueries ) {
            if ( window_width >= width && ! Helpers.isEmptyObject( mediaQueries[width] ) ) {
              setOptions( mediaQueries[width] );
            }
          }
        }
        else {
          const queriesArr = [];

          // convert to array so we can reverse loop it
          for ( const b in mediaQueries ) {
            const q = {};

            q[b] = mediaQueries[b];
            queriesArr.push( q );
          }

          for ( let i = queriesArr.length - 1; i >= 0; i-- ) {
            const query = queriesArr[i];
            const breakpoint = Object.keys( query )[0];

            if ( window_width <= breakpoint && ! Helpers.isEmptyObject( query[breakpoint] ) ) {
              setOptions( query[breakpoint] );
            }
          }
        }
      }
    };

    // our helper function for getting necessary styles
    const getStickyCss = ( el ) => {
      const cascadedStyle = Helpers.getCascadedStyle( el );
      const computedStyle = Helpers.getStyle( el );

      const css = {
        height: el.offsetHeight + 'px',
        left: cascadedStyle.left,
        right: cascadedStyle.right,
        top: cascadedStyle.top,
        bottom: cascadedStyle.bottom,
        position: computedStyle.position,
        display: computedStyle.display,
        verticalAlign: computedStyle.verticalAlign,
        boxSizing: computedStyle.boxSizing,
        marginLeft: cascadedStyle.marginLeft,
        marginRight: cascadedStyle.marginRight,
        marginTop: cascadedStyle.marginTop,
        marginBottom: cascadedStyle.marginBottom,
        paddingLeft: cascadedStyle.paddingLeft,
        paddingRight: cascadedStyle.paddingRight
      };

      if ( cascadedStyle['float'] ) {
        css['float'] = cascadedStyle['float'] || 'none';
      }

      if ( cascadedStyle.cssFloat ) {
        css['cssFloat'] = cascadedStyle.cssFloat || 'none';
      }

      css['width'] = cascadedStyle.width !== 'auto'
        ? cascadedStyle.width
        : ( css.boxSizing === 'border-box' ? el.offsetWidth + 'px' : computedStyle.width );

      return css;
    };

    const Sticky = {
      css: {},
      position: null, // so we don't need to check css all the time
      stick: ( args ) => {
        args = args || {};

        // check if element is already sticky
        if ( elem.classList.contains( STICKY_OPTIONS.stickyClass ) ) {
          return;
        }

        if ( Spacer.isAttached === false ) {
          Spacer.attach();
        }

        Sticky.position = 'fixed';

        // apply styles
        elem.style.position = 'fixed';
        elem.style.left = Spacer.offsetLeft + 'px';
        elem.style.width = Spacer.width;

        elem.style.bottom = typeof args.bottom === 'undefined' ? 'auto' : args.bottom + 'px';
        elem.style.top = typeof args.top === 'undefined' ? 'auto' : args.top + 'px';

        elem.classList.add( STICKY_OPTIONS.stickyClass );

        // fire 'start' event
        if ( STICKY_OPTIONS.onStart ) {
          STICKY_OPTIONS.onStart.call( elem, Object.assign( {}, STICKY_OPTIONS ) );
        }
      },
      release: ( args ) => {
        args = args || {};
        args.stop = args.stop || false;

        // check if we've already done this
        if ( args.stop !== true && Sticky.position !== 'fixed' && Sticky.position !== null && (
          ( typeof args.top === 'undefined' && typeof args.bottom === 'undefined' ) ||
          ( typeof args.top !== 'undefined' && ( parseInt( Helpers.getStyle( elem, 'top' ) ) || 0 ) === args.top ) ||
          ( typeof args.bottom !== 'undefined' && ( parseInt( Helpers.getStyle( elem, 'bottom' ) ) || 0 ) === args.bottom )
        ) ) {
          return;
        }

        if ( args.stop === true ) {
          // remove spacer
          if ( Spacer.isAttached === true ) {
            Spacer.detach();
          }
        }
        else {
          // check spacer
          if ( Spacer.isAttached === false ) {
            Spacer.attach();
          }
        }

        const position = args.position || Sticky.css.position;

        // remember position
        Sticky.position = position;

        // apply styles
        elem.style.position = position;
        elem.style.left = args.stop === true ? Sticky.css.left : Spacer.positionLeft + 'px';
        elem.style.width = position !== 'absolute' ? Sticky.css.width : Spacer.width;
        elem.style.bottom = typeof args.bottom === 'undefined' ? ( args.stop === true ? '' : 'auto' ) : args.bottom + 'px';
        elem.style.top = typeof args.top === 'undefined' ? ( args.stop === true ? '' : 'auto' ) : args.top + 'px';

        elem.classList.remove( STICKY_OPTIONS.stickyClass );

        // fire 'stop' event
        if ( STICKY_OPTIONS.onStop ) {
          STICKY_OPTIONS.onStop.call( elem, Object.assign( {}, STICKY_OPTIONS ) );
        }
      }
    };

    const Spacer = {
      el: document.createElement( 'div' ),
      offsetLeft: null,
      positionLeft: null,
      width: null,
      isAttached: false,
      init: () => {
        Spacer.el.className = 'sticky-spacer';

        // copy styles from sticky element
        for ( const prop in Sticky.css ) {
          Spacer.el.style[prop] = Sticky.css[prop];
        }

        // just to be sure the spacer is behind everything
        Spacer.el.style['z-index'] = '-1';

        const elemStyle = Helpers.getStyle( elem );

        // get spacer offset and position
        Spacer.offsetLeft = Helpers.offset( elem ).left - ( parseInt( elemStyle.marginLeft ) || 0 );
        Spacer.positionLeft = Helpers.position( elem ).left;

        // get spacer width
        Spacer.width = Helpers.getStyle( elem, 'width' );
      },
      attach: () => {
        // insert spacer to DOM
        elemParent.insertBefore( Spacer.el, elem );
        Spacer.isAttached = true;
      },
      detach: () => {
        // remove spacer from DOM
        Spacer.el = elemParent.removeChild( Spacer.el );
        Spacer.isAttached = false;
      }
    };

    // define our private variables
    let stickTo_document;
    let container;
    let inner_sticker;

    let container_height;
    let container_offsetTop;

    let elemParent_offsetTop;

    let window_height;

    let options_top;
    let options_bottom;

    let stick_top;
    let stick_bottom;

    let top_limit;
    let bottom_limit;

    let largerSticky;
    let sticky_height;
    let sticky_offsetTop;

    let calcContainerHeight;
    let calcStickyHeight;

    const calcSticky = () => {
      // get/set element styles
      Sticky.css = getStickyCss( elem );

      // init or reinit spacer
      Spacer.init();

      // check if referring element is document
      stickTo_document = STICKY_OPTIONS.stickTo && (
        STICKY_OPTIONS.stickTo === 'document'
        || ( STICKY_OPTIONS.stickTo.nodeType && STICKY_OPTIONS.stickTo.nodeType === 9 )
        || ( typeof STICKY_OPTIONS.stickTo === 'object' && STICKY_OPTIONS.stickTo instanceof Document )
      ) ? true : false;

      // select referred container
      container = STICKY_OPTIONS.stickTo
        ? stickTo_document
          ? document
          : Helpers.getElement( STICKY_OPTIONS.stickTo )
        : elemParent;

      // get sticky height
      calcStickyHeight = () => {
        const height = elem.offsetHeight + ( parseInt( Sticky.css.marginTop ) || 0 ) + ( parseInt( Sticky.css.marginBottom ) || 0 );
        const h_diff = ( sticky_height || 0 ) - height;

        return ( h_diff >= -1 && h_diff <= 1 ) ? sticky_height : height;
      };

      sticky_height = calcStickyHeight();

      calcContainerHeight = () => {
        return ! stickTo_document
          ? container.offsetHeight
          : Math.max( document.documentElement.clientHeight, document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight );
      };

      container_height = calcContainerHeight();
      container_offsetTop = ! stickTo_document ? Helpers.offset( container ).top : 0;
      elemParent_offsetTop = ! STICKY_OPTIONS.stickTo
        ? container_offsetTop // parent is container
        : ! stickTo_document
          ? Helpers.offset( elemParent ).top
          : 0;
      window_height = window.innerHeight;
      sticky_offsetTop = elem.offsetTop - ( parseInt( Sticky.css.marginTop ) || 0 );

      // get inner sticker element
      inner_sticker = Helpers.getElement( STICKY_OPTIONS.innerSticker );

      // top
      options_top = isNaN( STICKY_OPTIONS.top ) && STICKY_OPTIONS.top.indexOf( '%' ) > -1
        ? ( parseFloat( STICKY_OPTIONS.top ) / 100 ) * window_height
        : STICKY_OPTIONS.top;

      // bottom
      options_bottom = isNaN( STICKY_OPTIONS.bottom ) && STICKY_OPTIONS.bottom.indexOf( '%' ) > -1
        ? ( parseFloat( STICKY_OPTIONS.bottom ) / 100 ) * window_height
        : STICKY_OPTIONS.bottom;

      // calculate sticky breakpoints
      stick_top = inner_sticker
        ? inner_sticker.offsetTop
        : STICKY_OPTIONS.innerTop
          ? STICKY_OPTIONS.innerTop
          : 0;

      stick_bottom = isNaN( STICKY_OPTIONS.bottomEnd ) && STICKY_OPTIONS.bottomEnd.indexOf( '%' ) > -1
        ? ( parseFloat( STICKY_OPTIONS.bottomEnd ) / 100 ) * window_height
        : STICKY_OPTIONS.bottomEnd;

      top_limit = container_offsetTop - options_top + stick_top + sticky_offsetTop;
    };

    // store scroll position so we can determine scroll direction
    let last_pos = window.scrollY;
    let diff_y = 0;
    let scroll_dir;

    const runSticky = () => {
      // always calculate sticky and container height in case of DOM change
      sticky_height = calcStickyHeight();
      container_height = calcContainerHeight();

      bottom_limit = container_offsetTop + container_height - options_top - stick_bottom;

      // check if sticky is bigger than container
      largerSticky = sticky_height > window_height;

      const offset_top = window.scrollY;
      const sticky_top = Helpers.offset( elem ).top;
      const sticky_window_top = sticky_top - offset_top;
      let bottom_distance;

      // get scroll direction
      scroll_dir = offset_top < last_pos ? 'up' : 'down';
      diff_y = offset_top - last_pos;
      last_pos = offset_top;

      if ( offset_top > top_limit ) {
        if ( bottom_limit + options_top + ( largerSticky ? options_bottom : 0 ) - ( STICKY_OPTIONS.followScroll && largerSticky ? 0 : options_top ) <= offset_top + sticky_height - stick_top - ( ( sticky_height - stick_top > window_height - ( top_limit - stick_top ) && STICKY_OPTIONS.followScroll ) ? ( ( ( bottom_distance = sticky_height - window_height - stick_top ) > 0 ) ? bottom_distance : 0 ) : 0 ) ) {
          // bottom reached end
          Sticky.release( {
            position: 'absolute',
            bottom: elemParent_offsetTop + elemParent.offsetHeight - bottom_limit - options_top
          } );
        }
        else if ( largerSticky && STICKY_OPTIONS.followScroll ) {
           // sticky is bigger than container and follows scroll
          if ( scroll_dir === 'down' ) {
            if ( sticky_window_top + sticky_height + options_bottom <= window_height + .9 ) { // fix subpixel precision with adding .9 pixels
              Sticky.stick( {
                bottom: options_bottom
              } );
            }
            else if ( Sticky.position === 'fixed' ) { // bottom reached window bottom
              Sticky.release( {
                position: 'absolute',
                top: sticky_top - options_top - top_limit - diff_y + stick_top
              } );
            }
          }
          else { // scroll up
            if ( Math.ceil( sticky_window_top + stick_top ) < 0 && Sticky.position === 'fixed' ) { // top reached window top
              Sticky.release( {
                position: 'absolute',
                top: sticky_top - options_top - top_limit + stick_top - diff_y
              } );
            }
            else if ( sticky_top >= offset_top + options_top - stick_top ) { // stick on top
              Sticky.stick( {
                top: options_top - stick_top
              } );
            }
          }
        }
        else { // stick on top
          Sticky.stick( {
            top: options_top - stick_top
          } );
        }
      }
      else { // starting point
        Sticky.release( {
          stop: true
        } );
      }
    };

    let scrollAttached = false;
    let resizeAttached = false;

    const disableSticky = () => {
      if ( scrollAttached ) {
        // detach sticky from scroll
        window.removeEventListener( 'scroll', runSticky, Helpers.supportsPassive );

        // sticky is not attached to scroll anymore
        scrollAttached = false;
      }
    };

    const initSticky = () => {
      // check if element or it's parents are visible
      if ( elem.offsetParent === null || Helpers.getStyle( elem, 'display' ) === 'none' ) {
        disableSticky();
        return;
      }

      // calculate stuff
      calcSticky();

      // check if sticky is bigger than reffering container
      if ( sticky_height > container_height ) {
        disableSticky();
        return;
      }

      // run
      runSticky();

      if ( ! scrollAttached ) {
        // attach sticky to scroll
        window.addEventListener( 'scroll', runSticky, Helpers.supportsPassive );

        // sticky is attached to scroll
        scrollAttached = true;
      }
    };

    const resetSticky = () => {
      // remove inline styles
      elem.style.position = '';
      elem.style.left = '';
      elem.style.top = '';
      elem.style.bottom = '';
      elem.style.width = '';

      elem.classList.remove( STICKY_OPTIONS.stickyClass );

      // reset sticky object data
      Sticky.css = {};
      Sticky.position = null;

      // remove spacer
      if ( Spacer.isAttached === true ) {
        Spacer.detach();
      }
    };

    const reinitSticky = () => {
      resetSticky();
      applyQueries();

      if ( isDisabled() ) {
        disableSticky();
        return;
      }

      // restart sticky
      initSticky();
    };

    const resizeSticky = () => {
      // fire 'beforeResize' event
      if ( STICKY_OPTIONS.onBeforeResize ) {
        STICKY_OPTIONS.onBeforeResize.call( elem, Object.assign( {}, STICKY_OPTIONS ) );
      }

      // reinit sticky
      reinitSticky();

      // fire 'resize' event
      if ( STICKY_OPTIONS.onResize ) {
        STICKY_OPTIONS.onResize.call( elem, Object.assign( {}, STICKY_OPTIONS ) );
      }
    };

    const resize_cb = ! STICKY_OPTIONS.resizeDebounce
      ? resizeSticky
      : Helpers.debounce( resizeSticky, STICKY_OPTIONS.resizeDebounce );

    // Method for updating options
    const Update = ( options ) => {
      setOptions( options );

      // also update user settings
      userSettings = Object.assign( {}, userSettings, options || {} );

      reinitSticky();
    };

    const Detach = () => {
      // detach resize reinit
      if ( resizeAttached ) {
        window.removeEventListener( 'resize', resize_cb, Helpers.supportsPassive );
        resizeAttached = false;
      }

      disableSticky();
    };

    const Destroy = () => {
      Detach();
      resetSticky();
    };

    const Attach = () => {
      // attach resize reinit
      if ( ! resizeAttached ) {
        window.addEventListener( 'resize', resize_cb, Helpers.supportsPassive );
        resizeAttached = true;
      }

      applyQueries();

      if ( isDisabled() ) {
        disableSticky();
        return;
      }

      initSticky();
    };

    this.options = getOptions;
    this.refresh = reinitSticky;
    this.update = Update;
    this.attach = Attach;
    this.detach = Detach;
    this.destroy = Destroy;

    // jQuery methods
    this.triggerMethod = ( method, options ) => {
      if ( typeof this[method] === 'function' ) {
        this[method]( options );
      }
    };

    this.reinit = () => {
      deprecated( 'reinit', 'refresh', 'method' );
      reinitSticky();
    };

    // init settings
    setOptions( userSettings );

    // start sticky
    Attach();

    // reinit on complete page load
    window.addEventListener( 'load', reinitSticky );
  };

  // jQuery Plugin
  if ( typeof window.jQuery !== 'undefined' ) {
    const $ = window.jQuery;
    const namespace = 'hcSticky';

    $.fn.extend( {
      hcSticky: function( args, update ) {
        // check if selected element exist
        if ( ! this.length ) return this;

        // we need to return options
        if ( args === 'options' ) {
          return $.data( this.get( 0 ), namespace ).options();
        }

        return this.each( function() {
          let instance = $.data( this, namespace );

          if ( instance ) {
            // already created, trigger method
            instance.triggerMethod( args, update );
          }
          else {
            // create new instance
            instance = new hcSticky( this, args );
            $.data( this, namespace, instance );
          }
        } );
      }
    } );
  }

  // browser global
  window.hcSticky = window.hcSticky || hcSticky;

  return hcSticky;
} );