import Util from '../util'

/**
 * --------------------------------------------------------------------------
 * Bootstrap (v4.0.0-beta): dom/selectorEngine.js
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 * --------------------------------------------------------------------------
 */

const SelectorEngine = (() => {
  /**
   * ------------------------------------------------------------------------
   * Polyfills
   * ------------------------------------------------------------------------
   */

  // matches polyfill (see: https://mzl.la/2ikXneG)
  let fnMatches = null
  if (!Element.prototype.matches) {
    fnMatches =
      Element.prototype.msMatchesSelector ||
      Element.prototype.webkitMatchesSelector
  } else {
    fnMatches = Element.prototype.matches
  }

  // closest polyfill (see: https://mzl.la/2vXggaI)
  let fnClosest = null
  if (!Element.prototype.closest) {
    fnClosest = (element, selector) => {
      let ancestor = element
      do {
        if (fnMatches.call(ancestor, selector)) {
          return ancestor
        }

        ancestor = ancestor.parentElement
      } while (ancestor !== null && ancestor.nodeType === Node.ELEMENT_NODE)

      return null
    }
  } else {
    // eslint-disable-next-line arrow-body-style
    fnClosest = (element, selector) => {
      return element.closest(selector)
    }
  }

  const scopeSelectorRegex = /:scope\b/
  const supportScopeQuery = (() => {
    const element = document.createElement('div')
    try {
      element.querySelectorAll(':scope *')
    } catch (e) {
      return false
    }

    return true
  })()

  let findFn = null
  let findOneFn = null
  if (supportScopeQuery) {
    findFn = Element.prototype.querySelectorAll
    findOneFn = Element.prototype.querySelector
  } else {
    findFn = function (selector) {
      if (!scopeSelectorRegex.test(selector)) {
        return this.querySelectorAll(selector)
      }

      const hasId = Boolean(this.id)
      if (!hasId) {
        this.id = Util.getUID('scope')
      }

      let nodeList = null
      try {
        selector = selector.replace(scopeSelectorRegex, `#${this.id}`)
        nodeList = this.querySelectorAll(selector)
      } finally {
        if (!hasId) {
          this.removeAttribute('id')
        }
      }

      return nodeList
    }

    findOneFn = function (selector) {
      if (!scopeSelectorRegex.test(selector)) {
        return this.querySelector(selector)
      }

      const matches = findFn.call(this, selector)
      if (typeof matches[0] !== 'undefined') {
        return matches[0]
      }

      return null
    }
  }

  return {
    matches(element, selector) {
      return fnMatches.call(element, selector)
    },

    find(selector, element = document.documentElement) {
      if (typeof selector !== 'string') {
        return null
      }

      return findFn.call(element, selector)
    },

    findOne(selector, element = document.documentElement) {
      if (typeof selector !== 'string') {
        return null
      }

      return findOneFn.call(element, selector)
    },

    children(element, selector) {
      if (typeof selector !== 'string') {
        return null
      }

      const children = Util.makeArray(element.children)
      return children.filter((child) => this.matches(child, selector))
    },

    parents(element, selector) {
      if (typeof selector !== 'string') {
        return null
      }

      const parents = []

      let ancestor = element.parentNode
      while (ancestor && ancestor.nodeType === Node.ELEMENT_NODE) {
        if (fnMatches.call(ancestor, selector)) {
          parents.push(ancestor)
        }

        ancestor = ancestor.parentNode
      }

      return parents
    },

    closest(element, selector) {
      return fnClosest(element, selector)
    },

    prev(element, selector) {
      if (typeof selector !== 'string') {
        return null
      }

      const siblings = []

      let previous = element.previousSibling
      while (previous) {
        if (fnMatches.call(previous, selector)) {
          siblings.push(previous)
        }

        previous = previous.previousSibling
      }

      return siblings
    }
  }
})()

export default SelectorEngine