/**
@license
Copyright (c) 2017 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
*/

'use strict';

import {nativeShadow} from './style-settings.js';
import StyleTransformer from './style-transformer.js';
import {getIsExtends, elementHasBuiltCss, wrap} from './style-util.js';

export let flush = function () {};

/**
 * @param {!Element} element
 * @return {string}
 */
function getClasses(element) {
  if (element.classList && element.classList.value) {
    return element.classList.value;
  } else {
    // NOTE: className is patched to remove scoping classes in ShadyDOM
    // use getAttribute('class') instead, which is unpatched
    return element.getAttribute('class') || '';
  }
}

const scopeRegExp = new RegExp(`${StyleTransformer.SCOPE_NAME}\\s*([^\\s]*)`);

/**
 * @param {!Element} element
 * @return {string}
 */
export function getCurrentScope(element) {
  const match = getClasses(element).match(scopeRegExp);
  if (match) {
    return match[1];
  } else {
    return '';
  }
}

/**
 * @param {!Node} node
 */
export function getOwnerScope(node) {
  const ownerRoot = wrap(node).getRootNode();
  if (ownerRoot === node || ownerRoot === node.ownerDocument) {
    return '';
  }
  const host = /** @type {!ShadowRoot} */ (ownerRoot).host;
  if (!host) {
    // this may actually be a document fragment
    return '';
  }
  return getIsExtends(host).is;
}

/**
 * @param {!Element} element
 */
export function ensureCorrectScope(element) {
  const currentScope = getCurrentScope(element);
  const ownerRoot = wrap(element).getRootNode();
  if (ownerRoot === element) {
    return;
  }
  if (currentScope && ownerRoot === element.ownerDocument) {
    // node was scoped, but now is in document
    StyleTransformer.domRemoveScope(element, currentScope);
  } else if (ownerRoot instanceof ShadowRoot) {
    const ownerScope = getOwnerScope(element);
    if (ownerScope !== currentScope) {
      // node was scoped, but not by its current owner
      StyleTransformer.domReplaceScope(element, currentScope, ownerScope);
    }
  }
}

/**
 * @param {!HTMLElement|!HTMLDocument} element
 */
export function ensureCorrectSubtreeScoping(element) {
  // find unscoped subtree nodes
  const unscopedNodes = window['ShadyDOM']['nativeMethods'][
    'querySelectorAll'
  ].call(element, `:not(.${StyleTransformer.SCOPE_NAME})`);

  for (let j = 0; j < unscopedNodes.length; j++) {
    // it's possible, during large batch inserts, that nodes that aren't
    // scoped within the current scope were added.
    // To make sure that any unscoped nodes that were inserted in the current batch are correctly styled,
    // query all unscoped nodes and force their style-scope to be applied.
    // This could happen if a sub-element appended an unscoped node in its shadowroot and this function
    // runs on a parent element of the host of that unscoped node:
    // parent-element -> element -> unscoped node
    // Here unscoped node should have the style-scope element, not parent-element.
    const unscopedNode = unscopedNodes[j];
    const scopeForPreviouslyUnscopedNode = getOwnerScope(unscopedNode);
    if (scopeForPreviouslyUnscopedNode) {
      StyleTransformer.element(unscopedNode, scopeForPreviouslyUnscopedNode);
    }
  }
}

/**
 * @param {HTMLElement} el
 * @return {boolean}
 */
function isElementWithBuiltCss(el) {
  if (el.localName === 'style' || el.localName === 'template') {
    return elementHasBuiltCss(el);
  }
  return false;
}

/**
 * @param {Array<MutationRecord|null>|null} mxns
 */
function handler(mxns) {
  for (let x = 0; x < mxns.length; x++) {
    let mxn = mxns[x];
    if (
      mxn.target === document.documentElement ||
      mxn.target === document.head
    ) {
      continue;
    }
    for (let i = 0; i < mxn.addedNodes.length; i++) {
      let n = mxn.addedNodes[i];
      if (n.nodeType !== Node.ELEMENT_NODE) {
        continue;
      }
      n = /** @type {HTMLElement} */ (n); // eslint-disable-line no-self-assign
      let root = n.getRootNode();
      let currentScope = getCurrentScope(n);
      // node was scoped, but now is in document
      // If this element has built css, we must not remove scoping as this node
      // will be used as a template or style without re - applying scoping as an optimization
      if (
        currentScope &&
        root === n.ownerDocument &&
        !isElementWithBuiltCss(n)
      ) {
        StyleTransformer.domRemoveScope(n, currentScope);
      } else if (root instanceof ShadowRoot) {
        const newScope = getOwnerScope(n);
        // rescope current node and subtree if necessary
        if (newScope !== currentScope) {
          StyleTransformer.domReplaceScope(n, currentScope, newScope);
        }
        // make sure all the subtree elements are scoped correctly
        ensureCorrectSubtreeScoping(n);
      }
    }
  }
}

// if native Shadow DOM is being used, or ShadyDOM handles dynamic scoiping, do not activate the MutationObserver
if (
  !nativeShadow &&
  !(window['ShadyDOM'] && window['ShadyDOM']['handlesDynamicScoping'])
) {
  let observer = new MutationObserver(handler);
  let start = (node) => {
    observer.observe(node, {childList: true, subtree: true});
  };
  let nativeCustomElements =
    window['customElements'] &&
    !window['customElements']['polyfillWrapFlushCallback'];
  // need to start immediately with native custom elements
  // TODO(dfreedm): with polyfilled HTMLImports and native custom elements
  // excessive mutations may be observed; this can be optimized via cooperation
  // with the HTMLImports polyfill.
  if (nativeCustomElements) {
    start(document);
  } else {
    let delayedStart = () => {
      start(document.body);
    };
    // use polyfill timing if it's available
    if (window['HTMLImports']) {
      window['HTMLImports']['whenReady'](delayedStart);
      // otherwise push beyond native imports being ready
      // which requires RAF + readystate interactive.
    } else {
      requestAnimationFrame(function () {
        if (document.readyState === 'loading') {
          let listener = function () {
            delayedStart();
            document.removeEventListener('readystatechange', listener);
          };
          document.addEventListener('readystatechange', listener);
        } else {
          delayedStart();
        }
      });
    }
  }

  flush = function () {
    handler(observer.takeRecords());
  };
}
