function isIn(needle, haystack) {
  return haystack.indexOf(needle) >= 0;
}

function extend(custom, defaults) {
  for (const key in defaults) {
    if (custom[key] == null) {
      const value = defaults[key];
      custom[key] = value;
    }
  }
  return custom;
}

function isMobile(agent) {
  return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(agent);
}

function createEvent(event, bubble = false, cancel = false, detail = null) {
  let customEvent;
  if (document.createEvent != null) { // W3C DOM
    customEvent = document.createEvent('CustomEvent');
    customEvent.initCustomEvent(event, bubble, cancel, detail);
  } else if (document.createEventObject != null) { // IE DOM < 9
    customEvent = document.createEventObject();
    customEvent.eventType = event;
  } else {
    customEvent.eventName = event;
  }

  return customEvent;
}

function emitEvent(elem, event) {
  if (elem.dispatchEvent != null) { // W3C DOM
    elem.dispatchEvent(event);
  } else if (event in (elem != null)) {
    elem[event]();
  } else if (`on${event}` in (elem != null)) {
    elem[`on${event}`]();
  }
}

function addEvent(elem, event, fn) {
  if (elem.addEventListener != null) { // W3C DOM
    elem.addEventListener(event, fn, false);
  } else if (elem.attachEvent != null) { // IE DOM
    elem.attachEvent(`on${event}`, fn);
  } else { // fallback
    elem[event] = fn;
  }
}

function removeEvent(elem, event, fn) {
  if (elem.removeEventListener != null) { // W3C DOM
    elem.removeEventListener(event, fn, false);
  } else if (elem.detachEvent != null) { // IE DOM
    elem.detachEvent(`on${event}`, fn);
  } else { // fallback
    delete elem[event];
  }
}

function getInnerHeight() {
  if ('innerHeight' in window) {
    return window.innerHeight;
  }

  return document.documentElement.clientHeight;
}

// Minimalistic WeakMap shim, just in case.
const WeakMap = window.WeakMap || window.MozWeakMap ||
class WeakMap {
  constructor() {
    this.keys = [];
    this.values = [];
  }

  get(key) {
    for (let i = 0; i < this.keys.length; i++) {
      const item = this.keys[i];
      if (item === key) {
        return this.values[i];
      }
    }
    return undefined;
  }

  set(key, value) {
    for (let i = 0; i < this.keys.length; i++) {
      const item = this.keys[i];
      if (item === key) {
        this.values[i] = value;
        return this;
      }
    }
    this.keys.push(key);
    this.values.push(value);
    return this;
  }
};

// Dummy MutationObserver, to avoid raising exceptions.
const MutationObserver =
  window.MutationObserver || window.WebkitMutationObserver ||
  window.MozMutationObserver ||
  class MutationObserver {
    constructor() {
      if (typeof console !== 'undefined' && console !== null) {
        console.warn('MutationObserver is not supported by your browser.');
        console.warn(
          'WOW.js cannot detect dom mutations, please call .sync() after loading new content.'
        );
      }
    }

    static notSupported = true;

    observe() {}
  };

// getComputedStyle shim, from http://stackoverflow.com/a/21797294
const getComputedStyle = window.getComputedStyle ||
function getComputedStyle(el) {
  const getComputedStyleRX = /(\-([a-z]){1})/g;
  return {
    getPropertyValue(prop) {
      if (prop === 'float') { prop = 'styleFloat'; }
      if (getComputedStyleRX.test(prop)) {
        prop.replace(getComputedStyleRX, (_, _char) => _char.toUpperCase());
      }
      const { currentStyle } = el;
      return (currentStyle != null ? currentStyle[prop] : void 0) || null;
    },
  };
};

export default class WOW {
  defaults = {
    boxClass: 'wow',
    animateClass: 'animated',
    offset: 0,
    mobile: true,
    live: true,
    callback: null,
    scrollContainer: null,
    resetAnimation: true,
  };

