import { curvar, type CurvaDoQuadro } from "./animacao";
import type { PecaDeTexto } from "./documento";

/*
 * O número que sobe sozinho (R$ 0 → R$ 1.300): num texto, o PRIMEIRO número
 * (no jeito brasileiro: 1.300 / 49,90 / 25) conta de `de` até ele, de `inicio`
 * a `inicio + duracao`, pela curva. O resto do texto (o "R$", o "+", o "por
 * ano") fica. O jeito de escrever (casas depois da vírgula, ponto de milhar)
 * é o do número que está no texto.
 *
 * A mesma conta serve ao palco do editor (troca o texto no DOM), ao servidor
 * (fotografa o texto em cada valor) e ao MP4 (escolhe a foto do valor).
 */

export type ContadorDaPeca = { de: number; inicio: number; duracao: number; curva: CurvaDoQuadro };
export type MoldeDoNumero = { valor: number; casas: number; milhar: boolean };

const NUMERO = /\d{1,3}(?:\.\d{3})+(?:,\d+)?|\d+(?:,\d+)?/;
const MAXIMO_DE_FOTOS = 36;

/** O primeiro número do texto e o jeito de escrevê-lo (null se não há número). */
export function numeroDoTexto(texto: string): MoldeDoNumero | null {
  const m = NUMERO.exec(texto);
  if (!m) return null;
  const [inteiro, fracao = ""] = m[0].split(",");
  return { valor: Number(inteiro.replace(/\./g, "") + (fracao ? `.${fracao}` : "")), casas: fracao.length, milhar: inteiro.includes(".") };
}

/** O valor escrito como o número do molde (pt-BR). */
export function escreverComo(valor: number, molde: MoldeDoNumero): string {
  const fixo = Math.abs(valor).toFixed(molde.casas);
  const [inteiro, fracao] = fixo.split(".");
  const comMilhar = molde.milhar ? inteiro.replace(/\B(?=(\d{3})+(?!\d))/g, ".") : inteiro;
  return `${valor < 0 ? "-" : ""}${comMilhar}${fracao ? `,${fracao}` : ""}`;
}

/** O texto com o primeiro número trocado pelo valor. */
export const textoComValor = (texto: string, valor: number, molde: MoldeDoNumero) => texto.replace(NUMERO, escreverComo(valor, molde));

/** O HTML com o primeiro número dos TRECHOS DE TEXTO trocado (as tags e os estilos ficam como estão). */
export function htmlComValor(html: string, valor: number, molde: MoldeDoNumero): string {
  let feito = false;
  return html
    .split(/(<[^>]*>)/)
    .map((pedaco) => {
      if (feito || pedaco.startsWith("<") || !NUMERO.test(pedaco)) return pedaco;
      feito = true;
      return pedaco.replace(NUMERO, escreverComo(valor, molde));
    })
    .join("");
}

/** O texto da peça mostrando `valor` (no texto puro e nos trechos formatados). */
export function pecaNoValor(p: PecaDeTexto, valor: number): PecaDeTexto {
  const molde = numeroDoTexto(p.texto);
  if (!molde) return p;
  return { ...p, texto: textoComValor(p.texto, valor, molde), ...(p.html ? { html: htmlComValor(p.html, valor, molde) } : {}) };
}

/** O valor no instante `t` da cena (antes: `de`; depois: o do texto). */
export function valorNoTempo(c: ContadorDaPeca, molde: MoldeDoNumero, t: number): number {
  if (t <= c.inicio) return c.de;
  if (t >= c.inicio + c.duracao) return molde.valor;
  const e = curvar(c.curva, (t - c.inicio) / Math.max(0.05, c.duracao));
  const v = c.de + (molde.valor - c.de) * e;
  const passo = Math.pow(10, -molde.casas);
  return Math.round(v / passo) * passo;
}

/**
 * Os valores que o vídeo precisa fotografar: um por quadro durante a
 * contagem, sem repetidos, no máximo MAXIMO_DE_FOTOS (espalhados; o primeiro
 * e o último sempre). O quadro usa a foto do valor mais perto.
 */
export function valoresParaFotografar(c: ContadorDaPeca, molde: MoldeDoNumero, fps: number): number[] {
  const quadros = Math.max(1, Math.ceil(c.duracao * fps));
  const todos = [...new Set(Array.from({ length: quadros + 1 }, (_, i) => valorNoTempo(c, molde, c.inicio + (i / quadros) * c.duracao)))];
  if (todos.length <= MAXIMO_DE_FOTOS) return todos;
  return Array.from({ length: MAXIMO_DE_FOTOS }, (_, i) => todos[Math.round((i / (MAXIMO_DE_FOTOS - 1)) * (todos.length - 1))]);
}

/** O valor mais perto de `v` na lista (em ordem de contagem). */
export function maisPerto(lista: number[], v: number): number {
  let melhor = lista[0];
  for (const x of lista) if (Math.abs(x - v) < Math.abs(melhor - v)) melhor = x;
  return melhor;
}

/** O contador conferido (ou null). */
export function contadorLimpo(bruto: unknown, numero: (v: unknown, min: number, max: number, reserva: number) => number, curvas: readonly string[]): ContadorDaPeca | null {
  if (!bruto || typeof bruto !== "object") return null;
  const c = bruto as Record<string, unknown>;
  return { de: numero(c.de, -1e12, 1e12, 0), inicio: numero(c.inicio, 0, 60, 0), duracao: numero(c.duracao, 0.2, 20, 1.5), curva: curvas.includes(String(c.curva)) ? (c.curva as CurvaDoQuadro) : "freia" };
}
