import { inflateSync } from "node:zlib";
import { FONTES } from "./documento";

/*
 * Lê o arquivo da fonte para o painel de glifos: os caracteres que ela tem
 * (a tabela cmap) e os recursos de letra (as etiquetas da tabela GSUB:
 * floreios, alternativas, ligaduras, estilos). TTF, OTF e WOFF; o WOFF2
 * (comprimido em Brotli, com as tabelas transformadas) fica de fora.
 *
 * As fontes do Google vêm em TTF inteiro (o Google manda TTF a quem não diz
 * ser navegador) e a lista é cortada pelo que o navegador recebe de verdade
 * (os pedaços "unicode-range" da folha de estilo para o Chrome): caractere
 * fora deles não desenha na arte.
 */

/*
 * `alternativas`: para cada caractere que tem outro desenho (a letra com
 * floreio, com perninha, inicial, final...), o jeito de chegar em cada um no
 * CSS ("'salt' 2", "'ss03' 1"): é o que vai no trecho da letra no texto.
 */
export type LeituraDaFonte = { caracteres: number[]; recursos: string[]; alternativas: Record<number, string[]> };

const marca = (v: DataView, o: number) => String.fromCharCode(v.getUint8(o), v.getUint8(o + 1), v.getUint8(o + 2), v.getUint8(o + 3));

/** As tabelas da fonte (etiqueta → bytes), descomprimindo o WOFF. */
function tabelas(bytes: Uint8Array): Map<string, DataView> {
  const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
  const assinatura = marca(v, 0);
  const saida = new Map<string, DataView>();
  if (assinatura === "wOF2") throw new Error("woff2");
  if (assinatura === "wOFF") {
    const quantas = v.getUint16(12);
    for (let i = 0; i < quantas; i++) {
      const e = 44 + i * 20;
      const [deslocamento, comprimido, original] = [v.getUint32(e + 4), v.getUint32(e + 8), v.getUint32(e + 12)];
      const pedaco = bytes.subarray(deslocamento, deslocamento + comprimido);
      const aberto = comprimido < original ? new Uint8Array(inflateSync(pedaco)) : pedaco;
      saida.set(marca(v, e), new DataView(aberto.buffer, aberto.byteOffset, aberto.byteLength));
    }
    return saida;
  }
  /* Coleção (ttcf): vale a primeira fonte. */
  const inicio = assinatura === "ttcf" ? v.getUint32(12) : 0;
  const quantas = v.getUint16(inicio + 4);
  for (let i = 0; i < quantas; i++) {
    const e = inicio + 12 + i * 16;
    const [deslocamento, comprimento] = [v.getUint32(e + 8), v.getUint32(e + 12)];
    if (deslocamento + comprimento > bytes.byteLength) continue;
    saida.set(marca(v, e), new DataView(bytes.buffer, bytes.byteOffset + deslocamento, comprimento));
  }
  return saida;
}

