/**
 * @license
 * Copyright (c) 2018 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 cloneObject from 'clone';

import { isDocumentFragment, predicates as p } from './predicates.js';
import { queryAll } from './walking.js';

function newTextNode(value: string): any {
  return {
    nodeName: '#text',
    value: value,
    parentNode: undefined,
    attrs: [],
    __location: <any>undefined,
  };
}

function newCommentNode(comment: string): any {
  return {
    nodeName: '#comment',
    data: comment,
    parentNode: undefined,
    attrs: [],
    __location: <any>undefined,
  };
}

function newElement(tagName: string, namespace?: string): any {
  return {
    nodeName: tagName,
    tagName: tagName,
    childNodes: [],
    namespaceURI: namespace || 'http://www.w3.org/1999/xhtml',
    attrs: [],
    parentNode: undefined,
    __location: <any>undefined,
  };
}

function newDocumentFragment(): any {
  return {
    nodeName: '#document-fragment',
    childNodes: [],
    parentNode: undefined,
    quirksMode: false,
    // TODO(rictic): update parse5 typings upstream to mention that attrs and
    //     __location are optional and not always present.
    attrs: undefined as any,
    __location: null as any,
  };
}

export function cloneNode(node: any): any {
  // parent is a backreference, and we don't want to clone the whole tree, so
  // make it null before cloning.
  const parent = node.parentNode;
  node.parentNode = undefined;
  const clone = cloneObject(node);
  node.parentNode = parent;
  return clone;
}

/**
 * Inserts `newNode` into `parent` at `index`, optionally replaceing the
 * current node at `index`. If `newNode` is a DocumentFragment, its childNodes
 * are inserted and removed from the fragment.
 */
function insertNode(parent: any, index: number, newNode: any, replace?: boolean) {
  if (!parent.childNodes) {
    parent.childNodes = [];
  }
  let newNodes: any[] = [];
  let removedNode = replace ? parent.childNodes[index] : null;

  if (newNode) {
    if (isDocumentFragment(newNode)) {
      if (newNode.childNodes) {
        newNodes = Array.from(newNode.childNodes);
        newNode.childNodes.length = 0;
      }
    } else {
      newNodes = [newNode];
      remove(newNode);
    }
  }

  if (replace) {
    removedNode = parent.childNodes[index];
  }

  Array.prototype.splice.apply(parent.childNodes, (<any>[index, replace ? 1 : 0]).concat(newNodes));

  newNodes.forEach(function (n) {
    n.parentNode = parent;
  });

  if (removedNode) {
    removedNode.parentNode = undefined;
  }
}

export function replace(oldNode: any, newNode: any) {
  const parent = oldNode.parentNode;
  const index = parent!.childNodes!.indexOf(oldNode);
  insertNode(parent!, index, newNode, true);
}

export function remove(node: any) {
  const parent = node.parentNode;
  if (parent && parent.childNodes) {
    const idx = parent.childNodes.indexOf(node);
    parent.childNodes.splice(idx, 1);
  }
  node.parentNode = undefined;
}

export function insertBefore(parent: any, target: any, newNode: any) {
  const index = parent.childNodes!.indexOf(target);
  insertNode(parent, index, newNode);
}

export function insertAfter(parent: any, target: any, newNode: any) {
  const index = parent.childNodes!.indexOf(target);
  insertNode(parent, index + 1, newNode);
}

/**
 * Removes a node and places its children in its place.  If the node
 * has no parent, the operation is impossible and no action takes place.
 */
export function removeNodeSaveChildren(node: any) {
  // We can't save the children if there's no parent node to provide
  // for them.
  const fosterParent = node.parentNode;
  if (!fosterParent) {
    return;
  }
  const children = (node.childNodes || []).slice();
  for (const child of children) {
    insertBefore(node.parentNode!, node, child);
  }
  remove(node);
}

/**
 * When parse5 parses an HTML document with `parse`, it injects missing root
 * elements (html, head and body) if they are missing.  This function removes
 * these from the AST if they have no location info, so it requires that
 * the `parse5.parse` be used with the `locationInfo` option of `true`.
 */
export function removeFakeRootElements(ast: Node) {
  const injectedNodes = queryAll(
    ast,
    p.AND(node => !node.__location, p.hasMatchingTagName(/^(html|head|body)$/i)),
    undefined,
    // Don't descend past 3 levels 'document > html > head|body'
    node => (node.parentNode && node.parentNode.parentNode ? undefined : node.childNodes),
  );
  injectedNodes.reverse().forEach(removeNodeSaveChildren);
}

export function append(parent: any, newNode: any) {
  const index = (parent.childNodes && parent.childNodes.length) || 0;
  insertNode(parent, index, newNode);
}

export const constructors = {
  text: newTextNode,
  comment: newCommentNode,
  element: newElement,
  fragment: newDocumentFragment,
};
