"use strict";

const DOMException = require("domexception/webidl2js-wrapper");
const { filter, FILTER_ACCEPT, FILTER_REJECT, FILTER_SKIP } = require("./helpers");

const FIRST = false;
const LAST = true;
const NEXT = false;
const PREVIOUS = true;

exports.implementation = class TreeWalkerImpl {
  constructor(globalObject, args, privateData) {
    this._active = false;
    this.root = privateData.root;
    this.currentNode = this.root;
    this.whatToShow = privateData.whatToShow;
    this.filter = privateData.filter;

    this._globalObject = globalObject;
  }

  get currentNode() {
    return this._currentNode;
  }

  set currentNode(node) {
    if (node === null) {
      throw DOMException.create(this._globalObject, ["Cannot set currentNode to null", "NotSupportedError"]);
    }

    this._currentNode = node;
  }

  parentNode() {
    let node = this._currentNode;
    while (node !== null && node !== this.root) {
      node = node.parentNode;

      if (node !== null && filter(this, node) === FILTER_ACCEPT) {
        return (this._currentNode = node);
      }
    }
    return null;
  }

  firstChild() {
    return this._traverseChildren(FIRST);
  }

  lastChild() {
    return this._traverseChildren(LAST);
  }

  previousSibling() {
    return this._traverseSiblings(PREVIOUS);
  }

  nextSibling() {
    return this._traverseSiblings(NEXT);
  }

  previousNode() {
    let node = this._currentNode;

    while (node !== this.root) {
      let sibling = node.previousSibling;

      while (sibling !== null) {
        node = sibling;
        let result = filter(this, node);

        while (result !== FILTER_REJECT && node.hasChildNodes()) {
          node = node.lastChild;
          result = filter(this, node);
        }

        if (result === FILTER_ACCEPT) {
          return (this._currentNode = node);
        }

        sibling = node.previousSibling;
      }

      if (node === this.root || node.parentNode === null) {
        return null;
      }

      node = node.parentNode;

      if (filter(this, node) === FILTER_ACCEPT) {
        return (this._currentNode = node);
      }
    }

    return null;
  }

  nextNode() {
    let node = this._currentNode;
    let result = FILTER_ACCEPT;

    for (;;) {
      while (result !== FILTER_REJECT && node.hasChildNodes()) {
        node = node.firstChild;
        result = filter(this, node);
        if (result === FILTER_ACCEPT) {
          return (this._currentNode = node);
        }
      }

      do {
        if (node === this.root) {
          return null;
        }

        const sibling = node.nextSibling;

        if (sibling !== null) {
          node = sibling;
          break;
        }

        node = node.parentNode;
      } while (node !== null);

      if (node === null) {
        return null;
      }

      result = filter(this, node);

      if (result === FILTER_ACCEPT) {
        return (this._currentNode = node);
      }
    }
  }

  _traverseChildren(type) {
    let node = this._currentNode;
    node = type === FIRST ? node.firstChild : node.lastChild;

    if (node === null) {
      return null;
    }

    main: for (;;) {
      const result = filter(this, node);

      if (result === FILTER_ACCEPT) {
        return (this._currentNode = node);
      }

      if (result === FILTER_SKIP) {
        const child = type === FIRST ? node.firstChild : node.lastChild;

        if (child !== null) {
          node = child;
          continue;
        }
      }

      for (;;) {
        const sibling = type === FIRST ? node.nextSibling : node.previousSibling;

        if (sibling !== null) {
          node = sibling;
          continue main;
        }

        const parent = node.parentNode;

        if (parent === null || parent === this.root || parent === this._currentNode) {
          return null;
        }

        node = parent;
      }
    }
  }

  _traverseSiblings(type) {
    let node = this._currentNode;

    if (node === this.root) {
      return null;
    }

    for (;;) {
      let sibling = type === NEXT ? node.nextSibling : node.previousSibling;

      while (sibling !== null) {
        node = sibling;
        const result = filter(this, node);

        if (result === FILTER_ACCEPT) {
          return (this._currentNode = node);
        }

        sibling = type === NEXT ? node.firstChild : node.lastChild;

        if (result === FILTER_REJECT || sibling === null) {
          sibling = type === NEXT ? node.nextSibling : node.previousSibling;
        }
      }

      node = node.parentNode;

      if (node === null || node === this.root) {
        return null;
      }

      if (filter(this, node) === FILTER_ACCEPT) {
        return null;
      }
    }
  }
};
