// Helpers de gestion du caret pour les contentEditable.
// Depuis Phase 2, l'arbre interne peut contenir plusieurs Text nodes
// emballés dans des wrappers (<strong>, <em>, <a>, <span data-color>...).
// Les fonctions ci-dessous calculent / posent un offset texte global,
// en comptant les <br> comme 1 caractère (équivalent au \n du modèle InlineNode).

// Longueur « modèle » d'un sous-arbre DOM : texte + <br>=1 chacun.
function textLength(node) {
  if (!node) return 0;
  if (node.nodeType === 3) return node.nodeValue?.length ?? 0;
  if (node.nodeType !== 1) return 0;
  if (node.tagName === 'BR') return 1;
  let total = 0;
  for (const child of node.childNodes) total += textLength(child);
  return total;
}

// Compte les caractères depuis root jusqu'à (targetNode, targetOffset),
// en traitant les <br> comme 1 caractère.
function countCharsUpTo(root, targetNode, targetOffset) {
  if (root === targetNode) {
    let count = 0;
    const kids = root.childNodes;
    for (let i = 0; i < targetOffset && i < kids.length; i++) count += textLength(kids[i]);
    return count;
  }
  let count = 0;
  for (const child of root.childNodes) {
    if (child === targetNode) {
      if (child.nodeType === 3) {
        count += targetOffset;
      } else {
        const kids = child.childNodes;
        for (let i = 0; i < targetOffset && i < kids.length; i++) count += textLength(kids[i]);
      }
      return count;
    }
    if (child.nodeType === 1 && child.tagName !== 'BR' && child.contains?.(targetNode)) {
      return count + countCharsUpTo(child, targetNode, targetOffset);
    }
    count += textLength(child);
  }
  return count;
}

// Trouve le Text node + offset local correspondant à un offset global.
// Compte les <br> comme 1 caractère.
function locateOffset(el, offset) {
  if (!el) return null;
  let consumed = 0;

  function walk(node) {
    if (node.nodeType === 3 /* TEXT */) {
      const len = node.nodeValue?.length ?? 0;
      if (offset <= consumed + len) return { node, offset: offset - consumed };
      consumed += len;
      return null;
    }
    if (node.nodeType !== 1) return null;
    if (node.tagName === 'BR') {
      if (offset <= consumed) {
        // Cursor tombe juste avant ce <br> : se positionner avant dans le parent.
        const parent = node.parentNode ?? el;
        let idx = 0;
        for (const sib of parent.childNodes) {
          if (sib === node) break;
          idx++;
        }
        return { node: parent, offset: idx };
      }
      consumed += 1;
      return null;
    }
    for (const child of node.childNodes) {
      const result = walk(child);
      if (result) return result;
    }
    return null;
  }

  for (const child of el.childNodes) {
    const result = walk(child);
    if (result) return result;
  }

  // offset au-delà de la fin : pointe la fin du dernier nœud texte.
  const walker = el.ownerDocument.createTreeWalker(el, NodeFilter.SHOW_TEXT);
  let last = null;
  let node = walker.nextNode();
  while (node) { last = node; node = walker.nextNode(); }
  if (last) return { node: last, offset: last.nodeValue?.length ?? 0 };
  return { node: el, offset: 0 };
}

export function getCaretOffset(el) {
  if (!el) return 0;
  const sel = typeof window !== 'undefined' ? window.getSelection() : null;
  if (!sel || sel.rangeCount === 0) return 0;
  const range = sel.getRangeAt(0);
  if (!el.contains(range.startContainer)) return 0;
  return countCharsUpTo(el, range.startContainer, range.startOffset);
}

export function getCaretRange(el) {
  if (!el) return null;
  const sel = typeof window !== 'undefined' ? window.getSelection() : null;
  if (!sel || sel.rangeCount === 0) return null;
  const range = sel.getRangeAt(0);
  if (!el.contains(range.startContainer) || !el.contains(range.endContainer)) return null;
  return {
    start: countCharsUpTo(el, range.startContainer, range.startOffset),
    end: countCharsUpTo(el, range.endContainer, range.endOffset),
  };
}

export function setCaretOffset(el, offset) {
  if (!el) return;
  el.focus();
  const sel = window.getSelection();
  if (!sel) return;
  const target = locateOffset(el, Math.max(0, offset || 0));
  if (!target) return;
  const range = document.createRange();
  range.setStart(target.node, target.offset);
  range.collapse(true);
  sel.removeAllRanges();
  sel.addRange(range);
}

// Pose une sélection couvrant [start, end[ dans l'élément.
export function setCaretRange(el, start, end) {
  if (!el) return;
  el.focus();
  const sel = window.getSelection();
  if (!sel) return;
  const a = locateOffset(el, Math.max(0, start || 0));
  const b = locateOffset(el, Math.max(0, end || 0));
  if (!a || !b) return;
  const range = document.createRange();
  range.setStart(a.node, a.offset);
  range.setEnd(b.node, b.offset);
  sel.removeAllRanges();
  sel.addRange(range);
}

export function focusEnd(el) {
  if (!el) return;
  setCaretOffset(el, textLength(el));
}

export function isCaretAtStart(el) {
  return getCaretOffset(el) === 0;
}

export function isCaretAtEnd(el) {
  return getCaretOffset(el) === textLength(el);
}
