/*
 * A silhueta da peça em curvas, calculada no navegador (nov/2026).
 *
 * 1. O desenho da peça (o mesmo HTML do palco: a letra com a fonte, a foto
 *    recortada, a forma) vai para uma imagem SVG com foreignObject, numa
 *    escala grande, com as fontes e as imagens embutidas (data:), porque a
 *    imagem não enxerga as fontes da página. É o mesmo motor de letra do
 *    palco: a mesma quebra de linha, os mesmos recursos de letra, o mesmo
 *    espaçamento.
 * 2. A imagem vai para uma tela (canvas) e a borda é achada no alfa, com
 *    precisão de fração de pixel (quadrados que marcham, com interpolação).
 * 3. Cada contorno vira um caminho fino (o polígono da borda enxugado, com
 *    erro de ~0,08 px da arte) e volta para a caixa da peça.
 *
 * O resultado (silhuetas-prontas.ts) vira o traço SVG em volta da peça: o
 * contorno arredondado, o duplo, o "por fora" e o de adesivo, vetor de
 * verdade. `contornoEmCurvas` faz o contorno separado: o desenho engordado
 * (com os cantos pedidos, só o anel ou cheio, com ou sem furos) em curvas,
 * para virar um desenho fechado (traço) sem nenhuma ação de texto.
 */
import { cssDasFontesDaMarca, fonteDaChave, linkDasFontes, type PontoDoTraco } from "./documento";
import { registrarCalculoDaSilhueta, type PedidoDeSilhueta, type Silhueta } from "./silhuetas-prontas";

const LIMIAR = 128;
/* O lado maior da tela do cálculo (px): ~0,05 px de precisão numa arte de 1080. */
const LADO_DA_TELA = 2800;

/* ---------------------------------------------------- os arquivos embutidos */

const dados = new Map<string, Promise<string>>();
/** O arquivo como data: (uma vez por endereço). */
function dadosDe(url: string): Promise<string> {
  let p = dados.get(url);
  if (!p) {
    p = fetch(url, { mode: "cors" })
      .then((r) => (r.ok ? r.blob() : Promise.reject(new Error(String(r.status)))))
      .then(
        (b) =>
          new Promise<string>((ok, erro) => {
            const leitor = new FileReader();
            leitor.onload = () => ok(String(leitor.result));
            leitor.onerror = () => erro(leitor.error);
            leitor.readAsDataURL(b);
          }),
      );
    p.catch(() => dados.delete(url));
    dados.set(url, p);
  }
  return p;
}

const folhas = new Map<string, Promise<string>>();
function folhaDe(url: string) {
  let p = folhas.get(url);
  if (!p) {
    p = fetch(url).then((r) => (r.ok ? r.text() : Promise.reject(new Error(String(r.status)))));
    p.catch(() => folhas.delete(url));
    folhas.set(url, p);
  }
  return p;
}

