import { animacaoNova } from "./animacao";
import { duracaoDaPagina, paginaNoTempo, temposDoProjeto } from "./cena";
import { novoId, solida, type DocumentoDoBanner, type PecaDeTexto } from "./documento";

/*
 * A narração (texto → voz, no servidor) e a legenda automática (fala →
 * palavras com o tempo de cada uma, no servidor). Aqui: as vozes e os jeitos
 * de falar que a tela oferece, e a montagem da legenda nas cenas — trechos
 * curtos (até 4 palavras), cada palavra entrando no segundo exato em que é
 * falada, o trecho na cena em que começa.
 */

export const VOZES = [
  { chave: "nova", nome: "Clara", detalhe: "Feminina, clara e animada" },
  { chave: "shimmer", nome: "Sofia", detalhe: "Feminina, suave" },
  { chave: "coral", nome: "Bia", detalhe: "Feminina, calorosa" },
  { chave: "onyx", nome: "Rafael", detalhe: "Masculina, grave" },
  { chave: "echo", nome: "Lucas", detalhe: "Masculina, leve" },
  { chave: "ash", nome: "Davi", detalhe: "Masculina, firme" },
] as const;
export type VozDaNarracao = (typeof VOZES)[number]["chave"];

export const JEITOS_DE_FALAR = [
  { chave: "anuncio", nome: "Anúncio animado", instrucao: "Fale em português do Brasil, com energia de anúncio de redes sociais: animado, confiante, ritmo rápido, sorrindo na voz." },
  { chave: "conversa", nome: "Conversa", instrucao: "Fale em português do Brasil, natural e próximo, como quem conversa com um amigo, sem exagero." },
  { chave: "calmo", nome: "Calmo", instrucao: "Fale em português do Brasil, calmo e acolhedor, devagar, com pausas suaves." },
] as const;
export type JeitoDeFalar = (typeof JEITOS_DE_FALAR)[number]["chave"];

export const MAXIMO_DA_NARRACAO = 1500;

/** Uma palavra falada: o texto e os segundos (do começo do arquivo). */
export type PalavraFalada = { texto: string; inicio: number; fim: number };

export const ESTILOS_DA_LEGENDA = [
  { chave: "palavras", nome: "Palavra a palavra", detalhe: "Branca com contorno, cada palavra pula na hora em que é falada" },
  { chave: "caixa", nome: "Caixa preta", detalhe: "Texto branco numa caixa, palavra por palavra" },
  { chave: "destaque", nome: "Marca-texto", detalhe: "Texto escuro com faixa limão" },
] as const;
export type EstiloDaLegenda = (typeof ESTILOS_DA_LEGENDA)[number]["chave"];
export const NOME_DA_LEGENDA = "Legenda automática";

/* Os trechos: até 4 palavras (ou ~24 letras), cortando na pontuação e nas pausas da fala. */
function trechos(palavras: PalavraFalada[]): PalavraFalada[][] {
  const lista: PalavraFalada[][] = [];
  let atual: PalavraFalada[] = [];
  for (const [i, p] of palavras.entries()) {
    const letras = atual.reduce((s, x) => s + x.texto.length + 1, 0);
    const pausa = i > 0 && p.inicio - palavras[i - 1].fim > 0.45;
    if (atual.length && (atual.length >= 4 || letras + p.texto.length > 24 || pausa)) {
      lista.push(atual);
      atual = [];
    }
    atual.push(p);
    if (/[.!?;:]$/.test(p.texto)) {
      lista.push(atual);
      atual = [];
    }
  }
  if (atual.length) lista.push(atual);
  return lista;
}

/**
 * As páginas com a legenda: `palavras` no tempo do arquivo, que toca no
 * projeto a partir de `comeco` (segundos do projeto), aparado em `corte`. A
 * legenda antiga (do mesmo nome) sai. Fica na parte de baixo da arte.
 */