  constructor(options = {}) {
    this.start = this.start.bind(this);
    this.resetAnimation = this.resetAnimation.bind(this);
    this.scrollHandler = this.scrollHandler.bind(this);
    this.scrollCallback = this.scrollCallback.bind(this);
    this.scrolled = true;
    this.config = extend(options, this.defaults);
    if (options.scrollContainer != null) {
      this.config.scrollContainer = document.querySelector(options.scrollContainer);
    }
  // Map of elements to animation names:
    this.animationNameCache = new WeakMap();
    this.wowEvent = createEvent(this.config.boxClass);
  }

  init() {
    this.element = window.document.documentElement;
    if (isIn(document.readyState, ['interactive', 'complete'])) {
      this.start();
    } else {
      addEvent(document, 'DOMContentLoaded', this.start);
    }
    this.finished = [];
  }

  start() {
    this.stopped = false;
    this.boxes = [].slice.call(this.element.querySelectorAll(`.${this.config.boxClass}`));
    this.all = this.boxes.slice(0);
    if (this.boxes.length) {
      if (this.disabled()) {
        this.resetStyle();
      } else {
        for (let i = 0; i < this.boxes.length; i++) {
          const box = this.boxes[i];
          this.applyStyle(box, true);
        }
      }
    }
    if (!this.disabled()) {
      addEvent(this.config.scrollContainer || window, 'scroll', this.scrollHandler);
      addEvent(window, 'resize', this.scrollHandler);
      this.interval = setInterval(this.scrollCallback, 50);
    }
    if (this.config.live) {
      const mut = new MutationObserver(records => {
        for (let j = 0; j < records.length; j++) {
          const record = records[j];
          for (let k = 0; k < record.addedNodes.length; k++) {
            const node = record.addedNodes[k];
            this.doSync(node);
          }
        }
        return undefined;
      });
      mut.observe(document.body, {
        childList: true,
        subtree: true,
      });
    }
  }

// unbind the scroll event
  stop() {
    this.stopped = true;
    removeEvent(this.config.scrollContainer || window, 'scroll', this.scrollHandler);
    removeEvent(window, 'resize', this.scrollHandler);
    if (this.interval != null) {
      clearInterval(this.interval);
    }
  }

  sync() {
    if (MutationObserver.notSupported) {
      this.doSync(this.element);
    }
  }

  doSync(element) {
    if (typeof element === 'undefined' || element === null) { ({ element } = this); }
    if (element.nodeType !== 1) { return; }
    element = element.parentNode || element;
    const iterable = element.querySelectorAll(`.${this.config.boxClass}`);
    for (let i = 0; i < iterable.length; i++) {
      const box = iterable[i];
      if (!isIn(box, this.all)) {
        this.boxes.push(box);
        this.all.push(box);
        if (this.stopped || this.disabled()) {
          this.resetStyle();
        } else {
          this.applyStyle(box, true);
        }
        this.scrolled = true;
      }
    }
  }

// show box element
  show(box) {
    this.applyStyle(box);
    box.className = `${box.className} ${this.config.animateClass}`;
    if (this.config.callback != null) { this.config.callback(box); }
    emitEvent(box, this.wowEvent);


    if (this.config.resetAnimation) {
      addEvent(box, 'animationend', this.resetAnimation);
      addEvent(box, 'oanimationend', this.resetAnimation);
      addEvent(box, 'webkitAnimationEnd', this.resetAnimation);
      addEvent(box, 'MSAnimationEnd', this.resetAnimation);
    }

    return box;
  }

  applyStyle(box, hidden) {
    const duration = box.getAttribute('data-wow-duration');
    const delay = box.getAttribute('data-wow-delay');
    const iteration = box.getAttribute('data-wow-iteration');

    return this.animate(() => this.customStyle(box, hidden, duration, delay, iteration));
  }

  animate = (function animateFactory() {
    if ('requestAnimationFrame' in window) {
      return callback => window.requestAnimationFrame(callback);
    }
    return callback => callback();
  }());

