export function setForeColor(editor: HTMLElement, hex: string) {
  applyInlineStyle(editor, 'color', hex, 'data-rte-color');
}
export function setBackColor(editor: HTMLElement, hex: string) {
  applyInlineStyle(editor, 'backgroundColor', hex, 'data-rte-bg');
}

function enclosingStyledSpan(
  editor: HTMLElement,
  node: Node | null,
  dataAttr: 'data-rte-color' | 'data-rte-bg'
): HTMLSpanElement | null {
  while (node && node !== editor) {
    if (node instanceof HTMLSpanElement && node.hasAttribute(dataAttr)) {
      return node;
    }
    node = node.parentNode;
  }
  return null;
}

function applyInlineStyle(
  editor: HTMLElement,
  cssProp: 'color' | 'backgroundColor',
  value: string,
  dataAttr: 'data-rte-color' | 'data-rte-bg'
) {
  const sel = window.getSelection();
  if (!sel || sel.rangeCount === 0) return;
  const r0 = sel.getRangeAt(0);
  if (!editor.contains(r0.commonAncestorContainer)) return;

  const range = r0.cloneRange();

  if (range.collapsed) {
    const enclosing = enclosingStyledSpan(
      editor,
      range.startContainer,
      dataAttr
    );
    if (enclosing) {
      (enclosing.style as any)[cssProp] = value;
      mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
      return;
    }

    const s = document.createElement('span');
    s.setAttribute(dataAttr, '1');
    (s.style as any)[cssProp] = value;
    s.appendChild(document.createTextNode('\u200B'));
    range.insertNode(s);

    const caret = document.createRange();
    caret.setStart(s.firstChild!, 1);
    caret.collapse(true);
    sel.removeAllRanges();
    sel.addRange(caret);

    mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
    return;
  }

  const leftEdge = enclosingStyledSpan(editor, range.startContainer, dataAttr);
  const rightEdge = enclosingStyledSpan(editor, range.endContainer, dataAttr);
  if (leftEdge && leftEdge === rightEdge) {
    if (rangeCoversWholeNode(range, leftEdge)) {
      (leftEdge.style as any)[cssProp] = value;
    } else {
      const mid = splitAndRecolorWithinSpan(
        leftEdge,
        range,
        dataAttr,
        cssProp,
        value
      );

      const sel = window.getSelection();
      const r = document.createRange();
      r.selectNodeContents(mid);
      sel?.removeAllRanges();
      sel?.addRange(r);
    }
    mergeAdjacentStyledSpans(editor, dataAttr, cssProp);
    return;
  }

  const commonEl = (() => {
    let n: Node | null = range.commonAncestorContainer;
    while (n && !(n instanceof HTMLElement)) n = n.parentNode;
    return n as HTMLElement | null;
  })();

  const walker = document.createTreeWalker(
    commonEl || editor,
    NodeFilter.SHOW_TEXT,
    {
      acceptNode: n => {
        if (!n.nodeValue || !n.nodeValue.trim())
          return NodeFilter.FILTER_REJECT;
        const nodeRange = document.createRange();
        nodeRange.selectNodeContents(n);
        const intersects =
          range.compareBoundaryPoints(Range.END_TO_START, nodeRange) < 0 &&
          range.compareBoundaryPoints(Range.START_TO_END, nodeRange) > 0;
        return intersects ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
      },
    }
  );

  const toProcess: Text[] = [];
  let t: Node | null;
  while ((t = walker.nextNode())) toProcess.push(t as Text);

  toProcess.forEach(text => {
    let start = 0,
      end = text.length;
    if (text === range.startContainer) start = range.startOffset;
    if (text === range.endContainer) end = range.endOffset;
    if (start > 0) text = text.splitText(start);
    if (end < text.length) text.splitText(end);

    const existing = enclosingStyledSpan(editor, text, dataAttr);
    if (existing) {
      (existing.style as any)[cssProp] = value;
      return;
    }

    const span = document.createElement('span');
    span.setAttribute(dataAttr, '1');
    (span.style as any)[cssProp] = value;
    const parent = text.parentElement!;
    parent.replaceChild(span, text);
    span.appendChild(text);
  });

  mergeAdjacentStyledSpans(editor, dataAttr, cssProp);

  // restore selection
  sel.removeAllRanges();
  sel.addRange(range);
}

// Is the range covering the entire node's contents?
function rangeCoversWholeNode(range: Range, node: Node): boolean {
  const all = document.createRange();
  all.selectNodeContents(node);
  return (
    range.compareBoundaryPoints(Range.START_TO_START, all) <= 0 &&
    range.compareBoundaryPoints(Range.END_TO_END, all) >= 0
  );
}

// Split one styled span into [left][middle][right]; recolor only middle
function splitAndRecolorWithinSpan(
  span: HTMLSpanElement,
  range: Range,
  dataAttr: 'data-rte-color' | 'data-rte-bg',
  cssProp: 'color' | 'backgroundColor',
  newValue: string
): HTMLSpanElement {
  const oldValue = (span.style as any)[cssProp];

  const left = document.createRange();
  left.setStart(span, 0);
  left.setEnd(range.startContainer, range.startOffset);

  const right = document.createRange();
  right.setStart(range.endContainer, range.endOffset);
  right.setEnd(span, span.childNodes.length);

  // Build replacement fragment
  const frag = document.createDocumentFragment();

  // helper to make a styled clone shell
  const makeShell = (val: string) => {
    const s = document.createElement('span');
    s.setAttribute(dataAttr, '1');
    (s.style as any)[cssProp] = val;
    return s;
  };

  if (hasRangeContent(left)) {
    const sLeft = makeShell(oldValue);
    sLeft.appendChild(left.cloneContents());
    frag.appendChild(sLeft);
  }

  const mid = makeShell(newValue);
  mid.appendChild(range.cloneContents());
  frag.appendChild(mid);

  if (hasRangeContent(right)) {
    const sRight = makeShell(oldValue);
    sRight.appendChild(right.cloneContents());
    frag.appendChild(sRight);
  }

  span.replaceWith(frag);
  return mid;
}

function mergeAdjacentStyledSpans(
  root: HTMLElement,
  dataAttr: 'data-rte-color' | 'data-rte-bg',
  cssProp: 'color' | 'backgroundColor'
) {
  const spans = Array.from(
    root.querySelectorAll<HTMLSpanElement>(`span[${dataAttr}]`)
  );

  const valOf = (el: HTMLElement) => (el.style as any)[cssProp];

  spans.forEach(s => {
    const nested = Array.from(
      s.querySelectorAll<HTMLSpanElement>(`span[${dataAttr}]`)
    );
    nested.forEach(child => {
      if (valOf(child) === valOf(s)) {
        while (child.firstChild) s.insertBefore(child.firstChild, child);
        child.remove();
      }
    });

    const prev = s.previousSibling;
    if (
      prev instanceof HTMLSpanElement &&
      prev.hasAttribute(dataAttr) &&
      valOf(prev) === valOf(s)
    ) {
      while (s.firstChild) prev.appendChild(s.firstChild);
      s.remove();
      return;
    }

    const next = s.nextSibling;
    if (
      next instanceof HTMLSpanElement &&
      next.hasAttribute(dataAttr) &&
      valOf(next) === valOf(s)
    ) {
      while (next.firstChild) s.appendChild(next.firstChild);
      next.remove();
    }
  });
}

function hasRangeContent(r: Range): boolean {
  if (r.collapsed) return false;
  const text = r.cloneContents().textContent || '';
  return text.length > 0;
}