export function comLegenda(paginas: DocumentoDoBanner[], palavras: PalavraFalada[], comeco: number, corte: number, estilo: EstiloDaLegenda, velocidade = 1): DocumentoDoBanner[] {
  const { inicios } = temposDoProjeto(paginas);
  const limpas = paginas.map((p) => ({ ...p, pecas: p.pecas.filter((x) => x.nome !== NOME_DA_LEGENDA) }));
  const noProjeto = palavras
    .filter((p) => p.inicio >= corte - 0.05)
    .map((p) => ({ ...p, texto: p.texto.trim(), inicio: comeco + (p.inicio - corte) / velocidade, fim: comeco + (p.fim - corte) / velocidade }))
    .filter((p) => p.texto);
  const lista = trechos(noProjeto);
  for (const [k, t] of lista.entries()) {
    const { indice } = paginaNoTempo(limpas, t[0].inicio);
    const pagina = limpas[indice];
    const local = Math.max(0, t[0].inicio - inicios[indice]);
    /* Sai quando o trecho seguinte entra (nunca duas legendas juntas). */
    const seguinte = lista[k + 1]?.[0].inicio ?? Infinity;
    const fim = Math.min(duracaoDaPagina(pagina), Math.min(t[t.length - 1].fim + 0.12, seguinte - 0.02) - inicios[indice]);
    if (fim - local < 0.15) continue;
    const L = pagina.largura;
    const tamanho = Math.round(Math.min(L, pagina.altura) * (estilo === "palavras" ? 0.085 : 0.075));
    const texto = t.map((p) => p.texto).join(" ").toUpperCase();
    const w = Math.round(L * 0.86);
    const h = Math.round(tamanho * 1.25 * 2.2);
    const base: PecaDeTexto = {
      id: novoId("t"),
      tipo: "texto",
      nome: NOME_DA_LEGENDA,
      x: Math.round((L - w) / 2),
      y: Math.round(pagina.altura * 0.7 - h / 2),
      w,
      h,
      rot: 0,
      opacidade: 1,
      texto,
      fonte: "anton",
      tamanho,
      peso: 400,
      pintura: solida(estilo === "destaque" ? "#0B0D09" : "#FFFFFF"),
      alinhamento: "centro",
      vertical: "meio",
      italico: false,
      caixaAlta: false,
      riscado: false,
      sublinhado: false,
      espaco: 0.01,
      linha: estilo === "destaque" ? 1.25 : 1.1,
      fundo: estilo === "caixa" ? { forma: "retangulo", pintura: solida("#0B0D09"), borda: null, raio: Math.round(tamanho * 0.18), pontas: 16, profundidade: 0.2 } : null,
      contorno: estilo === "palavras" ? { cor: "#0B0D09", largura: Math.max(3, Math.round(tamanho * 0.07)) } : null,
      sombra: estilo === "palavras" ? { cor: "#000000", x: 0, y: Math.round(tamanho * 0.06), desfoque: Math.round(tamanho * 0.12), opacidade: 0.55 } : null,
      ...(estilo === "destaque" ? { destaque: { pintura: solida("#C6F432"), respiro: 0.14, raio: 6 } } : {}),
    };
    const tempos = t.map((p) => Math.max(0, Math.round((p.inicio - t[0].inicio) * 100) / 100));
    const entrada = estilo === "destaque" ? "revelar" : estilo === "caixa" ? "chicote" : "pop";
    const animacao = {
      ...animacaoNova(entrada, { inicio: Math.round(local * 100) / 100, duracao: Math.max(0.3, (tempos[tempos.length - 1] ?? 0) + 0.25), fim: Math.round(fim * 100) / 100, saida: null }),
      ...(estilo === "destaque" ? { direcao: "direita" as const, duracao: 0.35 } : { porPalavra: true, tempoDasPalavras: tempos }),
    };
    limpas[indice] = { ...pagina, pecas: [...pagina.pecas, { ...base, animacao }] };
  }
  return limpas;
}

/**
 * Acerta os tempos aproximados (a transcrição de reserva) no próprio som: o
 * começo de cada palavra que vem depois de uma pausa vai para onde a voz
 * volta de verdade (a energia do som sobe), e nenhuma termina depois de a
 * seguinte começar. `amostras`: o canal do áudio; `taxa`: amostras por segundo.
 */