  resetStyle() {
    for (let i = 0; i < this.boxes.length; i++) {
      const box = this.boxes[i];
      box.style.visibility = 'visible';
    }
    return undefined;
  }

  resetAnimation(event) {
    if (event.type.toLowerCase().indexOf('animationend') >= 0) {
      const target = event.target || event.srcElement;
      target.className = target.className.replace(this.config.animateClass, '').trim();
    }
  }

  customStyle(box, hidden, duration, delay, iteration) {
    if (hidden) { this.cacheAnimationName(box); }
    box.style.visibility = hidden ? 'hidden' : 'visible';

    if (duration) { this.vendorSet(box.style, { animationDuration: duration }); }
    if (delay) { this.vendorSet(box.style, { animationDelay: delay }); }
    if (iteration) { this.vendorSet(box.style, { animationIterationCount: iteration }); }
    this.vendorSet(box.style, { animationName: hidden ? 'none' : this.cachedAnimationName(box) });

    return box;
  }

  vendors = ['moz', 'webkit'];
  vendorSet(elem, properties) {
    for (const name in properties) {
      if (properties.hasOwnProperty(name)) {
        const value = properties[name];
        elem[`${name}`] = value;
        for (let i = 0; i < this.vendors.length; i++) {
          const vendor = this.vendors[i];
          elem[`${vendor}${name.charAt(0).toUpperCase()}${name.substr(1)}`] = value;
        }
      }
    }
  }
  vendorCSS(elem, property) {
    const style = getComputedStyle(elem);
    let result = style.getPropertyCSSValue(property);
    for (let i = 0; i < this.vendors.length; i++) {
      const vendor = this.vendors[i];
      result = result || style.getPropertyCSSValue(`-${vendor}-${property}`);
    }
    return result;
  }

  animationName(box) {
    let aName;
    try {
      aName = this.vendorCSS(box, 'animation-name').cssText;
    } catch (error) { // Opera, fall back to plain property value
      aName = getComputedStyle(box).getPropertyValue('animation-name');
    }

    if (aName === 'none') {
      return '';  // SVG/Firefox, unable to get animation name?
    }

    return aName;
  }

  cacheAnimationName(box) {
  // https://bugzilla.mozilla.org/show_bug.cgi?id=921834
  // box.dataset is not supported for SVG elements in Firefox
    return this.animationNameCache.set(box, this.animationName(box));
  }
  cachedAnimationName(box) {
    return this.animationNameCache.get(box);
  }

  // fast window.scroll callback
  scrollHandler() {
    this.scrolled = true;
  }

  scrollCallback() {
    if (this.scrolled) {
      this.scrolled = false;
      const results = [];
      for (let i = 0; i < this.boxes.length; i++) {
        const box = this.boxes[i];
        if (box) {
          if (this.isVisible(box)) {
            this.show(box);
            continue;
          }
          results.push(box);
        }
      }
      this.boxes = results;
      if (!this.boxes.length && !this.config.live) {
        this.stop();
      }
    }
  }


  // Calculate element offset top
  offsetTop(element) {
    // SVG elements don't have an offsetTop in Firefox.
    // This will use their nearest parent that has an offsetTop.
    // Also, using ('offsetTop' of element) causes an exception in Firefox.
    while (element.offsetTop === undefined) {
      element = element.parentNode;
    }
    let top = element.offsetTop;
    while (element.offsetParent) {
      element = element.offsetParent;
      top += element.offsetTop;
    }
    return top;
  }

// check if box is visible
  isVisible(box) {
    const offset = box.getAttribute('data-wow-offset') || this.config.offset;
    const viewTop = (
      this.config.scrollContainer && this.config.scrollContainer.scrollTop
    ) || window.pageYOffset;
    const viewBottom =
      viewTop + Math.min(this.element.clientHeight, getInnerHeight()) - offset;
    const top = this.offsetTop(box);
    const bottom = top + box.clientHeight;

    return top <= viewBottom && bottom >= viewTop;
  }

  disabled() {
    return !this.config.mobile && isMobile(navigator.userAgent);
  }
}
