/**
@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
*/
import '../utils/boot.js';
import { wrap } from '../utils/wrap.js';
import '../utils/settings.js';
import { FlattenedNodesObserver } from '../utils/flattened-nodes-observer.js';
export { flush, enqueueDebouncer as addDebouncer } from '../utils/flush.js';
/* eslint-disable no-unused-vars */

import { Debouncer } from '../utils/debounce.js'; // used in type annotations

/* eslint-enable no-unused-vars */

const p = Element.prototype;
/**
 * @const {function(this:Node, string): boolean}
 */

const normalizedMatchesSelector = p.matches || p.matchesSelector || p.mozMatchesSelector || p.msMatchesSelector || p.oMatchesSelector || p.webkitMatchesSelector;
/**
 * Cross-platform `element.matches` shim.
 *
 * @function matchesSelector
 * @param {!Node} node Node to check selector against
 * @param {string} selector Selector to match
 * @return {boolean} True if node matched selector
 */

export const matchesSelector = function (node, selector) {
  return normalizedMatchesSelector.call(node, selector);
};
/**
 * Node API wrapper class returned from `Polymer.dom.(target)` when
 * `target` is a `Node`.
 * @implements {PolymerDomApi}
 * @unrestricted
 */

class DomApiNative {
  /**
   * @param {!Node} node Node for which to create a Polymer.dom helper object.
   */
  constructor(node) {
    if (window['ShadyDOM'] && window['ShadyDOM']['inUse']) {
      window['ShadyDOM']['patch'](node);
    }

    this.node = node;
  }
  /**
   * Returns an instance of `FlattenedNodesObserver` that
   * listens for node changes on this element.
   *
   * @param {function(this:HTMLElement, { target: !HTMLElement, addedNodes: !Array<!Element>, removedNodes: !Array<!Element> }):void} callback Called when direct or distributed children
   *   of this element changes
   * @return {!PolymerDomApi.ObserveHandle} Observer instance
   * @override
   */


  observeNodes(callback) {
    return new FlattenedNodesObserver(
    /** @type {!HTMLElement} */
    this.node, callback);
  }
  /**
   * Disconnects an observer previously created via `observeNodes`
   *
   * @param {!PolymerDomApi.ObserveHandle} observerHandle Observer instance
   *   to disconnect.
   * @return {void}
   * @override
   */


  unobserveNodes(observerHandle) {
    observerHandle.disconnect();
  }
  /**
   * Provided as a backwards-compatible API only.  This method does nothing.
   * @return {void}
   */


  notifyObserver() {}
  /**
   * Returns true if the provided node is contained with this element's
   * light-DOM children or shadow root, including any nested shadow roots
   * of children therein.
   *
   * @param {Node} node Node to test
   * @return {boolean} Returns true if the given `node` is contained within
   *   this element's light or shadow DOM.
   * @override
   */


  deepContains(node) {
    if (wrap(this.node).contains(node)) {
      return true;
    }

    let n = node;
    let doc = node.ownerDocument; // walk from node to `this` or `document`

    while (n && n !== doc && n !== this.node) {
      // use logical parentnode, or native ShadowRoot host
      n = wrap(n).parentNode || wrap(n).host;
    }

    return n === this.node;
  }
  /**
   * Returns the root node of this node.  Equivalent to `getRootNode()`.
   *
   * @return {!Node} Top most element in the dom tree in which the node
   * exists. If the node is connected to a document this is either a
   * shadowRoot or the document; otherwise, it may be the node
   * itself or a node or document fragment containing it.
   * @override
   */


  getOwnerRoot() {
    return wrap(this.node).getRootNode();
  }
  /**
   * For slot elements, returns the nodes assigned to the slot; otherwise
   * an empty array. It is equivalent to `<slot>.addignedNodes({flatten:true})`.
   *
   * @return {!Array<!Node>} Array of assigned nodes
   * @override
   */


  getDistributedNodes() {
    return this.node.localName === 'slot' ? wrap(this.node).assignedNodes({
      flatten: true
    }) : [];
  }
  /**
   * Returns an array of all slots this element was distributed to.
   *
   * @return {!Array<!HTMLSlotElement>} Description
   * @override
   */


  getDestinationInsertionPoints() {
    let ip$ = [];
    let n = wrap(this.node).assignedSlot;

    while (n) {
      ip$.push(n);
      n = wrap(n).assignedSlot;
    }

    return ip$;
  }
  /**
   * Calls `importNode` on the `ownerDocument` for this node.
   *
   * @param {!Node} node Node to import
   * @param {boolean} deep True if the node should be cloned deeply during
   *   import
   * @return {Node} Clone of given node imported to this owner document
   */


