import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';

import type { VElement, VLine } from '../components/index.js';
import type { DomPoint, TextPoint } from '../types.js';

import { INLINE_ROOT_ATTR, ZERO_WIDTH_SPACE } from '../consts.js';
import {
  isInlineRoot,
  isNativeTextInVText,
  isVElement,
  isVLine,
} from './guard.js';
import { calculateTextLength, getTextNodesFromElement } from './text.js';

export function nativePointToTextPoint(
  node: unknown,
  offset: number
): TextPoint | null {
  if (isNativeTextInVText(node)) {
    return [node, offset];
  }

  if (isVElement(node)) {
    const texts = getTextNodesFromElement(node);
    const vElement = texts[0].parentElement?.closest('[data-v-element="true"]');

    if (
      texts.length === 1 &&
      vElement instanceof HTMLElement &&
      vElement.dataset.vEmbed === 'true'
    ) {
      return [texts[0], 0];
    }

    if (texts.length > 0) {
      return texts[offset] ? [texts[offset], 0] : null;
    }
  }

  if (isVLine(node) || isInlineRoot(node)) {
    return getTextPointRoughlyFromElementByOffset(node, offset, true);
  }

  if (!(node instanceof Node)) {
    return null;
  }

  const vNodes = getVNodesFromNode(node);

  if (vNodes) {
    return getTextPointFromVNodes(vNodes, node, offset);
  }

  return null;
}

export function textPointToDomPoint(
  text: Text,
  offset: number,
  rootElement: HTMLElement
): DomPoint | null {
  if (rootElement.dataset.vRoot !== 'true') {
    throw new BlockSuiteError(
      ErrorCode.InlineEditorError,
      'textRangeToDomPoint should be called with editor root element'
    );
  }

  if (!rootElement.contains(text)) return null;

  const texts = getTextNodesFromElement(rootElement);
  if (texts.length === 0) return null;

  const goalIndex = texts.indexOf(text);
  let index = 0;
  for (const text of texts.slice(0, goalIndex)) {
    index += calculateTextLength(text);
  }

  if (text.wholeText !== ZERO_WIDTH_SPACE) {
    index += offset;
  }

  const textParentElement = text.parentElement;
  if (!textParentElement) {
    throw new BlockSuiteError(
      ErrorCode.InlineEditorError,
      'text element parent not found'
    );
  }

  const lineElement = textParentElement.closest('v-line');

  if (!lineElement) {
    throw new BlockSuiteError(
      ErrorCode.InlineEditorError,
      'line element not found'
    );
  }

  const lineIndex = Array.from(rootElement.querySelectorAll('v-line')).indexOf(
    lineElement
  );

  return { text, index: index + lineIndex };
}

function getVNodesFromNode(node: Node): VElement[] | VLine[] | null {
  const vLine = node.parentElement?.closest('v-line');

  if (vLine) {
    return Array.from(vLine.querySelectorAll('v-element'));
  }

  const container =
    node instanceof Element
      ? node.closest(`[${INLINE_ROOT_ATTR}]`)
      : node.parentElement?.closest(`[${INLINE_ROOT_ATTR}]`);

  if (container) {
    return Array.from(container.querySelectorAll('v-line'));
  }

  return null;
}

function getTextPointFromVNodes(
  vNodes: VLine[] | VElement[],
  node: Node,
  offset: number
): TextPoint | null {
  const first = vNodes[0];
  for (let i = 0; i < vNodes.length; i++) {
    const vNode = vNodes[i];

    if (i === 0 && AFollowedByB(node, vNode)) {
      return getTextPointRoughlyFromElementByOffset(first, offset, true);
    }

    if (AInsideB(node, vNode)) {
      return getTextPointRoughlyFromElementByOffset(first, offset, false);
    }

    if (i === vNodes.length - 1 && APrecededByB(node, vNode)) {
      return getTextPointRoughlyFromElement(vNode);
    }

    if (
      i < vNodes.length - 1 &&
      APrecededByB(node, vNode) &&
      AFollowedByB(node, vNodes[i + 1])
    ) {
      return getTextPointRoughlyFromElement(vNode);
    }
  }

  return null;
}

function getTextPointRoughlyFromElement(element: Element): TextPoint | null {
  const texts = getTextNodesFromElement(element);
  if (texts.length === 0) return null;
  const text = texts[texts.length - 1];
  return [text, calculateTextLength(text)];
}

function getTextPointRoughlyFromElementByOffset(
  element: Element,
  offset: number,
  fromStart: boolean
): TextPoint | null {
  const texts = getTextNodesFromElement(element);
  if (texts.length === 0) return null;
  const text = fromStart ? texts[0] : texts[texts.length - 1];
  return [text, offset === 0 ? offset : text.length];
}

function AInsideB(a: Node, b: Node): boolean {
  return (
    b.compareDocumentPosition(a) === Node.DOCUMENT_POSITION_CONTAINED_BY ||
    b.compareDocumentPosition(a) ===
      (Node.DOCUMENT_POSITION_CONTAINED_BY | Node.DOCUMENT_POSITION_FOLLOWING)
  );
}

function AFollowedByB(a: Node, b: Node): boolean {
  return a.compareDocumentPosition(b) === Node.DOCUMENT_POSITION_FOLLOWING;
}

function APrecededByB(a: Node, b: Node): boolean {
  return a.compareDocumentPosition(b) === Node.DOCUMENT_POSITION_PRECEDING;
}