/** Os caracteres da cmap (formato 4 ou 12) e o desenho (glifo) de cada um; só os que têm desenho. */
function caracteresDaCmap(t: DataView): Map<number, number> {
  const registros = t.getUint16(2);
  let melhor = -1;
  let nota = -1;
  for (let i = 0; i < registros; i++) {
    const [plataforma, codificacao, deslocamento] = [t.getUint16(4 + i * 8), t.getUint16(6 + i * 8), t.getUint32(8 + i * 8)];
    if (deslocamento >= t.byteLength) continue;
    const formato = t.getUint16(deslocamento);
    const pontos = formato === 12 && (plataforma === 3 || plataforma === 0) ? 3 : formato === 4 && ((plataforma === 3 && codificacao === 1) || plataforma === 0) ? 2 : 0;
    if (pontos > nota) {
      nota = pontos;
      melhor = deslocamento;
    }
  }
  const saida = new Map<number, number>();
  if (melhor < 0 || nota <= 0) return saida;
  if (t.getUint16(melhor) === 12) {
    const grupos = Math.min(t.getUint32(melhor + 12), 20000);
    for (let g = 0; g < grupos; g++) {
      const o = melhor + 16 + g * 12;
      const [de, ate, primeiro] = [t.getUint32(o), t.getUint32(o + 4), t.getUint32(o + 8)];
      for (let c = de; c <= ate && saida.size < 70000; c++) if (primeiro + (c - de)) saida.set(c, primeiro + (c - de));
    }
  } else {
    const segmentos = t.getUint16(melhor + 6) / 2;
    const fins = melhor + 14;
    const comecos = fins + segmentos * 2 + 2;
    const deltas = comecos + segmentos * 2;
    const saltos = deltas + segmentos * 2;
    for (let s = 0; s < segmentos; s++) {
      const [fim, comeco, delta, salto] = [t.getUint16(fins + s * 2), t.getUint16(comecos + s * 2), t.getInt16(deltas + s * 2), t.getUint16(saltos + s * 2)];
      for (let c = comeco; c <= fim && c !== 0xffff; c++) {
        let glifo: number;
        if (!salto) glifo = (c + delta) & 0xffff;
        else {
          const onde = saltos + s * 2 + salto + (c - comeco) * 2;
          if (onde + 2 > t.byteLength) continue;
          const bruto = t.getUint16(onde);
          glifo = bruto ? (bruto + delta) & 0xffff : 0;
        }
        if (glifo) saida.set(c, glifo);
      }
    }
  }
  return saida;
}

/** As etiquetas da lista de recursos da GSUB (sem repetir). */
function recursosDaGsub(t: DataView | undefined): string[] {
  if (!t || t.byteLength < 10) return [];
  const lista = t.getUint16(6);
  if (!lista || lista + 2 > t.byteLength) return [];
  const quantos = t.getUint16(lista);
  const saida = new Set<string>();
  for (let i = 0; i < quantos && lista + 2 + i * 6 + 4 <= t.byteLength; i++) saida.add(marca(t, lista + 2 + i * 6));
  return [...saida].filter((tag) => /^[a-z0-9]{4}$/.test(tag)).sort();
}

/** Os desenhos de uma tabela de cobertura, na ordem dela (formato 1: lista; 2: faixas). */
function cobertura(t: DataView, o: number): number[] {
  if (o + 4 > t.byteLength) return [];
  const formato = t.getUint16(o);
  const quantos = t.getUint16(o + 2);
  const saida: number[] = [];
  if (formato === 1) for (let i = 0; i < quantos && o + 6 + i * 2 <= t.byteLength; i++) saida.push(t.getUint16(o + 4 + i * 2));
  else if (formato === 2) {
    for (let i = 0; i < quantos && o + 4 + i * 6 + 6 <= t.byteLength; i++) {
      const [de, ate] = [t.getUint16(o + 4 + i * 6), t.getUint16(o + 6 + i * 6)];
      for (let g = de; g <= ate && saida.length < 5000; g++) saida.push(g);
    }
  }
  return saida;
}

/* Os recursos que trocam uma letra por outro desenho dela, na ordem em que aparecem no painel. */
const RECURSOS_DE_DESENHO = ["swsh", "salt", ...Array.from({ length: 20 }, (_, i) => `ss${String(i + 1).padStart(2, "0")}`), "init", "fina", "titl", "hist", "aalt"];

/**
 * As formas alternativas de cada desenho: as trocas simples (tipo 1) e as de
 * várias opções (tipo 3) dos recursos de desenho, com a extensão (tipo 7).
 * Devolve desenho → [{ css, desenho novo }].
 */