/** Troca os endereços (url(...), src, href) do texto pelo arquivo embutido. */
async function embutir(texto: string, achar: RegExp, endereco: (m: RegExpMatchArray) => string) {
  const achados = [...texto.matchAll(achar)];
  const enderecos = [...new Set(achados.map(endereco))].filter((u) => /^https?:\/\//.test(u));
  const prontos = new Map(await Promise.all(enderecos.map(async (u) => [u, await dadosDe(u)] as const)));
  return texto.replace(achar, (...args) => {
    const m = args.slice(0, -2) as unknown as RegExpMatchArray;
    const u = endereco(m);
    return prontos.has(u) ? m[0].replace(u, prontos.get(u)!) : m[0];
  });
}

/* As faixas de caracteres de uma face ("U+0000-00FF, U+0131, U+2000-206F"). */
function faixas(valor: string): Array<[number, number]> {
  return valor
    .split(",")
    .map((s) => s.trim().replace(/^U\+/i, ""))
    .filter(Boolean)
    .map((s) => {
      if (s.includes("?")) return [parseInt(s.replace(/\?/g, "0"), 16), parseInt(s.replace(/\?/g, "F"), 16)] as [number, number];
      const [a, b] = s.split("-");
      return [parseInt(a, 16), parseInt(b ?? a, 16)] as [number, number];
    });
}

/** O @font-face só das faces que o desenho usa (peso, itálico e caracteres), com o arquivo embutido. */
async function cssDasFontes(chaves: string[], html: string, texto: string): Promise<string> {
  const pesos = new Set([...html.matchAll(/font-weight:\s*(\d{3})/g)].map((m) => Number(m[1])));
  if (/<(b|strong)[\s>]/i.test(html)) pesos.add(700);
  const italico = /font-style:\s*italic|<(i|em)[\s>]/i.test(html);
  const letras = new Set<number>();
  for (const c of texto + texto.toUpperCase() + texto.toLowerCase()) letras.add(c.codePointAt(0)!);
  const google = [...new Set(chaves.map((c) => fonteDaChave(c)).filter((f) => !f.propria).map((f) => f.chave))];
  const partes: string[] = [];
  if (google.length) {
    const css = await folhaDe(linkDasFontes(google));
    for (const bloco of css.match(/@font-face\s*\{[^}]*\}/g) || []) {
      const estilo = /font-style:\s*italic/.test(bloco) ? "italic" : "normal";
      if (estilo === "italic" && !italico) continue;
      const peso = bloco.match(/font-weight:\s*(\d+)(?:\s+(\d+))?/);
      if (peso && pesos.size) {
        const [de, ate] = [Number(peso[1]), Number(peso[2] ?? peso[1])];
        if (![...pesos].some((p) => p >= de && p <= ate)) continue;
      }
      const faixa = bloco.match(/unicode-range:\s*([^;]+);/);
      if (faixa) {
        const lista = faixas(faixa[1]);
        if (![...letras].some((c) => lista.some(([a, b]) => c >= a && c <= b))) continue;
      }
      partes.push(bloco);
    }
  }
  partes.push(cssDasFontesDaMarca(chaves));
  return embutir(partes.join(""), /url\((["']?)([^"')]+)\1\)/g, (m) => m[2]);
}

/** HTML virando XHTML (o foreignObject é XML: <br> precisa fechar). */
function emXhtml(html: string) {
  const doc = document.implementation.createHTMLDocument("");
  doc.body.innerHTML = html;
  const s = new XMLSerializer();
  return { xhtml: [...doc.body.childNodes].map((n) => s.serializeToString(n)).join(""), texto: doc.body.textContent || "" };
}

/* ---------------------------------------------------- o desenho na tela */

type Tela = { alfa: Uint8ClampedArray; W: number; H: number; escala: number; folga: number };

/** A medida da tela do cálculo para a caixa (a folga pega o que passa da caixa). */
function medidaDaTela(w: number, h: number, folgaExtra = 0) {
  const folga = Math.ceil(Math.max(w, h) * 0.12 + 16 + folgaExtra);
  const escala = Math.max(1, Math.min(4, LADO_DA_TELA / (Math.max(w, h) + 2 * folga)));
  return { folga, escala, W: Math.ceil((w + 2 * folga) * escala), H: Math.ceil((h + 2 * folga) * escala) };
}

async function desenharNaTela(pedido: PedidoDeSilhueta): Promise<Tela> {
  const { xhtml, texto } = emXhtml(pedido.html);
  const corpo = await embutir(xhtml, /(?:url\((["']?)([^"')]+)\1\)|(?:src|href)="([^"]+)")/g, (m) => (m[2] ?? m[3] ?? "").replace(/&amp;/g, "&"));
  const css = await cssDasFontes(pedido.fontes, pedido.html, texto);
  const { folga, escala, W, H } = medidaDaTela(pedido.w, pedido.h);
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}"><foreignObject x="0" y="0" width="${W}" height="${H}"><div xmlns="http://www.w3.org/1999/xhtml" style="position:absolute;left:0;top:0;width:${pedido.w}px;height:${pedido.h}px;transform-origin:0 0;transform:translate(${folga * escala}px,${folga * escala}px) scale(${escala})"><style>${css.replace(/&/g, "&amp;").replace(/</g, "&lt;")}</style>${corpo}</div></foreignObject></svg>`;
  /* Em data: (o endereço blob: deixa a tela "contaminada" e o alfa não pode ser lido). */
  const endereco = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
  {
    const img = new Image();
    img.src = endereco;
    await img.decode();
    const tela = document.createElement("canvas");
    tela.width = W;
    tela.height = H;
    const ctx = tela.getContext("2d", { willReadFrequently: true })!;
    /*
     * A fonte embutida carrega DEPOIS do primeiro desenho da imagem (o
     * primeiro sai com a letra de reserva, com outro desenho e outra quebra):
     * desenha de novo até duas leituras seguidas darem iguais.
     */
    let alfa: Uint8ClampedArray = new Uint8ClampedArray(0);
    let antes = -1;
    for (let volta = 0; volta < 14; volta++) {
      ctx.clearRect(0, 0, W, H);
      ctx.drawImage(img, 0, 0);
      alfa = ctx.getImageData(0, 0, W, H).data;
      let soma = 0;
      for (let i = 3; i < alfa.length; i += 4 * 7) soma = (soma + alfa[i] * ((i >> 2) % 251)) % 2147483647;
      if (soma === antes && volta >= 2) break;
      antes = soma;
      await new Promise((r) => setTimeout(r, volta < 3 ? 40 : 90));
    }
    return { alfa, W, H, escala, folga };
  }
}

/* ---------------------------------------------------- a borda no alfa */

/*
 * Os quadrados que marcham: cada célula de 2×2 amostras diz por onde a borda
 * passa (os cantos dentro/fora), com o ponto interpolado no alfa de cada
 * aresta. Os segmentos já saem com o dentro sempre do mesmo lado, então os
 * contornos de fora e os furos saem em sentidos opostos.
 */
const T = 0;
const R = 1;
const B = 2;
const L = 3;
const MEIO: Record<number, [number, number]> = { [T]: [0.5, 0], [R]: [1, 0.5], [B]: [0.5, 1], [L]: [0, 0.5] };
const CANTOS: Array<[number, number, number]> = [
  [0, 0, 8],
  [1, 0, 4],
  [1, 1, 2],
  [0, 1, 1],
];
/* Os segmentos de cada caso (o 5 e o 10, ambíguos, têm as duas leituras: centro dentro / centro fora). */
const CASOS: Record<number, number[][]> = {
  1: [[L, B]],
  2: [[B, R]],
  3: [[L, R]],
  4: [[T, R]],
  6: [[T, B]],
  7: [[L, T]],
  8: [[L, T]],
  9: [[T, B]],
  11: [[T, R]],
  12: [[L, R]],
  13: [[B, R]],
  14: [[L, B]],
};
const SELA: Record<number, { dentro: number[][]; fora: number[][] }> = {
  5: { dentro: [[L, T], [B, R]], fora: [[T, R], [L, B]] },
  10: { dentro: [[T, R], [L, B]], fora: [[L, T], [B, R]] },
};
/* Cada segmento com o sentido que deixa o dentro à esquerda (y para baixo). */
function orientar(caso: number, seg: number[]): [number, number] {
  const [a, b] = seg;
  const p = MEIO[a];
  const q = MEIO[b];
  const lado = (c: [number, number, number]) => (q[0] - p[0]) * (c[1] - p[1]) - (q[1] - p[1]) * (c[0] - p[0]);
  const esquerda = CANTOS.filter((c) => lado(c) < 0);
  const direita = CANTOS.filter((c) => lado(c) > 0);
  const [menor, naEsquerda] = esquerda.length <= direita.length ? [esquerda, true] : [direita, false];
  const dentro = Boolean(caso & menor[0][2]);
  return dentro === naEsquerda ? [a, b] : [b, a];
}
const TABELA: Record<string, Array<[number, number]>> = {};
for (const [caso, segs] of Object.entries(CASOS)) TABELA[caso] = segs.map((s) => orientar(Number(caso), s));
/* Na sela, cada segmento corta um canto só: a mesma regra vale. */
for (const [caso, s] of Object.entries(SELA)) {
  TABELA[`${caso}d`] = s.dentro.map((g) => orientar(Number(caso), g));
  TABELA[`${caso}f`] = s.fora.map((g) => orientar(Number(caso), g));
}

/** Os contornos fechados do alfa (em px da tela, no centro das amostras). */
function contornosDoAlfa(alfa: Uint8ClampedArray, W: number, H: number): Array<Array<[number, number]>> {
  const v = (x: number, y: number) => (x < 0 || y < 0 || x >= W || y >= H ? 0 : alfa[(y * W + x) * 4 + 3]);
  const largura = W + 2;
  /* A aresta como número: horizontal (linha y, coluna x) ou vertical. */
  const arestaH = (x: number, y: number) => ((y + 1) * largura + (x + 1)) * 2;
  const arestaV = (x: number, y: number) => ((y + 1) * largura + (x + 1)) * 2 + 1;
  const proximo = new Map<number, number>();
  const ponto = new Map<number, [number, number]>();
  const onde = (x: number, y: number, a: number, va: number, vb: number, vc: number, vd: number): [number, [number, number]] => {
    /* va..vd: tl, tr, br, bl */
    const t = (p: number, q: number) => (p === q ? 0.5 : Math.max(0, Math.min(1, (LIMIAR - p) / (q - p))));
    if (a === T) return [arestaH(x, y), [x + t(va, vb), y]];
    if (a === B) return [arestaH(x, y + 1), [x + t(vd, vc), y + 1]];
    if (a === L) return [arestaV(x, y), [x, y + t(va, vd)]];
    return [arestaV(x + 1, y), [x + 1, y + t(vb, vc)]];
  };
  for (let y = -1; y < H; y++) {
    for (let x = -1; x < W; x++) {
      const va = v(x, y);
      const vb = v(x + 1, y);
      const vc = v(x + 1, y + 1);
      const vd = v(x, y + 1);
      const caso = (va >= LIMIAR ? 8 : 0) | (vb >= LIMIAR ? 4 : 0) | (vc >= LIMIAR ? 2 : 0) | (vd >= LIMIAR ? 1 : 0);
      if (caso === 0 || caso === 15) continue;
      const segs = caso === 5 || caso === 10 ? TABELA[`${caso}${(va + vb + vc + vd) / 4 >= LIMIAR ? "d" : "f"}`] : TABELA[caso];
      for (const [a, b] of segs) {
        const [ea, pa] = onde(x, y, a, va, vb, vc, vd);
        const [eb, pb] = onde(x, y, b, va, vb, vc, vd);
        proximo.set(ea, eb);
        ponto.set(ea, pa);
        ponto.set(eb, pb);
      }
    }
  }
  const laços: Array<Array<[number, number]>> = [];
  const visto = new Set<number>();
  for (const inicio of proximo.keys()) {
    if (visto.has(inicio)) continue;
    const laco: Array<[number, number]> = [];
    let e: number | undefined = inicio;
    while (e !== undefined && !visto.has(e)) {
      visto.add(e);
      const [px, py] = ponto.get(e)!;
      laco.push([px + 0.5, py + 0.5]);
      e = proximo.get(e);
    }
    if (laco.length >= 4) laços.push(laco);
  }
  return laços;
}

const areaDe = (laco: Array<[number, number]>) => {
  let s = 0;
  for (let i = 0, j = laco.length - 1; i < laco.length; j = i++) s += (laco[j][0] - laco[i][0]) * (laco[j][1] + laco[i][1]);
  return s / 2;
};

/**
 * Tira os pontos quase em linha (Douglas-Peucker, pela distância ao
 * SEGMENTO). O laço é fechado: ele é cortado no ponto mais longe do
 * primeiro e cada metade é enxugada (o primeiro e o último são vizinhos; a
 * reta entre eles não diz nada).
 */
function enxugar(laco: Array<[number, number]>, tol: number): Array<[number, number]> {
  const n = laco.length;
  if (n < 8) return laco;
  let longe = 0;
  let maior = -1;
  for (let i = 1; i < n; i++) {
    const d = (laco[i][0] - laco[0][0]) ** 2 + (laco[i][1] - laco[0][1]) ** 2;
    if (d > maior) {
      maior = d;
      longe = i;
    }
  }
  const manter = new Uint8Array(n + 1);
  const ponto = (i: number) => laco[i % n];
  const pilha: Array<[number, number]> = [
    [0, longe],
    [longe, n],
  ];
  manter[0] = manter[longe] = manter[n] = 1;
  while (pilha.length) {
    const [a, b] = pilha.pop()!;
    if (b - a < 2) continue;
    const [ax, ay] = ponto(a);
    const [bx, by] = ponto(b);
    const dx = bx - ax;
    const dy = by - ay;
    const n2 = dx * dx + dy * dy;
    let pior = -1;
    let dist = tol * tol;
    for (let i = a + 1; i < b; i++) {
      const [x, y] = ponto(i);
      const t = n2 > 0 ? Math.max(0, Math.min(1, ((x - ax) * dx + (y - ay) * dy) / n2)) : 0;
      const qx = ax + t * dx - x;
      const qy = ay + t * dy - y;
      const d = qx * qx + qy * qy;
      if (d > dist) {
        dist = d;
        pior = i;
      }
    }
    if (pior >= 0) {
      manter[pior] = 1;
      pilha.push([a, pior], [pior, b]);
    }
  }
  const saida: Array<[number, number]> = [];
  for (let i = 0; i < n; i++) if (manter[i]) saida.push(laco[i]);
  return saida;
}

type Curva = { pontos: PontoDoTraco[]; d: string; area: number };

/**
 * Os contornos virando caminho (na caixa da peça): o polígono fino da borda,
 * enxugado com erro de até ~0,3 px da tela (~0,08 px da arte), exato e sem
 * risco de laço torcido (o simplify do paper às vezes cruzava a curva sobre
 * ela mesma nos furos das letras e abria lascas). O traço em volta, com a
 * junta redonda, sai liso do mesmo jeito.
 */
async function emCurvas(laços: Array<Array<[number, number]>>, escala: number, folga: number): Promise<Curva[]> {
  const saida: Curva[] = [];
  const minimo = Math.max(2, (0.6 * escala) ** 2);
  const c = (v: number) => Math.round((v / escala - folga) * 100) / 100;
  for (const bruto of laços) {
    const area = areaDe(bruto);
    if (Math.abs(area) < minimo) continue;
    const laco = enxugar(bruto, 0.3);
    const pontos: PontoDoTraco[] = laco.map(([x, y]) => ({ x: c(x), y: c(y) }));
    const d = `M${pontos.map((q) => `${q.x} ${q.y}`).join("L")}Z`;
    saida.push({ pontos, d, area });
  }
  return saida;
}

/* O sinal dos contornos de fora: o do maior (o maior é sempre de fora). */
function sinalDeFora(curvas: Curva[]) {
  let maior = curvas[0];
  for (const c of curvas) if (Math.abs(c.area) > Math.abs(maior.area)) maior = c;
  return Math.sign(maior.area) || 1;
}

/* ---------------------------------------------------- o cálculo */

export async function calcularSilhueta(pedido: PedidoDeSilhueta): Promise<Silhueta | null> {
  const tela = await desenharNaTela(pedido);
  const curvas = await emCurvas(contornosDoAlfa(tela.alfa, tela.W, tela.H), tela.escala, tela.folga);
  if (!curvas.length) return null;
  const fora = sinalDeFora(curvas);
  return { d: curvas.map((c) => c.d).join(""), dSemFuros: curvas.filter((c) => Math.sign(c.area) === fora).map((c) => c.d).join("") };
}

export type OpcoesDoContornoEmCurvas = { raio: number; cantos: "redondos" | "vivos"; anel: boolean; semBuracos: boolean; w: number; h: number };

/**
 * O contorno separado em curvas: a silhueta engordada (o traço com a junta
 * pedida, redonda ou em esquadria) desenhada numa tela grande, a borda achada
 * e virada curva. "Só a borda" tira a própria peça do miolo; "sem furos"
 * deixa só os contornos de fora. Em px da caixa da peça (podem passar dela).
 */
export async function contornoEmCurvas(s: Silhueta, o: OpcoesDoContornoEmCurvas): Promise<PontoDoTraco[][]> {
  const { folga, escala, W, H } = medidaDaTela(o.w, o.h, o.raio);
  const tela = document.createElement("canvas");
  tela.width = W;
  tela.height = H;
  const ctx = tela.getContext("2d", { willReadFrequently: true })!;
  ctx.setTransform(escala, 0, 0, escala, folga * escala, folga * escala);
  const forma = new Path2D(o.semBuracos ? s.dSemFuros : s.d);
  ctx.fillStyle = "#000";
  ctx.strokeStyle = "#000";
  ctx.fill(forma);
  if (o.raio > 0) {
    ctx.lineWidth = o.raio * 2;
    ctx.lineJoin = o.cantos === "vivos" ? "miter" : "round";
    ctx.miterLimit = 4;
    ctx.stroke(forma);
  }
  if (o.anel) {
    ctx.globalCompositeOperation = "destination-out";
    ctx.fill(new Path2D(s.d));
  }
  const alfa = ctx.getImageData(0, 0, W, H).data;
  let curvas = await emCurvas(contornosDoAlfa(alfa, W, H), escala, folga);
  if (!curvas.length) return [];
  /* "Sem furos" no anel: o anel continua com o buraco da peça (é o anel); os furos pequenos que sobram, não. */
  if (o.semBuracos && !o.anel) {
    const fora = sinalDeFora(curvas);
    curvas = curvas.filter((c) => Math.sign(c.area) === fora);
  }
  return [...curvas].sort((a, b) => Math.abs(b.area) - Math.abs(a.area)).map((c) => c.pontos);
}

if (typeof window !== "undefined") registrarCalculoDaSilhueta(calcularSilhueta);