export function ajustarAoSom(palavras: PalavraFalada[], amostras: Float32Array, taxa: number): PalavraFalada[] {
  const passo = Math.max(1, Math.round(taxa * 0.01));
  const energia: number[] = [];
  for (let i = 0; i < amostras.length; i += passo) {
    let s = 0;
    const ate = Math.min(amostras.length, i + passo);
    for (let k = i; k < ate; k += 1) s += amostras[k] * amostras[k];
    energia.push(Math.sqrt(s / Math.max(1, ate - i)));
  }
  const pico = Math.max(...energia, 0.0001);
  const limiar = Math.max(0.01, pico * 0.12);
  /* Onde a voz volta depois de pelo menos 80 ms de silêncio. */
  const voltas: number[] = [];
  let calado = 8;
  energia.forEach((e, i) => {
    if (e < limiar) calado += 1;
    else {
      if (calado >= 8) voltas.push(i * 0.01);
      calado = 0;
    }
  });
  const duracao = amostras.length / taxa;
  const ajustadas = palavras.map((p) => ({ ...p, inicio: Math.min(duracao, p.inicio), fim: Math.min(duracao, Math.max(p.fim, p.inicio + 0.05)) }));
  ajustadas.forEach((p, i) => {
    const depoisDePausa = i === 0 || p.inicio - ajustadas[i - 1].fim > 0.04;
    if (!depoisDePausa) return;
    const perto = voltas.reduce((melhor, v) => (Math.abs(v - p.inicio) < Math.abs(melhor - p.inicio) ? v : melhor), Infinity);
    if (Math.abs(perto - p.inicio) <= 0.35 && (i === 0 || perto > ajustadas[i - 1].inicio)) {
      const dura = p.fim - p.inicio;
      p.inicio = Math.round(perto * 100) / 100;
      p.fim = Math.round((p.inicio + dura) * 100) / 100;
    }
  });
  ajustadas.forEach((p, i) => {
    const seguinte = ajustadas[i + 1];
    if (seguinte && p.fim > seguinte.inicio) p.fim = seguinte.inicio;
  });
  return ajustadas;
}

/* Quantas sílabas a palavra tem, mais ou menos (os números contam como falados por extenso). */
function silabas(texto: string): number {
  const numero = texto.replace(/[^\d]/g, "");
  if (numero) return Math.max(2, numero.length * 2.6);
  const vogais = texto.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").match(/[aeiouy]+/g);
  return Math.max(1, vogais?.length ?? 1);
}

/**
 * O tempo de cada palavra pelo próprio som: os trechos com voz (a energia
 * acima do chão) são o "relógio da fala"; as palavras se espalham por eles na
 * proporção das sílabas, e a palavra que vem depois de um ponto (ou de uma
 * vírgula) começa no trecho de voz seguinte, depois da pausa. Serve para a
 * voz gerada (o texto é conhecido) e para corrigir a transcrição aproximada.
 */
