import type DOMUtils from '../api/dom/DOMUtils';
import * as NodeType from '../dom/NodeType';

import * as RangeNodes from './RangeNodes';
import type { RangeLikeObject } from './RangeTypes';

const walk = (dom: DOMUtils, rng: RangeLikeObject, callback: (nodes: Node[]) => void): void => {
  const startOffset = rng.startOffset;
  const startContainer = RangeNodes.getNode(rng.startContainer, startOffset);
  const endOffset = rng.endOffset;
  const endContainer = RangeNodes.getNode(rng.endContainer, endOffset - 1);

  /**
   * Excludes start/end text node if they are out side the range
   *
   * @private
   * @param {Array} nodes Nodes to exclude items from.
   * @return {Array} Array with nodes excluding the start/end container if needed.
   */
  const exclude = (nodes: Node[]) => {
    // First node is excluded
    const firstNode = nodes[0];
    if (NodeType.isText(firstNode) && firstNode === startContainer && startOffset >= firstNode.data.length) {
      nodes.splice(0, 1);
    }

    // Last node is excluded
    const lastNode = nodes[nodes.length - 1];
    if (endOffset === 0 && nodes.length > 0 && lastNode === endContainer && NodeType.isText(lastNode)) {
      nodes.splice(nodes.length - 1, 1);
    }

    return nodes;
  };

  const collectSiblings = (node: Node | null, name: 'nextSibling' | 'previousSibling', endNode?: Node | null) => {
    const siblings = [];

    for (; node && node !== endNode; node = node[name]) {
      siblings.push(node);
    }

    return siblings;
  };

  const findEndPoint = (node: Node, root: Node) =>
    dom.getParent(node, (node) => node.parentNode === root, root);

  const walkBoundary = (startNode: Node, endNode: Node, next?: boolean) => {
    const siblingName = next ? 'nextSibling' : 'previousSibling';

    for (let node: Node | null = startNode, parent = node.parentNode; node && node !== endNode; node = parent) {
      parent = node.parentNode;
      const siblings = collectSiblings(node === startNode ? node : node[siblingName], siblingName);

      if (siblings.length) {
        if (!next) {
          siblings.reverse();
        }

        callback(exclude(siblings));
      }
    }
  };

  // Same container
  if (startContainer === endContainer) {
    return callback(exclude([ startContainer ]));
  }

  // Find common ancestor and end points
  const ancestor = dom.findCommonAncestor(startContainer, endContainer) ?? dom.getRoot();

  // Process left side
  if (dom.isChildOf(startContainer, endContainer)) {
    return walkBoundary(startContainer, ancestor, true);
  }

  // Process right side
  if (dom.isChildOf(endContainer, startContainer)) {
    return walkBoundary(endContainer, ancestor);
  }

  // Find start/end point
  const startPoint = findEndPoint(startContainer, ancestor) || startContainer;
  const endPoint = findEndPoint(endContainer, ancestor) || endContainer;

  // Walk left leaf
  walkBoundary(startContainer, startPoint, true);

  // Walk the middle from start to end point
  const siblings = collectSiblings(
    startPoint === startContainer ? startPoint : startPoint.nextSibling,
    'nextSibling',
    endPoint === endContainer ? endPoint.nextSibling : endPoint
  );

  if (siblings.length) {
    callback(exclude(siblings));
  }

  // Walk right leaf
  walkBoundary(endContainer, endPoint);
};

export {
  walk
};