function alternativasDaGsub(t: DataView | undefined): Map<number, Array<{ css: string; glifo: number }>> {
  const saida = new Map<number, Array<{ css: string; glifo: number }>>();
  if (!t || t.byteLength < 10) return saida;
  const [listaDeRecursos, listaDeTrocas] = [t.getUint16(6), t.getUint16(8)];
  if (!listaDeRecursos || !listaDeTrocas) return saida;
  const porRecurso = new Map<string, Set<number>>();
  const quantos = t.getUint16(listaDeRecursos);
  for (let i = 0; i < quantos; i++) {
    const r = listaDeRecursos + 2 + i * 6;
    if (r + 6 > t.byteLength) break;
    const tag = marca(t, r);
    if (!RECURSOS_DE_DESENHO.includes(tag)) continue;
    const recurso = listaDeRecursos + t.getUint16(r + 4);
    const indices = porRecurso.get(tag) ?? new Set<number>();
    for (let k = 0; k < t.getUint16(recurso + 2); k++) indices.add(t.getUint16(recurso + 4 + k * 2));
    porRecurso.set(tag, indices);
  }
  const totalDeTrocas = t.getUint16(listaDeTrocas);
  const por = (base: number, css: string, glifo: number) => {
    if (glifo === base) return;
    const lista = saida.get(base) ?? [];
    if (lista.length >= 24 || lista.some((a) => a.glifo === glifo)) return;
    lista.push({ css, glifo });
    saida.set(base, lista);
  };
  /* Uma parte da troca: tipo 1 (um desenho vira outro) ou 3 (várias opções, a escolha vai no número do CSS). */
  const lerParte = (tag: string, tipo: number, o: number) => {
    if (o + 6 > t.byteLength) return;
    const formato = t.getUint16(o);
    const cobertos = cobertura(t, o + t.getUint16(o + 2));
    if (tipo === 1 && formato === 1) {
      const delta = t.getInt16(o + 4);
      for (const g of cobertos) por(g, `'${tag}' 1`, (g + delta) & 0xffff);
    } else if (tipo === 1 && formato === 2) {
      cobertos.forEach((g, i) => o + 8 + i * 2 <= t.byteLength && por(g, `'${tag}' 1`, t.getUint16(o + 6 + i * 2)));
    } else if (tipo === 3 && formato === 1) {
      cobertos.forEach((g, i) => {
        const conjunto = o + t.getUint16(o + 6 + i * 2);
        const opcoes = t.getUint16(conjunto);
        for (let n = 0; n < opcoes && conjunto + 4 + n * 2 <= t.byteLength; n++) por(g, `'${tag}' ${n + 1}`, t.getUint16(conjunto + 2 + n * 2));
      });
    }
  };
  for (const tag of RECURSOS_DE_DESENHO) {
    for (const indice of porRecurso.get(tag) ?? []) {
      if (indice >= totalDeTrocas) continue;
      const troca = listaDeTrocas + t.getUint16(listaDeTrocas + 2 + indice * 2);
      const tipo = t.getUint16(troca);
      for (let k = 0; k < t.getUint16(troca + 4); k++) {
        const parte = troca + t.getUint16(troca + 6 + k * 2);
        /* Extensão (tipo 7): aponta para a troca de verdade, mais longe na tabela. */
        if (tipo === 7) lerParte(tag, t.getUint16(parte + 2), parte + t.getUint32(parte + 4));
        else lerParte(tag, tipo, parte);
      }
    }
  }
  return saida;
}

export function lerFonte(bytes: Uint8Array): LeituraDaFonte {
  const t = tabelas(bytes);
  const cmap = t.get("cmap");
  const desenhos = cmap ? caracteresDaCmap(cmap) : new Map<number, number>();
  const trocas = alternativasDaGsub(t.get("GSUB"));
  const alternativas: Record<number, string[]> = {};
  for (const [c, glifo] of desenhos) {
    const lista = trocas.get(glifo);
    if (lista?.length) alternativas[c] = lista.map((a) => a.css);
  }
  return { caracteres: [...desenhos.keys()].sort((a, b) => a - b), recursos: recursosDaGsub(t.get("GSUB")), alternativas };
}

/* ------------------------------------------------------- buscar a fonte */