export function alinharNoSom(textos: string[], amostras: Float32Array, taxa: number): PalavraFalada[] {
  const passo = Math.max(1, Math.round(taxa * 0.01));
  const energia: number[] = [];
  for (let i = 0; i < amostras.length; i += passo) {
    let s = 0;
    const ate = Math.min(amostras.length, i + passo);
    for (let k = i; k < ate; k += 1) s += amostras[k] * amostras[k];
    energia.push(Math.sqrt(s / Math.max(1, ate - i)));
  }
  const pico = Math.max(...energia, 0.0001);
  const limiar = Math.max(0.006, pico * 0.08);
  /* Os trechos de voz (buracos de até 70 ms não contam como pausa). */
  const trechos: Array<[number, number]> = [];
  let comeco = -1;
  let calado = 0;
  energia.forEach((e, i) => {
    if (e >= limiar) {
      if (comeco < 0) comeco = i;
      calado = 0;
    } else if (comeco >= 0 && (calado += 1) > 7) {
      if (i - calado - comeco >= 4) trechos.push([comeco * 0.01, (i - calado + 1) * 0.01]);
      comeco = -1;
      calado = 0;
    }
  });
  if (comeco >= 0) trechos.push([comeco * 0.01, (energia.length - calado) * 0.01]);
  if (!trechos.length || !textos.length) return textos.map((texto, i) => ({ texto, inicio: i * 0.3, fim: i * 0.3 + 0.3 }));
  const pesos = textos.map(silabas);
  /* As frases (até a pontuação) e onde, pela proporção, cada uma deveria acabar. */
  const grupos: number[][] = [[]];
  textos.forEach((t, i) => {
    grupos[grupos.length - 1].push(i);
    if (/[.,!?;:]$/.test(t) && i < textos.length - 1) grupos.push([]);
  });
  const voz = (lista: Array<[number, number]>) => lista.reduce((s, [a, b]) => s + (b - a), 0);
  const noAudio = (lista: Array<[number, number]>, v: number) => {
    let resta = v;
    for (const [a, b] of lista) {
      if (resta <= b - a) return a + resta;
      resta -= b - a;
    }
    return lista[lista.length - 1][1];
  };
  const soma = pesos.reduce((s, p) => s + p, 0);
  const total = voz(trechos);
  /* Cada fim de frase vai para a pausa de verdade mais perto (sempre depois da anterior). */
  const pausas = trechos.slice(1).map(([a], k) => ({ k: k + 1, meio: (trechos[k][1] + a) / 2, tamanho: a - trechos[k][1] }));
  const cortes: number[] = [];
  let acumulado = 0;
  let ultimo = 0;
  for (const g of grupos.slice(0, -1)) {
    acumulado += g.reduce((s, i) => s + pesos[i], 0);
    const esperado = noAudio(trechos, (acumulado / soma) * total);
    /* O ponto pede pausa maior que a vírgula; entre as perto, vale mais a pausa maior. */
    const minima = /[.!?]$/.test(textos[g[g.length - 1]]) ? 0.12 : 0.05;
    const nota = (x: (typeof pausas)[number]) => Math.abs(x.meio - esperado) - 2.5 * x.tamanho;
    const candidata = pausas.filter((x) => x.k > ultimo && x.tamanho >= minima).reduce<(typeof pausas)[number] | null>((m, x) => (!m || nota(x) < nota(m) ? x : m), null);
    if (candidata && Math.abs(candidata.meio - esperado) < 1.2) {
      cortes.push(candidata.k);
      ultimo = candidata.k;
    } else cortes.push(-1);
  }
  /* Os blocos de voz de cada frase (a frase sem pausa achada junta com a seguinte). */
  const palavras: PalavraFalada[] = [];
  let deTrecho = 0;
  let pendentes: number[] = [];
  grupos.forEach((g, gi) => {
    pendentes = [...pendentes, ...g];
    const corte = gi < cortes.length ? cortes[gi] : trechos.length;
    if (corte === -1) return;
    const bloco = trechos.slice(deTrecho, Math.max(deTrecho + 1, corte));
    deTrecho = Math.max(deTrecho + 1, corte);
    const vozDoBloco = voz(bloco);
    const somaDoBloco = pendentes.reduce((s, i) => s + pesos[i], 0);
    let andou = 0;
    for (const i of pendentes) {
      const inicio = noAudio(bloco, (andou / somaDoBloco) * vozDoBloco);
      andou += pesos[i];
      palavras.push({ texto: textos[i], inicio, fim: noAudio(bloco, (andou / somaDoBloco) * vozDoBloco) });
    }
    pendentes = [];
  });
  palavras.forEach((p, i) => {
    if (palavras[i + 1] && p.fim > palavras[i + 1].inicio) p.fim = palavras[i + 1].inicio;
    p.inicio = Math.round(p.inicio * 100) / 100;
    p.fim = Math.round(Math.max(p.fim, p.inicio + 0.05) * 100) / 100;
  });
  return palavras;
}
