/**
@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;
};
