/**
 * @license
 * Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
 * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
 * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
 * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
 * Code distributed by Google as part of the polymer project is also
 * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
 */

// minimal template polyfill
;(function () {
  const needsTemplate = typeof HTMLTemplateElement === 'undefined'
  const brokenDocFragment = !(document.createDocumentFragment().cloneNode() instanceof DocumentFragment)
  let needsDocFrag = false

  // NOTE: Replace DocumentFragment to work around IE11 bug that
  // causes children of a document fragment modified while
  // there is a mutation observer to not have a parentNode, or
  // have a broken parentNode (!?!)
  if (/Trident/.test(navigator.userAgent)) {
    ;(function () {
      needsDocFrag = true

      const origCloneNode = Node.prototype.cloneNode
      Node.prototype.cloneNode = function cloneNode(deep) {
        const newDom = origCloneNode.call(this, deep)
        if (this instanceof DocumentFragment) {
          // eslint-disable-next-line no-proto
          newDom.__proto__ = DocumentFragment.prototype
        }
        return newDom
      }

      // IE's DocumentFragment querySelector code doesn't work when
      // called on an element instance
      DocumentFragment.prototype.querySelectorAll = HTMLElement.prototype.querySelectorAll
      DocumentFragment.prototype.querySelector = HTMLElement.prototype.querySelector

      Object.defineProperties(DocumentFragment.prototype, {
        nodeType: {
          get: function () {
            return Node.DOCUMENT_FRAGMENT_NODE
          },
          configurable: true,
        },

        localName: {
          get: function () {
            return undefined
          },
          configurable: true,
        },

        nodeName: {
          get: function () {
            return '#document-fragment'
          },
          configurable: true,
        },
      })

      const origInsertBefore = Node.prototype.insertBefore
      function insertBefore(newNode, refNode) {
        if (newNode instanceof DocumentFragment) {
          let child
          while ((child = newNode.firstChild)) {
            origInsertBefore.call(this, child, refNode)
          }
        } else {
          origInsertBefore.call(this, newNode, refNode)
        }
        return newNode
      }
      Node.prototype.insertBefore = insertBefore

      const origAppendChild = Node.prototype.appendChild
      Node.prototype.appendChild = function appendChild(child) {
        if (child instanceof DocumentFragment) {
          insertBefore.call(this, child, null)
        } else {
          origAppendChild.call(this, child)
        }
        return child
      }

      const origRemoveChild = Node.prototype.removeChild
      const origReplaceChild = Node.prototype.replaceChild
      Node.prototype.replaceChild = function replaceChild(newChild, oldChild) {
        if (newChild instanceof DocumentFragment) {
          insertBefore.call(this, newChild, oldChild)
          origRemoveChild.call(this, oldChild)
        } else {
          origReplaceChild.call(this, newChild, oldChild)
        }
        return oldChild
      }

      Document.prototype.createDocumentFragment = function createDocumentFragment() {
        const frag = this.createElement('df')
        // eslint-disable-next-line no-proto
        frag.__proto__ = DocumentFragment.prototype
        return frag
      }

      var origImportNode = Document.prototype.importNode
      Document.prototype.importNode = function importNode(impNode, deep) {
        deep = deep || false
        var newNode = origImportNode.call(this, impNode, deep)
        if (impNode instanceof DocumentFragment) {
          newNode.__proto__ = DocumentFragment.prototype
        }
        return newNode
      }
    })()
  }

  // NOTE: we rely on this cloneNode not causing element upgrade.
  // This means this polyfill must load before the CE polyfill and
  // this would need to be re-worked if a browser supports native CE
  // but not <template>.
  var capturedCloneNode = Node.prototype.cloneNode
  var capturedCreateElement = Document.prototype.createElement
  var capturedImportNode = Document.prototype.importNode
  var capturedRemoveChild = Node.prototype.removeChild
  var capturedAppendChild = Node.prototype.appendChild
  var capturedReplaceChild = Node.prototype.replaceChild
  var capturedParseFromString = DOMParser.prototype.parseFromString
  var capturedHTMLElementInnerHTML = Object.getOwnPropertyDescriptor(window.HTMLElement.prototype, 'innerHTML') || {
    /**
     * @this {!HTMLElement}
     * @return {string}
     */
    get: function () {
      return this.innerHTML
    },
    /**
     * @this {!HTMLElement}
     * @param {string}
     */
    set: function (text) {
      this.innerHTML = text
    },
  }
  var capturedChildNodes = Object.getOwnPropertyDescriptor(window.Node.prototype, 'childNodes') || {
    /**
     * @this {!Node}
     * @return {!NodeList}
     */
    get: function () {
      return this.childNodes
    },
  }

  var elementQuerySelectorAll = Element.prototype.querySelectorAll
  var docQuerySelectorAll = Document.prototype.querySelectorAll
  var fragQuerySelectorAll = DocumentFragment.prototype.querySelectorAll

  var scriptSelector = 'script:not([type]),script[type="application/javascript"],script[type="text/javascript"]'

  function QSA(node, selector) {
    // IE 11 throws a SyntaxError with `scriptSelector` if the node has no children due to the `:not([type])` syntax
    if (!node.childNodes.length) {
      return []
    }
    switch (node.nodeType) {
      case Node.DOCUMENT_NODE:
        return docQuerySelectorAll.call(node, selector)
      case Node.DOCUMENT_FRAGMENT_NODE:
        return fragQuerySelectorAll.call(node, selector)
      default:
        return elementQuerySelectorAll.call(node, selector)
    }
  }

  // returns true if nested templates cannot be cloned (they cannot be on
  // some impl's like Safari 8 and Edge)
  // OR if cloning a document fragment does not result in a document fragment
  var needsCloning = (function () {
    if (!needsTemplate) {
      var t = document.createElement('template')
      var t2 = document.createElement('template')
      t2.content.appendChild(document.createElement('div'))
      t.content.appendChild(t2)
      var clone = t.cloneNode(true)
      return (
        clone.content.childNodes.length === 0 ||
        clone.content.firstChild.content.childNodes.length === 0 ||
        brokenDocFragment
      )
    }
  })()

  var TEMPLATE_TAG = 'template'
  var PolyfilledHTMLTemplateElement = function () {}

  if (needsTemplate) {
    var contentDoc = document.implementation.createHTMLDocument('template')
    var canDecorate = true

    var templateStyle = document.createElement('style')
    templateStyle.textContent = TEMPLATE_TAG + '{display:none;}'

    var head = document.head
    head.insertBefore(templateStyle, head.firstElementChild)

    /**
      Provides a minimal shim for the <template> element.
    */
    PolyfilledHTMLTemplateElement.prototype = Object.create(HTMLElement.prototype)

    // if elements do not have `innerHTML` on instances, then
    // templates can be patched by swizzling their prototypes.
    var canProtoPatch = !document.createElement('div').hasOwnProperty('innerHTML')

    /**
      The `decorate` method moves element children to the template's `content`.
      NOTE: there is no support for dynamically adding elements to templates.
    */
    PolyfilledHTMLTemplateElement.decorate = function (template) {
      // if the template is decorated or not in HTML namespace, return fast
      if (template.content || template.namespaceURI !== document.documentElement.namespaceURI) {
        return
      }
      template.content = contentDoc.createDocumentFragment()
      var child
      while ((child = template.firstChild)) {
        capturedAppendChild.call(template.content, child)
      }
      // NOTE: prefer prototype patching for performance and
      // because on some browsers (IE11), re-defining `innerHTML`
      // can result in intermittent errors.
      if (canProtoPatch) {
        template.__proto__ = PolyfilledHTMLTemplateElement.prototype
      } else {
        template.cloneNode = function (deep) {
          return PolyfilledHTMLTemplateElement._cloneNode(this, deep)
        }
        // add innerHTML to template, if possible
        // Note: this throws on Safari 7
        if (canDecorate) {
          try {
            defineInnerHTML(template)
            defineOuterHTML(template)
          } catch (err) {
            canDecorate = false
          }
        }
      }
      // bootstrap recursively
      PolyfilledHTMLTemplateElement.bootstrap(template.content)
    }

    // Taken from https://github.com/jquery/jquery/blob/73d7e6259c63ac45f42c6593da8c2796c6ce9281/src/manipulation/wrapMap.js
    var topLevelWrappingMap = {
      option: ['select'],
      thead: ['table'],
      col: ['colgroup', 'table'],
      tr: ['tbody', 'table'],
      th: ['tr', 'tbody', 'table'],
      td: ['tr', 'tbody', 'table'],
    }

    var getTagName = function (text) {
      // Taken from https://github.com/jquery/jquery/blob/73d7e6259c63ac45f42c6593da8c2796c6ce9281/src/manipulation/var/rtagName.js
      return (/<([a-z][^/\0>\x20\t\r\n\f]+)/i.exec(text) || ['', ''])[1].toLowerCase()
    }

    var defineInnerHTML = function defineInnerHTML(obj) {
      Object.defineProperty(obj, 'innerHTML', {
        get: function () {
          return getInnerHTML(this)
        },
        set: function (text) {
          // For IE11, wrap the text in the correct (table) context
          var wrap = topLevelWrappingMap[getTagName(text)]
          if (wrap) {
            for (var i = 0; i < wrap.length; i++) {
              text = '<' + wrap[i] + '>' + text + '</' + wrap[i] + '>'
            }
          }
          contentDoc.body.innerHTML = text
          PolyfilledHTMLTemplateElement.bootstrap(contentDoc)
          while (this.content.firstChild) {
            capturedRemoveChild.call(this.content, this.content.firstChild)
          }
          var body = contentDoc.body
          // If we had wrapped, get back to the original node
          if (wrap) {
            for (var j = 0; j < wrap.length; j++) {
              body = body.lastChild
            }
          }
          while (body.firstChild) {
            capturedAppendChild.call(this.content, body.firstChild)
          }
        },
        configurable: true,
      })
    }

    var defineOuterHTML = function defineOuterHTML(obj) {
      Object.defineProperty(obj, 'outerHTML', {
        get: function () {
          return '<' + TEMPLATE_TAG + '>' + this.innerHTML + '</' + TEMPLATE_TAG + '>'
        },
        set: function (innerHTML) {
          if (this.parentNode) {
            contentDoc.body.innerHTML = innerHTML
            var docFrag = this.ownerDocument.createDocumentFragment()
            while (contentDoc.body.firstChild) {
              capturedAppendChild.call(docFrag, contentDoc.body.firstChild)
            }
            capturedReplaceChild.call(this.parentNode, docFrag, this)
          } else {
            throw new Error("Failed to set the 'outerHTML' property on 'Element': This element has no parent node.")
          }
        },
        configurable: true,
      })
    }

    defineInnerHTML(PolyfilledHTMLTemplateElement.prototype)
    defineOuterHTML(PolyfilledHTMLTemplateElement.prototype)

    /**
      The `bootstrap` method is called automatically and "fixes" all
      <template> elements in the document referenced by the `doc` argument.
    */
    PolyfilledHTMLTemplateElement.bootstrap = function bootstrap(doc) {
      var templates = QSA(doc, TEMPLATE_TAG)
      for (var i = 0, l = templates.length, t; i < l && (t = templates[i]); i++) {
        PolyfilledHTMLTemplateElement.decorate(t)
      }
    }

    // auto-bootstrapping for main document
    document.addEventListener('DOMContentLoaded', function () {
      PolyfilledHTMLTemplateElement.bootstrap(document)
    })

    // Patch document.createElement to ensure newly created templates have content
    Document.prototype.createElement = function createElement() {
      var el = capturedCreateElement.apply(this, arguments)
      if (el.localName === 'template') {
        PolyfilledHTMLTemplateElement.decorate(el)
      }
      return el
    }

    DOMParser.prototype.parseFromString = function () {
      var el = capturedParseFromString.apply(this, arguments)
      PolyfilledHTMLTemplateElement.bootstrap(el)
      return el
    }

    Object.defineProperty(HTMLElement.prototype, 'innerHTML', {
      get: function () {
        return getInnerHTML(this)
      },
      set: function (text) {
        capturedHTMLElementInnerHTML.set.call(this, text)
        PolyfilledHTMLTemplateElement.bootstrap(this)
      },
      configurable: true,
      enumerable: true,
    })

    // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#escapingString
    var escapeAttrRegExp = /[&\u00A0"]/g
    var escapeDataRegExp = /[&\u00A0<>]/g

    var escapeReplace = function (c) {
      switch (c) {
        case '&':
          return '&amp;'
        case '<':
          return '&lt;'
        case '>':
          return '&gt;'
        case '"':
          return '&quot;'
        case '\u00A0':
          return '&nbsp;'
      }
    }

    var escapeAttr = function (s) {
      return s.replace(escapeAttrRegExp, escapeReplace)
    }

    var escapeData = function (s) {
      return s.replace(escapeDataRegExp, escapeReplace)
    }

    var makeSet = function (arr) {
      var set = {}
      for (var i = 0; i < arr.length; i++) {
        set[arr[i]] = true
      }
      return set
    }

    // http://www.whatwg.org/specs/web-apps/current-work/#void-elements
    var voidElements = makeSet([
      'area',
      'base',
      'br',
      'col',
      'command',
      'embed',
      'hr',
      'img',
      'input',
      'keygen',
      'link',
      'meta',
      'param',
      'source',
      'track',
      'wbr',
    ])

    var plaintextParents = makeSet(['style', 'script', 'xmp', 'iframe', 'noembed', 'noframes', 'plaintext', 'noscript'])

    /**
     * @param {Node} node
     * @param {Node} parentNode
     * @param {Function=} callback
     */
    var getOuterHTML = function (node, parentNode, callback) {
      switch (node.nodeType) {
        case Node.ELEMENT_NODE: {
          var tagName = node.localName
          var s = '<' + tagName
          var attrs = node.attributes
          for (var i = 0, attr; (attr = attrs[i]); i++) {
            s += ' ' + attr.name + '="' + escapeAttr(attr.value) + '"'
          }
          s += '>'
          if (voidElements[tagName]) {
            return s
          }
          return s + getInnerHTML(node, callback) + '</' + tagName + '>'
        }
        case Node.TEXT_NODE: {
          var data = /** @type {Text} */ (node).data
          if (parentNode && plaintextParents[parentNode.localName]) {
            return data
          }
          return escapeData(data)
        }
        case Node.COMMENT_NODE: {
          return '<!--' + /** @type {Comment} */ (node).data + '-->'
        }
        default: {
          window.console.error(node)
          throw new Error('not implemented')
        }
      }
    }

    /**
     * @param {Node} node
     * @param {Function=} callback
     */
    var getInnerHTML = function (node, callback) {
      if (node.localName === 'template') {
        node = /** @type {HTMLTemplateElement} */ (node).content
      }
      var s = ''
      var c$ = callback ? callback(node) : capturedChildNodes.get.call(node)
      for (var i = 0, l = c$.length, child; i < l && (child = c$[i]); i++) {
        s += getOuterHTML(child, node, callback)
      }
      return s
    }
  }

  // make cloning/importing work!
  if (needsTemplate || needsCloning) {
    PolyfilledHTMLTemplateElement._cloneNode = function _cloneNode(template, deep) {
      var clone = capturedCloneNode.call(template, false)
      // NOTE: decorate doesn't auto-fix children because they are already
      // decorated so they need special clone fixup.
      if (this.decorate) {
        this.decorate(clone)
      }
      if (deep) {
        // NOTE: use native clone node to make sure CE's wrapped
        // cloneNode does not cause elements to upgrade.
        capturedAppendChild.call(clone.content, capturedCloneNode.call(template.content, true))
        // now ensure nested templates are cloned correctly.
        fixClonedDom(clone.content, template.content)
      }
      return clone
    }

    // Given a source and cloned subtree, find <template>'s in the cloned
    // subtree and replace them with cloned <template>'s from source.
    // We must do this because only the source templates have proper .content.
    var fixClonedDom = function fixClonedDom(clone, source) {
      // do nothing if cloned node is not an element
      if (!source.querySelectorAll) return
      // these two lists should be coincident
      var s$ = QSA(source, TEMPLATE_TAG)
      if (s$.length === 0) {
        return
      }
      var t$ = QSA(clone, TEMPLATE_TAG)
      for (var i = 0, l = t$.length, t, s; i < l; i++) {
        s = s$[i]
        t = t$[i]
        if (PolyfilledHTMLTemplateElement && PolyfilledHTMLTemplateElement.decorate) {
          PolyfilledHTMLTemplateElement.decorate(s)
        }
        capturedReplaceChild.call(t.parentNode, cloneNode.call(s, true), t)
      }
    }

    // make sure scripts inside of a cloned template are executable
    var fixClonedScripts = function fixClonedScripts(fragment) {
      var scripts = QSA(fragment, scriptSelector)
      for (var ns, s, i = 0; i < scripts.length; i++) {
        s = scripts[i]
        ns = capturedCreateElement.call(document, 'script')
        ns.textContent = s.textContent
        var attrs = s.attributes
        for (var ai = 0, a; ai < attrs.length; ai++) {
          a = attrs[ai]
          ns.setAttribute(a.name, a.value)
        }
        capturedReplaceChild.call(s.parentNode, ns, s)
      }
    }

    // override all cloning to fix the cloned subtree to contain properly
    // cloned templates.
    var cloneNode = (Node.prototype.cloneNode = function cloneNode(deep) {
      var dom
      // workaround for Edge bug cloning documentFragments
      // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8619646/
      if (!needsDocFrag && brokenDocFragment && this instanceof DocumentFragment) {
        if (!deep) {
          return this.ownerDocument.createDocumentFragment()
        } else {
          dom = importNode.call(this.ownerDocument, this, true)
        }
      } else if (
        this.nodeType === Node.ELEMENT_NODE &&
        this.localName === TEMPLATE_TAG &&
        this.namespaceURI == document.documentElement.namespaceURI
      ) {
        dom = PolyfilledHTMLTemplateElement._cloneNode(this, deep)
      } else {
        dom = capturedCloneNode.call(this, deep)
      }
      // template.content is cloned iff `deep`.
      if (deep) {
        fixClonedDom(dom, this)
      }
      return dom
    })

    // NOTE: we are cloning instead of importing <template>'s.
    // However, the ownerDocument of the cloned template will be correct!
    // This is because the native import node creates the right document owned
    // subtree and `fixClonedDom` inserts cloned templates into this subtree,
    // thus updating the owner doc.
    var importNode = (Document.prototype.importNode = function importNode(element, deep) {
      deep = deep || false
      if (element.localName === TEMPLATE_TAG) {
        return PolyfilledHTMLTemplateElement._cloneNode(element, deep)
      } else {
        var dom = capturedImportNode.call(this, element, deep)
        if (deep) {
          fixClonedDom(dom, element)
          fixClonedScripts(dom)
        }
        return dom
      }
    })
  }

  if (needsTemplate) {
    window.HTMLTemplateElement = PolyfilledHTMLTemplateElement
  }
})()