const NAVEGADOR = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36";
/* Lidas por 24 h (a fonte não muda; a primeira leitura leva ~1 s). */
const lidas = ((globalThis as { __glifosDasFontes?: Map<string, { ate: number; leitura: LeituraDaFonte }> }).__glifosDasFontes ??= new Map());

/** As faixas de caracteres que o Google entrega ao navegador para essa fonte e peso. */
function faixasDoCss(css: string): Array<[number, number]> {
  const faixas: Array<[number, number]> = [];
  for (const m of css.matchAll(/unicode-range:\s*([^;]+);/g)) {
    for (const parte of m[1].split(",")) {
      const r = /U\+([0-9A-F?]+)(?:-([0-9A-F]+))?/i.exec(parte.trim());
      if (!r) continue;
      const de = parseInt(r[1].replace(/\?/g, "0"), 16);
      const ate = r[2] ? parseInt(r[2], 16) : r[1].includes("?") ? parseInt(r[1].replace(/\?/g, "F"), 16) : de;
      faixas.push([de, ate]);
    }
  }
  return faixas;
}

async function baixar(url: string, cabecalhos: Record<string, string> = {}) {
  const r = await fetch(url, { headers: cabecalhos, signal: AbortSignal.timeout(15000) });
  if (!r.ok) throw new Error(`fonte ${r.status}`);
  return r;
}

/** Os glifos de uma fonte da lista do editor (Google), no peso mais perto do pedido. */
export async function glifosDaFonte(chave: string, peso: number): Promise<LeituraDaFonte | null> {
  const fonte = FONTES.find((f) => f.chave === chave);
  if (!fonte) return null;
  const pesoDaFonte = fonte.pesos.reduce((perto, p) => (Math.abs(p - peso) < Math.abs(perto - peso) ? p : perto), fonte.pesos[0]);
  const id = `${chave}:${pesoDaFonte}`;
  const lida = lidas.get(id);
  if (lida && lida.ate > Date.now()) return lida.leitura;
  const familia = encodeURIComponent(fonte.familia).replace(/%20/g, "+");
  const endereco = `https://fonts.googleapis.com/css2?family=${familia}:wght@${pesoDaFonte}&display=block`;
  const [cssDoTtf, cssDoNavegador] = await Promise.all([baixar(endereco, { "User-Agent": "KtaloguDesign/1.0" }).then((r) => r.text()), baixar(endereco, { "User-Agent": NAVEGADOR }).then((r) => r.text())]);
  const ttf = /url\((https:\/\/fonts\.gstatic\.com\/[^)]+\.ttf)\)/.exec(cssDoTtf)?.[1];
  if (!ttf) return null;
  const leitura = lerFonte(new Uint8Array(await (await baixar(ttf)).arrayBuffer()));
  const faixas = faixasDoCss(cssDoNavegador);
  const entrega = (c: number) => !faixas.length || faixas.some(([de, ate]) => c >= de && c <= ate);
  const resultado = { ...leitura, caracteres: leitura.caracteres.filter(entrega), alternativas: Object.fromEntries(Object.entries(leitura.alternativas).filter(([c]) => entrega(Number(c)))) };
  lidas.set(id, { ate: Date.now() + 86_400_000, leitura: resultado });
  if (lidas.size > 300) lidas.delete(lidas.keys().next().value as string);
  return resultado;
}

/** Os glifos de uma fonte do kit da marca (arquivo no depósito da Ktalogu, nunca de outro site). */
export async function glifosDoArquivo(url: string): Promise<LeituraDaFonte | null> {
  const deposito = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/`;
  if (!url.startsWith(deposito)) return null;
  const lida = lidas.get(url);
  if (lida && lida.ate > Date.now()) return lida.leitura;
  const bytes = new Uint8Array(await (await baixar(url)).arrayBuffer());
  if (bytes.byteLength > 15 * 1024 * 1024) return null;
  const leitura = lerFonte(bytes);
  lidas.set(url, { ate: Date.now() + 86_400_000, leitura });
  return leitura;
}
