import React from "react";
import { Range } from "../../editor";
import type { Controller } from "../../controller";
import { PID, Word } from "../../persist";

export function isTextNode(element: ChildNode): element is Text {
  return element.nodeType === Node.TEXT_NODE;
}

export function isElementNode(element: ChildNode): element is Element {
  return element.nodeType === Node.ELEMENT_NODE;
}

export function reviseRect(rect: DOMRect, lineHeight: number) {
  if (lineHeight <= rect.height) {
    return rect;
  }

  const offsetY = (lineHeight - rect.height) / 2;
  const r = new DOMRect(rect.x, rect.y - offsetY, rect.width, lineHeight);
  return r;
}

function getMergedRect(rects: DOMRect[]) {
  const mergedRect: DOMRect[] = [];

  rects.forEach(rect => {
    if (~~rect.left === ~~rect.right) {
      return;
    }
    if (mergedRect.length === 0) {
      mergedRect.push(rect);
    }

    const isMerged = mergedRect.some((mRect, index) => {
      if (mRect.top === rect.top && mRect.bottom === rect.bottom && rect.left <= mRect.right) {
        mergedRect[index] = new DOMRect(
          mRect.x,
          mRect.y,
          mRect.width + (rect.right - mRect.right),
          mRect.height
        );
        return true;
      }
      if (
        rect.top <= mRect.top &&
        rect.left <= mRect.left &&
        rect.right <= mRect.right &&
        rect.bottom <= mRect.bottom
      ) {
        return true;
      }
      return false;
    });
    if (!isMerged) {
      mergedRect.push(rect);
    }
  });
  return mergedRect;
}

function walkElement(
  element: ChildNode,
  index: number,
  callback: (elem: Element | Text, index: number) => void
): void {
  if (isTextNode(element)) {
    callback(element, index);
  } else if (isElementNode(element)) {
    const nodes = element.childNodes;
    const { length } = nodes;

    let pos = 0;
    for (let i = 0; i < length; i++) {
      const node = nodes[i];
      if (isElementNode(node)) {
        if (node.className.includes("ignore")) {
          continue;
        }
      }
      const nextPos = pos + (node.textContent || "").length;
      if (pos <= index && index <= nextPos) {
        return walkElement(node, index - pos, callback);
      }
      pos = nextPos;
    }
  }
}

export function getRectsByOffset(
  controller: Controller,
  pid: PID,
  startOffset: number,
  endOffset: number,
  contentElement: HTMLDivElement
) {
  const words = controller.getWords(pid);
  const textContent = controller.getTextContent(pid);
  const { length } = words;
  let pos = 0;
  let startWord: Word | undefined;
  let startWordOffset: number | undefined;
  let endWord: Word | undefined;
  let endWordOffset: number | undefined;

  if (!textContent.length) {
    const rect = contentElement.getBoundingClientRect();

    return [
      {
        ...rect,
        width: 0,
        height: rect.height,
        top: rect.top,
        right: rect.left,
        bottom: rect.bottom,
        left: rect.left,
        x: rect.x,
        y: rect.y,
      },
    ];
  }

  for (let i = 0; i < length; i++) {
    const word = words[i];
    const nextPos = pos + word.text.length;
    if (!startWord && pos <= startOffset && startOffset <= nextPos) {
      startWord = word;
      startWordOffset = startOffset - pos;
    }
    if (!endWord && pos <= endOffset && endOffset <= nextPos) {
      endWord = word;
      endWordOffset = endOffset - pos;
    }
    if (startWord && endWord) {
      break;
    }
    pos = nextPos;
  }

  if (startWord && endWord && startWordOffset != null && endWordOffset != null) {
    const range = document.createRange();
    const startElem = contentElement.querySelector(`[data-word-id="${startWord.wid}"]`)!;
    const endElem = contentElement.querySelector(`[data-word-id="${endWord.wid}"]`)!;

    if (!startElem || !endElem) {
      return undefined;
    }

    walkElement(startElem, startWordOffset, range.setStart.bind(range));

    walkElement(endElem, endWordOffset, range.setEnd.bind(range));

    const rects = getMergedRect(
      Array.from(range.getClientRects()).map(rect => reviseRect(rect, 24))
    );
    return rects;
  }
}

export function getSelectionRects(
  controller: Controller,
  range: Range,
  pid: PID,
  contentElement: HTMLDivElement
): React.CSSProperties[] {
  const contentRect = contentElement.getBoundingClientRect();

  const { start, end } = range;

  const content = controller.getTextContent(pid);

  let startOffset = 0;
  let endOffset = content.length;
  if (start.pid === pid) {
    startOffset = start.offset;
  }

  if (end.pid === pid) {
    endOffset = end.offset;
  }

  if (start.offset === end.offset && start.pid === end.pid) {
    return [];
  }

  const rects = getRectsByOffset(controller, pid, startOffset, endOffset, contentElement) || [];

  return rects.map(rect => {
    return {
      top: rect.top - contentRect.top,
      left: rect.left - contentRect.left,
      width: rect.width,
      height: rect.height,
    };
  });
}