  importNode(node, deep) {
    let doc = this.node instanceof Document ? this.node : this.node.ownerDocument;
    return wrap(doc).importNode(node, deep);
  }
  /**
   * @return {!Array<!Node>} Returns a flattened list of all child nodes and
   * nodes assigned to child slots.
   * @override
   */


  getEffectiveChildNodes() {
    return FlattenedNodesObserver.getFlattenedNodes(
    /** @type {!HTMLElement} */
    this.node);
  }
  /**
   * Returns a filtered list of flattened child elements for this element based
   * on the given selector.
   *
   * @param {string} selector Selector to filter nodes against
   * @return {!Array<!HTMLElement>} List of flattened child elements
   * @override
   */


  queryDistributedElements(selector) {
    let c$ = this.getEffectiveChildNodes();
    let list = [];

    for (let i = 0, l = c$.length, c; i < l && (c = c$[i]); i++) {
      if (c.nodeType === Node.ELEMENT_NODE && matchesSelector(c, selector)) {
        list.push(c);
      }
    }

    return list;
  }
  /**
   * For shadow roots, returns the currently focused element within this
   * shadow root.
   *
   * return {Node|undefined} Currently focused element
   * @override
   */


  get activeElement() {
    let node = this.node;
    return node._activeElement !== undefined ? node._activeElement : node.activeElement;
  }

}

function forwardMethods(proto, methods) {
  for (let i = 0; i < methods.length; i++) {
    let method = methods[i];
    /* eslint-disable valid-jsdoc */

    proto[method] =
    /** @this {DomApiNative} */
    function () {
      return this.node[method].apply(this.node, arguments);
    };
    /* eslint-enable */

  }
}

function forwardReadOnlyProperties(proto, properties) {
  for (let i = 0; i < properties.length; i++) {
    let name = properties[i];
    Object.defineProperty(proto, name, {
      get: function () {
        const domApi =
        /** @type {DomApiNative} */
        this;
        return domApi.node[name];
      },
      configurable: true
    });
  }
}

function forwardProperties(proto, properties) {
  for (let i = 0; i < properties.length; i++) {
    let name = properties[i];
    Object.defineProperty(proto, name, {
      /**
       * @this {DomApiNative}
       * @return {*} .
       */
      get: function () {
        return this.node[name];
      },

      /**
       * @this {DomApiNative}
       * @param {*} value .
       */
      set: function (value) {
        this.node[name] = value;
      },
      configurable: true
    });
  }
}
/**
 * Event API wrapper class returned from `dom.(target)` when
 * `target` is an `Event`.
 */


export class EventApi {
  constructor(event) {
    this.event = event;
  }
  /**
   * Returns the first node on the `composedPath` of this event.
   *
   * @return {!EventTarget} The node this event was dispatched to
   */


  get rootTarget() {
    return this.path[0];
  }
  /**
   * Returns the local (re-targeted) target for this event.
   *
   * @return {!EventTarget} The local (re-targeted) target for this event.
   */


  get localTarget() {
    return this.event.target;
  }
  /**
   * Returns the `composedPath` for this event.
   * @return {!Array<!EventTarget>} The nodes this event propagated through
   */


  get path() {
    return this.event.composedPath();
  }

}
/**
 * @function
 * @param {boolean=} deep
 * @return {!Node}
 */

DomApiNative.prototype.cloneNode;
/**
 * @function
 * @param {!Node} node
 * @return {!Node}
 */

DomApiNative.prototype.appendChild;
/**
 * @function
 * @param {!Node} newChild
 * @param {Node} refChild
 * @return {!Node}
 */

DomApiNative.prototype.insertBefore;
/**
 * @function
 * @param {!Node} node
 * @return {!Node}
 */

DomApiNative.prototype.removeChild;
/**
 * @function
 * @param {!Node} oldChild
 * @param {!Node} newChild
 * @return {!Node}
 */

DomApiNative.prototype.replaceChild;
/**
 * @function
 * @param {string} name
 * @param {string} value
 * @return {void}
 */

DomApiNative.prototype.setAttribute;
/**
 * @function
 * @param {string} name
 * @return {void}
 */

DomApiNative.prototype.removeAttribute;
/**
 * @function
 * @param {string} selector
 * @return {?Element}
 */

DomApiNative.prototype.querySelector;
/**
 * @function
 * @param {string} selector
 * @return {!NodeList<!Element>}
 */

DomApiNative.prototype.querySelectorAll;
/** @type {?Node} */

DomApiNative.prototype.parentNode;
/** @type {?Node} */

DomApiNative.prototype.firstChild;
/** @type {?Node} */

