import { PREV, NEXT, CHILD, PARENT, IS_CONNECTED, NodeType, HOOKS, OWNER_DOCUMENT } from './constants.mjs';
import { ChildNode, toNode } from './ChildNode.mjs';
import { NodeList } from './NodeList.mjs';
import { querySelectorAll, querySelector } from './selectors.mjs';
import { selfAndDescendants } from './shared.mjs';

class ParentNode extends ChildNode {
  childNodes = (() => new NodeList())();
  children = (() => new NodeList())();
  appendChild(child) {
    this.insertInto(child, null);
  }
  insertBefore(child, ref) {
    this.insertInto(child, ref || null);
  }
  append(...nodes) {
    for (const child of nodes) {
      if (child == null) continue;
      this.appendChild(toNode(this, child));
    }
  }
  prepend(...nodes) {
    const before = this.firstChild;
    for (const child of nodes) {
      if (child == null) continue;
      this.insertBefore(toNode(this, child), before);
    }
  }
  replaceChildren(...nodes) {
    let child;
    while (child = this.firstChild) {
      this.removeChild(child);
    }
    this.append(...nodes);
  }
  removeChild(child) {
    if (child.parentNode !== this) throw Error(`not a child of this node`);
    const prev = child[PREV];
    const next = child[NEXT];
    if (prev) prev[NEXT] = next;else this[CHILD] = next;
    if (next) next[PREV] = prev;
    const childNodes = this.childNodes;
    const childNodesIndex = childNodes.indexOf(child);
    childNodes.splice(childNodesIndex, 1);
    if (child.nodeType === 1) {
      const children = this.children;
      children.splice(children.indexOf(child), 1);
    }
    child[PARENT] = null;
    child[NEXT] = null;
    child[PREV] = null;
    if (this[IS_CONNECTED]) {
      for (const node of selfAndDescendants(child)) {
        node[IS_CONNECTED] = false;
        node.disconnectedCallback?.();
      }
    }
    if (this.nodeType === NodeType.ELEMENT_NODE) {
      this[HOOKS].removeChild?.(this, child, childNodesIndex);
    }
  }
  replaceChild(newChild, oldChild) {
    if (oldChild.parentNode !== this) {
      throw Error('reference node is not a child of this parent');
    }
    const next = oldChild[NEXT];
    this.removeChild(oldChild);
    this.insertInto(newChild, next);
  }
  querySelectorAll(selector) {
    return querySelectorAll(this, selector);
  }
  querySelector(selector) {
    return querySelector(this, selector);
  }
  insertInto(child, before) {
    // append the children of a DocumentFragment:
    if (child.nodeType === NodeType.DOCUMENT_FRAGMENT_NODE) {
      let node = child[CHILD];
      while (node) {
        const next = node[NEXT];
        this.insertInto(node, before);
        node = next;
      }
      return;
    }
    if (child.parentNode !== null) {
      child.parentNode.removeChild(child);
    }
    if (before) {
      if (before.parentNode !== this) {
        throw Error('reference node is not a child of this parent');
      }
      child[NEXT] = before;
      child[PREV] = before[PREV];
      if (before[PREV] === null) this[CHILD] = child;
      before[PREV] = child;
    } else {
      child[NEXT] = null;
      let last = this[CHILD];
      if (last) {
        let next;
        while (next = last[NEXT]) last = next;
        last[NEXT] = child;
        child[PREV] = last;
      } else {
        this[CHILD] = child;
        child[PREV] = null;
      }
    }
    const ownerDocument = this[OWNER_DOCUMENT];
    const isElement = child.nodeType === NodeType.ELEMENT_NODE;
    child[PARENT] = this;
    child[OWNER_DOCUMENT] = ownerDocument;
    const childNodes = this.childNodes;
    let insertIndex;
    if (before) {
      insertIndex = childNodes.indexOf(before);
      childNodes.splice(insertIndex, 0, child);
      if (isElement) {
        const children = this.children;
        let ref = before;
        while (ref && ref.nodeType !== 1) ref = ref[NEXT];
        if (ref) {
          children.splice(children.indexOf(ref), 0, child);
        } else {
          children.push(child);
        }
      }
    } else {
      insertIndex = childNodes.length;
      childNodes.push(child);
      if (isElement) this.children.push(child);
    }
    if (this[IS_CONNECTED]) {
      for (const node of selfAndDescendants(child)) {
        node[IS_CONNECTED] = true;
        node.connectedCallback?.();
      }
    }
    if (this.nodeType === NodeType.ELEMENT_NODE) {
      this[HOOKS].insertChild?.(this, child, insertIndex);
    }
  }
}

export { ParentNode };
