"use client";

import { useSyncExternalStore } from "react";

/*
 * O trecho escolhido dentro do texto em escrita (como no Canva: selecionar
 * "50%" e só ele ficar em negrito, em outra cor ou maior).
 *
 * O campo em escrita (TextoEmEscrita, no palco) se registra aqui. A seleção
 * de dentro dele fica guardada: clicar num botão da barra ou numa cor do
 * painel tira o foco do campo, mas o trecho volta antes de aplicar. Os
 * comandos são os do próprio navegador (execCommand com CSS); o HTML que
 * sai passa pelo limpador do documento quando o campo avisa que mudou.
 */

let campo: HTMLElement | null = null;
let faixa: Range | null = null;
let escolhido = false;
const ouvintes = new Set<() => void>();
let ouvindo = false;

function avisar() {
  const agora = Boolean(campo && faixa && !faixa.collapsed && campo.isConnected);
  if (agora === escolhido) return;
  escolhido = agora;
  ouvintes.forEach((ouvinte) => ouvinte());
}

function aoMudarSelecao() {
  if (!campo) return;
  const selecao = window.getSelection();
  if (!selecao || !selecao.rangeCount) return;
  const r = selecao.getRangeAt(0);
  /* Seleção fora do campo (clique num botão) não apaga o trecho guardado. */
  if (!campo.contains(r.commonAncestorContainer)) return;
  faixa = r.cloneRange();
  avisar();
}

export function registrarCampo(el: HTMLElement | null) {
  campo = el;
  faixa = null;
  if (!ouvindo && typeof document !== "undefined") {
    document.addEventListener("selectionchange", aoMudarSelecao);
    ouvindo = true;
  }
  avisar();
}

export function trechoEscolhido() {
  return escolhido;
}

/** Se há um trecho escolhido no texto em escrita (redesenha quem usa quando muda). */
export function useTrechoEscolhido() {
  return useSyncExternalStore(
    (ouvinte) => {
      ouvintes.add(ouvinte);
      return () => ouvintes.delete(ouvinte);
    },
    () => escolhido,
    () => false,
  );
}

function devolverSelecao() {
  if (!campo || !faixa) return false;
  campo.focus({ preventScroll: true });
  const selecao = window.getSelection();
  if (!selecao) return false;
  selecao.removeAllRanges();
  selecao.addRange(faixa);
  return true;
}

/**
 * Põe um caractere (o painel de glifos) onde está o cursor do texto em
 * escrita, ou no lugar da letra escolhida; `html` é a letra com floreio (um
 * trecho com o recurso da fonte). Devolve false quando nenhum texto está em
 * escrita (aí quem chama põe no fim). Com o texto inteiro escolhido (logo
 * que a escrita abre), entra no fim em vez de trocar tudo.
 */
export function inserirNoTexto(texto: string, html?: string): boolean {
  if (!campo || !campo.isConnected || !faixa) return false;
  const tudo = document.createRange();
  tudo.selectNodeContents(campo);
  /* Uma letra escolhida no texto (a pessoa marcou o "M"): a de floreio entra no lugar dela. O texto inteiro escolhido, não: aí entra no fim. */
  const umaLetra = !faixa.collapsed && [...faixa.toString()].length === 1;
  if (!umaLetra && faixa.compareBoundaryPoints(Range.START_TO_START, tudo) <= 0 && faixa.compareBoundaryPoints(Range.END_TO_END, tudo) >= 0) faixa.collapse(false);
  if (!devolverSelecao()) return false;
  if (html) document.execCommand("insertHTML", false, html);
  else document.execCommand("insertText", false, texto);
  const selecao = window.getSelection();
  if (selecao?.rangeCount && campo.contains(selecao.getRangeAt(0).commonAncestorContainer)) faixa = selecao.getRangeAt(0).cloneRange();
  return true;
}

/** O tamanho da letra (px do banner) no começo do trecho. */
export function tamanhoDoTrecho(): number | null {
  if (!faixa) return null;
  const no = faixa.startContainer.nodeType === Node.TEXT_NODE ? faixa.startContainer.parentElement : (faixa.startContainer as HTMLElement);
  if (!no) return null;
  const valor = parseFloat(window.getComputedStyle(no).fontSize);
  return Number.isFinite(valor) ? valor : null;
}

export type AcaoNoTrecho = "negrito" | "italico" | "sublinhado" | "riscado" | "cor" | "tamanho" | "fonte";

const COMANDOS: Record<Exclude<AcaoNoTrecho, "cor" | "tamanho" | "fonte">, string> = { negrito: "bold", italico: "italic", sublinhado: "underline", riscado: "strikeThrough" };

/** Aplica a ação só no trecho escolhido. Devolve false se não havia trecho (aí vale a peça inteira). */
export function formatarTrecho(acao: AcaoNoTrecho, valor?: string | number): boolean {
  if (!trechoEscolhido() || !devolverSelecao() || !campo) return false;
  document.execCommand("styleWithCSS", false, "true");
  if (acao === "cor") document.execCommand("foreColor", false, String(valor));
  else if (acao === "fonte") document.execCommand("fontName", false, String(valor));
  else if (acao === "tamanho") {
    /* O navegador só conhece os tamanhos 1 a 7: marca com o 7 e troca pelo tamanho em px. */
    document.execCommand("fontSize", false, "7");
    const px = `${Math.max(4, Math.min(1200, Math.round(Number(valor) * 10) / 10))}px`;
    campo.querySelectorAll<HTMLElement>('font[size="7"]').forEach((el) => {
      const novo = document.createElement("span");
      novo.style.fontSize = px;
      while (el.firstChild) novo.appendChild(el.firstChild);
      el.replaceWith(novo);
    });
    campo.querySelectorAll<HTMLElement>("span").forEach((el) => {
      if (el.style.fontSize === "xxx-large" || el.style.fontSize === "-webkit-xxx-large") el.style.fontSize = px;
    });
  } else document.execCommand(COMANDOS[acao], false);
  const selecao = window.getSelection();
  if (selecao?.rangeCount && campo.contains(selecao.getRangeAt(0).commonAncestorContainer)) faixa = selecao.getRangeAt(0).cloneRange();
  campo.dispatchEvent(new Event("input", { bubbles: true }));
  return true;
}