DomApiNative.prototype.lastChild;
/** @type {?Node} */

DomApiNative.prototype.nextSibling;
/** @type {?Node} */

DomApiNative.prototype.previousSibling;
/** @type {?HTMLElement} */

DomApiNative.prototype.firstElementChild;
/** @type {?HTMLElement} */

DomApiNative.prototype.lastElementChild;
/** @type {?HTMLElement} */

DomApiNative.prototype.nextElementSibling;
/** @type {?HTMLElement} */

DomApiNative.prototype.previousElementSibling;
/** @type {!Array<!Node>} */

DomApiNative.prototype.childNodes;
/** @type {!Array<!HTMLElement>} */

DomApiNative.prototype.children;
/** @type {?DOMTokenList} */

DomApiNative.prototype.classList;
/** @type {string} */

DomApiNative.prototype.textContent;
/** @type {string} */

DomApiNative.prototype.innerHTML;
let DomApiImpl = DomApiNative;

if (window['ShadyDOM'] && window['ShadyDOM']['inUse'] && window['ShadyDOM']['noPatch'] && window['ShadyDOM']['Wrapper']) {
  /**
   * @private
   * @extends {HTMLElement}
   */
  class Wrapper extends window['ShadyDOM']['Wrapper'] {} // copy bespoke API onto wrapper


  Object.getOwnPropertyNames(DomApiNative.prototype).forEach(prop => {
    if (prop != 'activeElement') {
      Wrapper.prototype[prop] = DomApiNative.prototype[prop];
    }
  }); // Note, `classList` is here only for legacy compatibility since it does not
  // trigger distribution in v1 Shadow DOM.

  forwardReadOnlyProperties(Wrapper.prototype, ['classList']);
  DomApiImpl = Wrapper;
  Object.defineProperties(EventApi.prototype, {
    // Returns the "lowest" node in the same root as the event's currentTarget.
    // When in `noPatch` mode, this must be calculated by walking the event's
    // path.
    localTarget: {
      get() {
        const current = this.event.currentTarget;
        const currentRoot = current && dom(current).getOwnerRoot();
        const p$ = this.path;

        for (let i = 0; i < p$.length; i++) {
          const e = p$[i];

          if (dom(e).getOwnerRoot() === currentRoot) {
            return e;
          }
        }
      },

      configurable: true
    },
    path: {
      get() {
        return window['ShadyDOM']['composedPath'](this.event);
      },

      configurable: true
    }
  });
} else {
  // Methods that can provoke distribution or must return the logical, not
  // composed tree.
  forwardMethods(DomApiNative.prototype, ['cloneNode', 'appendChild', 'insertBefore', 'removeChild', 'replaceChild', 'setAttribute', 'removeAttribute', 'querySelector', 'querySelectorAll', 'attachShadow']); // Properties that should return the logical, not composed tree. Note, `classList`
  // is here only for legacy compatibility since it does not trigger distribution
  // in v1 Shadow DOM.

  forwardReadOnlyProperties(DomApiNative.prototype, ['parentNode', 'firstChild', 'lastChild', 'nextSibling', 'previousSibling', 'firstElementChild', 'lastElementChild', 'nextElementSibling', 'previousElementSibling', 'childNodes', 'children', 'classList', 'shadowRoot']);
  forwardProperties(DomApiNative.prototype, ['textContent', 'innerHTML', 'className']);
}

export const DomApi = DomApiImpl;
/**
 * Legacy DOM and Event manipulation API wrapper factory used to abstract
 * differences between native Shadow DOM and "Shady DOM" when polyfilling on
 * older browsers.
 *
 * Note that in Polymer 2.x use of `Polymer.dom` is no longer required and
 * in the majority of cases simply facades directly to the standard native
 * API.
 *
 * @summary Legacy DOM and Event manipulation API wrapper factory used to
 * abstract differences between native Shadow DOM and "Shady DOM."
 * @param {(Node|Event|DomApiNative|EventApi)=} obj Node or event to operate on
 * @return {!DomApiNative|!EventApi} Wrapper providing either node API or event API
 */

export const dom = function (obj) {
  obj = obj || document;

  if (obj instanceof DomApiImpl) {
    return (
      /** @type {!DomApi} */
      obj
    );
  }

  if (obj instanceof EventApi) {
    return (
      /** @type {!EventApi} */
      obj
    );
  }

  let helper = obj['__domApi'];

  if (!helper) {
    if (obj instanceof Event) {
      helper = new EventApi(obj);
    } else {
      helper = new DomApiImpl(
      /** @type {Node} */
      obj);
    }

    obj['__domApi'] = helper;
  }

  return helper;
};