import { animacaoNova, PARADO, PERSPECTIVA, type AnimacaoDaPeca } from "@/lib/banner-editor/animacao";
import { animarPagina, comAudios, audiosDoProjeto, duracaoDaPagina, estadosNoTempo, temposDoProjeto, unidadesDaCena, centroDa, ESTILOS_DA_PAGINA, type EstiloDaPagina } from "@/lib/banner-editor/cena";
import { numeroDoTexto, pecaNoValor, valorNoTempo } from "@/lib/banner-editor/contador";
import { CUPONS_PRONTOS, SELOS_DE_VENDA, type ElementoPronto } from "@/lib/banner-editor/cupons-e-selos";
import {
  documentoDasPaginas,
  documentoLimpo,
  ehFundoDaArte,
  escalarPeca,
  novoId,
  paginasDoDocumento,
  solida,
  type AudioDoProjeto,
  type DistorcaoDaPeca,
  type DocumentoDoBanner,
  type Peca,
  type PecaDeTexto,
  type Pintura,
} from "@/lib/banner-editor/documento";
import { DESTAQUES_DE_VIDEO, LEGENDAS_DE_VIDEO, PARTICULAS_DE_VIDEO, TELAS_DE_VIDEO, encaixar } from "@/lib/banner-editor/elementos-de-video";
import { FORMATOS_DA_ARTE } from "@/lib/banner-editor/formatos";
import { documento as documentoDoModelo, fundoDoBanner, fundoDoTexto, imagem as imagemDoModelo, texto as textoDoModelo, type PedidoDoModelo } from "@/lib/banner-editor/modelos-base";
import { DATAS_ESPECIAIS, ELEMENTOS_3D } from "@/lib/banner-editor/pacotes";
import { DEGRADES_METALICOS, metalicoPeloNome } from "@/lib/banner-editor/metalicos";
import { DEPOSITO_DA_BIBLIOTECA, GRUPOS_DOS_ORNAMENTOS } from "@/lib/banner-editor/biblioteca";
import { comFontesComGlifos, registrarFontesComGlifos } from "@/lib/banner-editor/fontes-com-glifos";
import { DEPOSITO_DAS_FIGURINHAS, MOLDURAS_PRONTAS, forma as formaPronta } from "@/lib/banner-editor/pecas-novas";
import { pontoNoEstado } from "@/lib/banner-editor/perspectiva";
import { TIPOS_DA_CHAVE_PIX, pixCopiaECola, type TipoDaChavePix } from "@/lib/banner-editor/pix-copia-e-cola";
import SONS from "@/lib/banner-editor/sons-da-biblioteca.json";
import { ErroDaFerramenta } from "./tipos";

/*
 * O editor de artes (KtaloguDesign) pelo assistente: o "jeito curto" de
 * escrever uma arte (o mesmo que os modelos da casa usam — cor em vez de
 * pintura, sombra por nome, animação só com o que muda, elemento pronto pela
 * chave), as operações de edição e o resumo que o assistente lê. Tudo aqui é
 * conta pura; quem baixa imagem, fotografa e grava é editor-de-artes.ts. No
 * fim sempre passa pela mesma limpeza da tela (documentoLimpo).
 */

type Bruto = Record<string, unknown>;
/* Vai ao assistente como recusa (é para ele se corrigir). */
export class ErroNaArte extends ErroDaFerramenta {}
const recusar = (m: string): never => {
  throw new ErroNaArte(m);
};
const ehObjeto = (v: unknown): v is Bruto => Boolean(v) && typeof v === "object" && !Array.isArray(v);
const num = (v: unknown, reserva: number) => (typeof v === "number" && Number.isFinite(v) ? v : typeof v === "string" && v.trim() && Number.isFinite(Number(v)) ? Number(v) : reserva);
const DEPOSITO_DAS_FOTOS = "https://ptpstwltddugnnlewvbw.supabase.co/storage/v1/object/public/products/editor-modelos/ramos";

/* ------------------------------------------------------------ tamanhos */

export const FORMATOS = FORMATOS_DA_ARTE.map((f) => f.chave);
export function tamanhoDaArte(formato: unknown, largura: unknown, altura: unknown): { largura: number; altura: number; formato: string | null } {
  const f = FORMATOS_DA_ARTE.find((x) => x.chave === formato);
  if (f) return { largura: f.largura, altura: f.altura, formato: f.chave };
  const w = Math.round(num(largura, 0));
  const h = Math.round(num(altura, 0));
  if (w < 100 || h < 100 || w > 8000 || h > 8000) recusar(`Diga o formato (${FORMATOS.join(", ")}) ou largura e altura entre 100 e 8000 px.`);
  return { largura: w, altura: h, formato: null };
}

/* ---------------------------------------------------- elementos prontos */

type Grupo = { nome: string; lista: Array<{ chave: string; nome: string; criar: (m: { largura: number; altura: number }) => Peca[] }> };
export const GRUPOS_DE_ELEMENTOS: Grupo[] = [
  { nome: "Para vídeo: telas e interface", lista: TELAS_DE_VIDEO },
  { nome: "Para vídeo: destaques animados", lista: DESTAQUES_DE_VIDEO },
  { nome: "Para vídeo: títulos e legendas", lista: LEGENDAS_DE_VIDEO },
  { nome: "Para vídeo: partículas (tela inteira)", lista: PARTICULAS_DE_VIDEO },
  { nome: "Cupons e vales", lista: CUPONS_PRONTOS },
  { nome: "Selos de venda", lista: SELOS_DE_VENDA },
  { nome: "Datas especiais", lista: DATAS_ESPECIAIS },
  { nome: "Formas em 3D", lista: ELEMENTOS_3D },
  { nome: "Molduras para foto (a pessoa solta a foto dentro)", lista: MOLDURAS_PRONTAS.map((m) => ({ chave: m.chave, nome: m.nome, criar: (x: { largura: number; altura: number }) => [m.criar(x)] })) },
];
const ELEMENTOS = new Map(GRUPOS_DE_ELEMENTOS.flatMap((g) => g.lista.map((e) => [e.chave, e] as const)));
export const descricaoDoElemento = (e: ElementoPronto | { chave: string; nome: string }) => ("busca" in e ? `${e.nome} — ${e.busca}` : e.nome);

/* Um elemento pronto encaixado na caixa (ou no meio da página), com os tempos empurrados `atraso` segundos. */
function elementoPronto(chave: string, m: { largura: number; altura: number }, caixa: unknown, atraso: number): Peca[] {
  const e = ELEMENTOS.get(chave);
  if (!e) recusar(`Elemento pronto "${chave}" não existe. Procure com procurar_no_editor (tipo "elementos").`);
  const pecas = e!.criar(m);
  const c = ehObjeto(caixa) ? { x: num(caixa.x, 0), y: num(caixa.y, 0), w: Math.max(10, num(caixa.w, m.largura * 0.6)), h: Math.max(10, num(caixa.h, m.altura * 0.4)) } : null;
  const postas = c ? encaixar(pecas, c) : pecas;
  if (!atraso) return postas;
  return postas.map((p) => {
    const a = p.animacao;
    return a ? ({ ...p, animacao: { ...a, inicio: a.inicio + atraso, fim: a.fim === null ? null : a.fim + atraso, mover: a.mover ? { ...a.mover, inicio: a.mover.inicio + atraso } : a.mover, contador: a.contador ? { ...a.contador, inicio: a.contador.inicio + atraso } : a.contador } } as Peca) : p;
  });
}

/* -------------------------------------------------------------- pintura */

/* "cor": "#FF0000"; "degrade": { "cor": "#..", "cor2": "#..", "angulo": 135 } ou "radial": {...}; "degrade": "dourado" (os metálicos); ou a pintura inteira. */
function pinturaDe(b: Bruto, reserva: string): Pintura {
  if (ehObjeto(b.pintura)) return b.pintura as Pintura;
  if (typeof b.degrade === "string") {
    const metal = metalicoPeloNome(b.degrade);
    if (!metal) recusar(`Degradê "${b.degrade}" não existe. Os metálicos prontos são: ${DEGRADES_METALICOS.map((m) => m.chave).join(", ")}; ou mande { "cor", "cor2", "angulo" }.`);
    return metal!;
  }
  if (ehObjeto(b.degrade)) {
    const d = b.degrade;
    return { tipo: d.radial ? "radial" : "linear", cor: String(d.cor ?? reserva), cor2: String(d.cor2 ?? d.cor ?? reserva), angulo: num(d.angulo, 135) };
  }
  if (b.cor === "nenhuma" || b.cor === null) return { tipo: "nenhuma", cor: "#FFFFFF", cor2: "#FFFFFF", angulo: 0 };
  return solida(typeof b.cor === "string" ? b.cor : reserva);
}
/* "sombra": "suave" | "dura" | { cor, x, y, desfoque, opacidade } | true. */
function sombraDe(v: unknown, lado: number) {
  if (v === "suave" || v === true) return { cor: "#000000", x: 0, y: Math.round(lado * 0.012), desfoque: Math.round(lado * 0.03), opacidade: 0.3 };
  if (v === "dura") return { cor: "#000000", x: Math.round(lado * 0.006), y: Math.round(lado * 0.006), desfoque: 0, opacidade: 0.9 };
  if (ehObjeto(v)) return { cor: String(v.cor ?? "#000000"), x: num(v.x, 0), y: num(v.y, 6), desfoque: num(v.desfoque, 16), opacidade: num(v.opacidade, 0.3) };
  return null;
}

/* ------------------------------------------------------------- animação */

/** A animação do jeito curto: só os campos que mudam (o resto vem do padrão do editor). `null` tira. */
export function animacaoDoAssistente(v: unknown): AnimacaoDaPeca | undefined {
  if (!ehObjeto(v)) return undefined;
  const entrada = typeof v.entrada === "string" ? v.entrada : null;
  const base = animacaoNova(entrada as AnimacaoDaPeca["entrada"], { inicio: num(v.inicio, entrada ? 0.2 : 0) });
  const a = { ...base, ...v, entrada } as Bruto;
  if (v.duracao === undefined && entrada) a.duracao = base.duracao;
  return a as unknown as AnimacaoDaPeca;
}

/* --------------------------------------------------------------- peças */

const CAMPOS_COMUNS = ["id", "nome", "x", "y", "w", "h", "rot", "opacidade", "mistura", "bloqueada", "oculta", "grupo", "efeitos", "distorcao", "fixa", "dentroDe", "espelharX", "espelharY", "transparencia"] as const;
function comuns(b: Bruto) {
  const c: Bruto = {};
  for (const k of CAMPOS_COMUNS) if (b[k] !== undefined) c[k] = b[k];
  return c;
}

/*
 * O QR do Pix pela chave: "pix": { chave, nome, cidade, valor?, mensagem?,
 * tipo_da_chave?, identificador? } vira o "copia e cola" do Banco Central
 * (o código com o CRC certo; montado à mão, o banco recusa). Sem "pix", vale
 * o "conteudo" (link, WhatsApp, texto ou um copia e cola pronto).
 */
function conteudoDoQr(b: Bruto): string {
  if (b.pix === undefined || b.pix === null) return String(b.conteudo ?? "");
  if (!ehObjeto(b.pix)) recusar('"pix" do QR é { "chave", "nome", "cidade", "valor"?, "mensagem"?, "tipo_da_chave"? }.');
  const p = b.pix as Bruto;
  const tipo = p.tipo_da_chave === undefined || p.tipo_da_chave === null ? undefined : TIPOS_DA_CHAVE_PIX.includes(p.tipo_da_chave as TipoDaChavePix) ? (p.tipo_da_chave as TipoDaChavePix) : recusar(`tipo_da_chave "${String(p.tipo_da_chave)}" não existe. Use: ${TIPOS_DA_CHAVE_PIX.join(", ")}.`);
  const valor = p.valor === undefined || p.valor === null || p.valor === "" ? null : Number(String(p.valor).replace(",", "."));
  const feito = pixCopiaECola({
    chave: String(p.chave ?? ""),
    tipo,
    nome: String(p.nome ?? ""),
    cidade: String(p.cidade ?? ""),
    valor,
    mensagem: p.mensagem === undefined || p.mensagem === null ? null : String(p.mensagem),
    identificador: p.identificador === undefined || p.identificador === null ? null : String(p.identificador),
  });
  return feito.ok ? feito.conteudo : recusar(`QR do Pix: ${feito.erro}`);
}

/* "giro3d": [graus de cima para baixo, graus de lado] → os cantos com perspectiva (a mesma conta do "Girar em 3D" do painel). */
function distorcaoDoGiro(v: unknown, w: number, h: number): DistorcaoDaPeca | null {
  if (!Array.isArray(v) || v.length !== 2) return null;
  const rx = Math.max(-80, Math.min(80, num(v[0], 0)));
  const ry = Math.max(-80, Math.min(80, num(v[1], 0)));
  if (!rx && !ry) return null;
  const p = PERSPECTIVA * Math.max(w, h);
  const cantos = ([[0, 0], [w, 0], [w, h], [0, h]] as const).map(([x0, y0]) => {
    const [x, y] = pontoNoEstado({ ...PARADO, rx, ry }, [w / 2, h / 2], p, x0, y0);
    return [Math.round((x / w) * 10000) / 10000, Math.round((y / h) * 10000) / 10000] as [number, number];
  }) as DistorcaoDaPeca["cantos"];
  return { cantos, giro3d: [rx, ry] };
}

/**
 * Uma peça do jeito do assistente → as peças do editor (um elemento pronto
 * vira várias). A imagem já vem com o endereço do depósito (editor-de-artes.ts
 * troca "envio:", "produto:", "foto:", "figurinha:" e os links antes).
 */
export function pecasDoAssistente(b: Bruto, m: { largura: number; altura: number }): Peca[] {
  if (typeof b.elemento === "string") return elementoPronto(b.elemento, m, b.caixa, num(b.atraso, 0));
  const lado = Math.min(m.largura, m.altura);
  const caixa = { x: num(b.x, m.largura * 0.1), y: num(b.y, m.altura * 0.1), w: Math.max(4, num(b.w, m.largura * 0.8)), h: Math.max(4, num(b.h, m.altura * 0.2)) };
  const animacao = b.animacao === null ? undefined : animacaoDoAssistente(b.animacao);
  const giro = distorcaoDoGiro(b.giro3d, caixa.w, caixa.h);
  const fim = (p: Peca): Peca[] => [{ ...p, ...comuns(b), ...caixa, id: typeof b.id === "string" && b.id ? b.id : p.id, ...(animacao ? { animacao } : {}), ...(giro ? { distorcao: giro } : {}) } as Peca];
  switch (b.tipo) {
    case "texto": {
      const t = textoDoModelo({
        texto: String(b.texto ?? ""),
        tamanho: num(b.tamanho, Math.round(lado * 0.07)),
        ...(Object.fromEntries(["html", "fonte", "peso", "alinhamento", "vertical", "italico", "caixaAlta", "riscado", "sublinhado", "espaco", "linha", "curva", "efeito", "lista", "contorno", "recursos"].filter((k) => b[k] !== undefined).map((k) => [k, b[k]])) as Partial<PecaDeTexto>),
        fundo: ehObjeto(b.fundo) ? { forma: (b.fundo.forma as never) ?? "retangulo", cor: String(b.fundo.cor ?? "#000000"), cor2: (b.fundo.cor2 as string) ?? null, raio: num(b.fundo.raio, 16) } : null,
      } as unknown as Parameters<typeof textoDoModelo>[0]);
      return fim({
        ...t,
        pintura: pinturaDe(b, "#141414"),
        ...(b.fundo && ehObjeto(b.fundo) && b.fundo.pintura ? { fundo: { ...fundoDoTexto({ forma: (b.fundo.forma as never) ?? "retangulo", cor: "#000000" }), ...(b.fundo as Bruto) } } : {}),
        sombra: sombraDe(b.sombra, lado),
        ...(ehObjeto(b.destaque) ? { destaque: { pintura: pinturaDe(b.destaque, "#FFE600"), respiro: num(b.destaque.respiro, 0.14), raio: num(b.destaque.raio, 6) } } : {}),
      } as Peca);
    }
    case "forma": {
      const f = formaPronta(m, (b.forma as never) ?? "retangulo", caixa.w, caixa.h, typeof b.cor === "string" ? b.cor : "#7C3AED", { raio: num(b.raio, 0) });
      return fim({ ...f, pintura: pinturaDe(b, "#7C3AED"), borda: ehObjeto(b.borda) ? { cor: String(b.borda.cor ?? "#000000"), largura: num(b.borda.largura, 4) } : null, sombra: sombraDe(b.sombra, lado), ...(b.pontas !== undefined ? { pontas: num(b.pontas, 5) } : {}), ...(b.profundidade !== undefined ? { profundidade: num(b.profundidade, 0.5) } : {}) } as Peca);
    }
    case "imagem": {
      const src = String(b.imagem ?? b.src ?? "");
      if (!src) recusar('A peça de imagem precisa de "imagem" (envio:..., produto:<id>, foto:<nome>, figurinha:<chave>, recorte:<código> ou um link https://).');
      const i = imagemDoModelo({ src, sombra: false, ajuste: b.ajuste === "conter" ? "conter" : "cobrir" });
      return fim({
        ...i,
        ...Object.fromEntries(["papel", "raio", "circulo", "mascara", "moldura", "enquadre", "duotone", "adesivo", "desvanecer", "chroma", "filtros", "vetor", "cilindro"].filter((k) => b[k] !== undefined).map((k) => [k, k === "filtros" && ehObjeto(b.filtros) ? { ...i.filtros, ...b.filtros } : b[k]])),
        borda: ehObjeto(b.borda) ? { cor: String(b.borda.cor ?? "#FFFFFF"), largura: num(b.borda.largura, 8) } : null,
        sombra: sombraDe(b.sombra, lado),
      } as Peca);
    }
    case "icone": {
      const chave = String(b.icone ?? "estrela");
      return fim({ id: novoId("ic"), tipo: "icone", nome: "Ícone", x: 0, y: 0, w: 100, h: 100, rot: 0, opacidade: 1, icone: chave, ...(typeof b.desenho === "string" ? { desenho: b.desenho } : {}), pintura: pinturaDe(b, "#141414"), espessura: num(b.espessura, 2), preenchido: b.preenchido === true, sombra: sombraDe(b.sombra, lado) } as Peca);
    }
    case "textura":
      return fim({ id: novoId("tex"), tipo: "textura", nome: "Textura", x: 0, y: 0, w: 100, h: 100, rot: 0, opacidade: num(b.opacidade, 0.2), estilo: (b.estilo as never) ?? "pontos", cor: String(b.cor ?? "#FFFFFF"), quantidade: num(b.quantidade, 20), tamanho: num(b.tamanho, 0), centroX: num(b.centroX, 50), centroY: num(b.centroY, 50), giro: num(b.giro, 0), ...(b.velocidade !== undefined ? { velocidade: num(b.velocidade, 1) } : {}) } as Peca);
    case "traco": {
      /* Pontos em px da arte ([[x, y], ...]); viram frações da caixa justa. */
      const pts = Array.isArray(b.pontos) ? (b.pontos as unknown[]).map((p) => (Array.isArray(p) ? [num(p[0], 0), num(p[1], 0)] : ehObjeto(p) ? [num(p.x, 0), num(p.y, 0)] : [0, 0])) : [];
      if (pts.length < 2) recusar('O traço precisa de "pontos": ao menos dois [x, y] em px da arte.');
      const esp = num(b.espessura, 6);
      const xs = pts.map((p) => p[0]);
      const ys = pts.map((p) => p[1]);
      const x = Math.min(...xs) - esp;
      const y = Math.min(...ys) - esp;
      const w = Math.max(...xs) - x + esp;
      const h = Math.max(...ys) - y + esp;
      return [
        {
          id: typeof b.id === "string" && b.id ? b.id : novoId("tr"),
          tipo: "traco",
          nome: String(b.nome ?? "Traço"),
          x,
          y,
          w,
          h,
          rot: num(b.rot, 0),
          opacidade: num(b.opacidade, 1),
          pontos: pts.map(([px, py]) => ({ x: (px - x) / w, y: (py - y) / h })),
          fechado: b.fechado === true,
          suave: b.suave !== false,
          cor: String(b.cor ?? "#141414"),
          espessura: esp,
          tracejado: b.tracejado === "tracejado" || b.tracejado === "pontilhado" ? b.tracejado : "solido",
          inicio: (b.inicio as never) ?? "nada",
          fim: (b.fim as never) ?? "nada",
          preenchimento: b.preenchimento ? solida(String(b.preenchimento)) : { tipo: "nenhuma", cor: "#FFFFFF", cor2: "#FFFFFF", angulo: 0 },
          sombra: sombraDe(b.sombra, lado),
          ...(animacao ? { animacao } : {}),
        } as Peca,
      ];
    }
    case "qr":
      return fim({ id: novoId("qr"), tipo: "qr", x: 0, y: 0, w: 100, h: 100, rot: 0, opacidade: 1, conteudo: conteudoDoQr(b), cor: String(b.cor ?? "#141414"), fundo: b.fundo === null ? null : String(b.fundo ?? "#FFFFFF"), arredondado: b.arredondado === true } as Peca);
    case "tabela":
    case "grafico":
      return fim({ id: novoId(b.tipo === "tabela" ? "tb" : "gr"), x: 0, y: 0, w: 100, h: 100, rot: 0, opacidade: 1, ...b } as unknown as Peca);
    default:
      return recusar(`Tipo de peça "${String(b.tipo)}" não existe. Use texto, forma, imagem, icone, textura, traco, qr, tabela, grafico, ou { "elemento": "<chave>" }.`);
  }
}

/* ------------------------------------------------------------- páginas */

/* "fundo": "#hex" | { cor, cor2, angulo, textura, corDaTextura } (vira as primeiras camadas, travadas, como nos modelos). */
function fundoDe(v: unknown) {
  if (typeof v === "string") return fundoDoBanner({ cor: v, textura: "nenhuma" });
  if (ehObjeto(v)) return fundoDoBanner({ cor: String(v.cor ?? "#FFFFFF"), cor2: (v.cor2 as string) ?? null, angulo: num(v.angulo, 135), textura: (v.textura as never) ?? "nenhuma", corDaTextura: (v.corDaTextura as string) ?? undefined });
  return fundoDoBanner({ cor: "#FFFFFF", textura: "nenhuma" });
}

/** Uma página (cena) do jeito do assistente. */

export function paginaDoAssistente(b: Bruto, m: { largura: number; altura: number }): DocumentoDoBanner {
  const pecas = (Array.isArray(b.pecas) ? b.pecas : []).flatMap((p, i) => {
    if (!ehObjeto(p)) recusar(`pecas[${i + 1}] precisa ser um objeto.`);
    try {
      return pecasDoAssistente(p as Bruto, m);
    } catch (e) {
      throw new ErroNaArte(`pecas[${i + 1}]: ${e instanceof Error ? e.message : e}`);
    }
  });
  const doc = comFontesComGlifos(documentoDoModelo(m.largura, m.altura, fundoDe(b.fundo), pecas));
  const transicao = typeof b.transicao === "string" ? { tipo: b.transicao, duracao: 0.6 } : ehObjeto(b.transicao) ? { tipo: b.transicao.tipo, duracao: num(b.transicao.duracao, 0.6) } : null;
  return { ...doc, ...(b.duracao !== undefined || b.movimento !== undefined ? { animacao: { duracao: num(b.duracao, 5), fundo: (b.movimento as never) ?? "parado" } } : {}), ...(transicao ? { transicao: transicao as never } : {}) };
}

/* ---------------------------------------------------------------- som */

type Som = { chave: string; nome: string; tipo: "musica" | "efeito"; src: string; duracao: number; onda: number[] };
export const CATALOGO_DE_SONS = SONS as Som[];

/** Um áudio do catálogo: { som, inicio, dura, volume, corte, fade_in, fade_out }. */
export function audioDoAssistente(b: Bruto, total: number): AudioDoProjeto {
  const s = CATALOGO_DE_SONS.find((x) => x.chave === b.som);
  if (!s) recusar(`Som "${String(b.som)}" não está no catálogo. Procure com procurar_no_editor (tipo "sons").`);
  const inicio = Math.max(0, num(b.inicio, 0));
  const corte = Math.max(0, num(b.corte, 0));
  const dura = Math.max(0.1, Math.min(num(b.dura, s!.tipo === "musica" ? total - inicio : s!.duracao), s!.duracao - corte));
  return {
    id: novoId("a"),
    nome: s!.nome,
    src: s!.src,
    tipo: s!.tipo,
    duracao: s!.duracao,
    inicio,
    corte,
    dura: Math.round(dura * 100) / 100,
    volume: Math.max(0, Math.min(1, num(b.volume, s!.tipo === "musica" ? 0.5 : 0.8))),
    suave: false,
    fadeDeEntrada: Math.max(0, num(b.fade_in, s!.tipo === "musica" ? 0.5 : 0)),
    fadeDeSaida: Math.max(0, num(b.fade_out, s!.tipo === "musica" ? 1.2 : 0)),
    onda: s!.onda,
  };
}

/* ------------------------------------------------------------ a arte */

/**
 * A arte inteira do jeito do assistente: { paginas: [...], audios: [...] }
 * no tamanho dado. Com `modelo`, a arte nasce do modelo pronto (as páginas
 * mandadas, se houver, entram no lugar das do modelo, na mesma ordem).
 */
export function documentoDoAssistente(b: Bruto, m: { largura: number; altura: number }, modelo?: DocumentoDoBanner | null): DocumentoDoBanner {
  /* As fontes com glifos passam a valer aqui (o documento as reconhece ao conferir). */
  registrarFontesComGlifos();
  const brutas = Array.isArray(b.paginas) ? (b.paginas as unknown[]) : ehObjeto(b) && Array.isArray(b.pecas) ? [b] : [];
  const doModelo = modelo ? paginasDoDocumento(modelo) : [];
  if (!brutas.length && !doModelo.length) recusar('Mande "paginas" (cada uma com "pecas"), ou escolha um "modelo".');
  const paginas = brutas.length ? brutas.map((p, i) => (ehObjeto(p) ? paginaDoAssistente(p, m) : recusar(`paginas[${i + 1}] precisa ser um objeto.`))) : doModelo;
  const { total } = temposDoProjeto(paginas);
  const audios = Array.isArray(b.audios) ? (b.audios as unknown[]).map((a) => (ehObjeto(a) ? audioDoAssistente(a, total) : recusar("Cada áudio precisa ser um objeto { som, inicio, ... }."))) : audiosDoProjeto(doModelo);
  return documentoLimpo(documentoDasPaginas(comAudios(paginas, audios)), m.largura, m.altura);
}

/* --------------------------------------------------------- operações */

/*
 * As operações de edição (alterar_arte). Páginas contam de 1. Campos de peça
 * no jeito curto (cor, sombra, animacao...) também valem em "alterar".
 */
export const OPERACOES = [
  "acrescentar_pecas",
  "alterar_peca",
  "remover_pecas",
  "ordenar_peca",
  "acrescentar_pagina",
  "duplicar_pagina",
  "remover_pagina",
  "mover_pagina",
  "alterar_pagina",
  "animar_pagina",
  "tirar_animacao",
  "trocar_audios",
  "acrescentar_audio",
  "remover_audio",
  "trocar_documento",
] as const;

function indiceDaPagina(paginas: DocumentoDoBanner[], v: unknown): number {
  const n = Math.round(num(v, 1));
  if (n < 1 || n > paginas.length) recusar(`A página ${String(v)} não existe (a arte tem ${paginas.length}).`);
  return n - 1;
}

/** Aplica as operações em ordem; devolve a arte limpa e o que foi feito, em palavras. */
export function aplicarOperacoes(doc: DocumentoDoBanner, operacoes: Bruto[]): { doc: DocumentoDoBanner; feitos: string[] } {
  registrarFontesComGlifos();
  let paginas = paginasDoDocumento(doc);
  let audios = audiosDoProjeto(paginas);
  const m = { largura: doc.largura, altura: doc.altura };
  const feitos: string[] = [];
  operacoes.forEach((o, k) => {
    const onde = `operacoes[${k + 1}] (${String(o.op)})`;
    try {
      switch (o.op) {
        case "acrescentar_pecas": {
          const i = indiceDaPagina(paginas, o.pagina);
          const novas = (Array.isArray(o.pecas) ? o.pecas : []).flatMap((p) => (ehObjeto(p) ? pecasDoAssistente(p, m) : []));
          if (!novas.length) recusar('Mande "pecas" com ao menos uma peça.');
          paginas[i] = { ...paginas[i], pecas: [...paginas[i].pecas, ...novas] };
          feitos.push(`${novas.length} peça(s) na página ${i + 1} (ids: ${novas.map((p) => p.id).join(", ")})`);
          break;
        }
        case "alterar_peca": {
          const i = indiceDaPagina(paginas, o.pagina);
          const atual = paginas[i].pecas.find((p) => p.id === o.id);
          if (!atual) throw new ErroNaArte(`A peça "${String(o.id)}" não está na página ${i + 1}. Veja os ids em ver_arte.`);
          const campos = ehObjeto(o.campos) ? o.campos : recusar('Mande "campos" com o que muda.');
          /* O jeito curto também vale: refaz a peça do tipo dela com o que veio e junta por cima. */
          const atalhos = ["cor", "degrade", "sombra", "borda", "imagem", "destaque", "fundo"].some((c) => (campos as Bruto)[c] !== undefined);
          let nova: Peca = { ...atual, ...(campos as Bruto) } as Peca;
          if (atalhos) {
            /* A cor nova vence a pintura antiga; a imagem nova, o endereço antigo. */
            const pinta = (campos as Bruto).cor !== undefined || (campos as Bruto).degrade !== undefined;
            const refeita = pecasDoAssistente({ ...atual, ...(pinta ? { pintura: undefined } : {}), ...((campos as Bruto).imagem !== undefined ? { src: undefined } : {}), ...(campos as Bruto) } as Bruto, m)[0];
            const muda = Object.fromEntries(Object.keys(campos as Bruto).map((c) => [c === "cor" || c === "degrade" ? "pintura" : c === "imagem" ? "src" : c, (refeita as unknown as Bruto)[c === "cor" || c === "degrade" ? "pintura" : c === "imagem" ? "src" : c]]));
            nova = { ...atual, ...(campos as Bruto), ...muda } as Peca;
            for (const c of ["cor", "degrade", "imagem"]) delete (nova as unknown as Bruto)[c];
          }
          if (atual.tipo === "qr" && (campos as Bruto).pix !== undefined) {
            nova = { ...nova, conteudo: conteudoDoQr(campos as Bruto) } as Peca;
            delete (nova as unknown as Bruto).pix;
          }
          if ((campos as Bruto).giro3d !== undefined) {
            nova = { ...nova, distorcao: distorcaoDoGiro((campos as Bruto).giro3d, nova.w, nova.h) } as Peca;
            delete (nova as unknown as Bruto).giro3d;
          }
          if ((campos as Bruto).animacao !== undefined) nova = { ...nova, animacao: (campos as Bruto).animacao === null ? undefined : animacaoDoAssistente({ ...(atual.animacao ?? {}), ...((campos as Bruto).animacao as Bruto) }) } as Peca;
          paginas[i] = { ...paginas[i], pecas: paginas[i].pecas.map((p) => (p.id === atual!.id ? nova : p)) };
          feitos.push(`peça ${atual!.id} alterada (${Object.keys(campos as Bruto).join(", ")})`);
          break;
        }
        case "remover_pecas": {
          const i = indiceDaPagina(paginas, o.pagina);
          const ids = new Set(Array.isArray(o.ids) ? (o.ids as unknown[]).map(String) : []);
          const antes = paginas[i].pecas.length;
          paginas[i] = { ...paginas[i], pecas: paginas[i].pecas.filter((p) => !ids.has(p.id)) };
          feitos.push(`${antes - paginas[i].pecas.length} peça(s) removida(s) da página ${i + 1}`);
          break;
        }
        case "ordenar_peca": {
          /* "para": "frente" | "tras" | "subir" | "descer" (a ordem é a das camadas: a última fica por cima). */
          const i = indiceDaPagina(paginas, o.pagina);
          const lista = [...paginas[i].pecas];
          const de = lista.findIndex((p) => p.id === o.id);
          if (de < 0) recusar(`A peça "${String(o.id)}" não está na página ${i + 1}.`);
          const [p] = lista.splice(de, 1);
          const para = o.para === "frente" ? lista.length : o.para === "tras" ? 0 : o.para === "subir" ? Math.min(lista.length, de + 1) : Math.max(0, de - 1);
          lista.splice(para, 0, p);
          paginas[i] = { ...paginas[i], pecas: lista };
          feitos.push(`peça ${p.id} foi para ${String(o.para)}`);
          break;
        }
        case "acrescentar_pagina": {
          const nova = paginaDoAssistente(ehObjeto(o.pagina_nova) ? o.pagina_nova : (o as Bruto), m);
          const depois = o.depois_de === undefined ? paginas.length : indiceDaPagina(paginas, o.depois_de) + 1;
          paginas.splice(depois, 0, nova);
          feitos.push(`página nova na posição ${depois + 1}`);
          break;
        }
        case "duplicar_pagina": {
          const i = indiceDaPagina(paginas, o.pagina);
          /* Ids novos, e quem aponta para eles (dentro de, quadros de) aponta para os novos. */
          const novos = new Map(paginas[i].pecas.map((p) => [p.id, novoId("p")]));
          const copia = documentoLimpo(
            JSON.parse(JSON.stringify({ ...paginas[i], audios: undefined }), (k, v) => ((k === "id" || k === "dentroDe" || k === "quadrosDe") && typeof v === "string" && novos.has(v) ? novos.get(v) : v)),
            m.largura,
            m.altura,
          );
          paginas.splice(i + 1, 0, copia);
          feitos.push(`página ${i + 1} duplicada (a cópia é a ${i + 2})`);
          break;
        }
        case "remover_pagina": {
          if (paginas.length < 2) recusar("A arte precisa de ao menos uma página.");
          const i = indiceDaPagina(paginas, o.pagina);
          paginas.splice(i, 1);
          feitos.push(`página ${i + 1} removida`);
          break;
        }
        case "mover_pagina": {
          const i = indiceDaPagina(paginas, o.pagina);
          const para = indiceDaPagina(paginas, o.para);
          const [p] = paginas.splice(i, 1);
          paginas.splice(para, 0, p);
          feitos.push(`página ${i + 1} foi para a posição ${para + 1}`);
          break;
        }
        case "alterar_pagina": {
          /* "fundo" (como em paginas), "duracao", "movimento", "transicao". */
          const i = indiceDaPagina(paginas, o.pagina);
          let p = paginas[i];
          if (o.fundo !== undefined) {
            const novo = documentoDoModelo(m.largura, m.altura, fundoDe(o.fundo), []);
            /* O fundo de antes (a forma "Fundo" do tamanho da página e a textura travada logo acima dela) sai. */
            const fundosVelhos = new Set(p.pecas.filter((x, k) => ehFundoDaArte(x, m.largura, m.altura) || (k <= 1 && x.tipo === "textura" && x.bloqueada && x.nome === "Textura")).map((x) => x.id));
            p = { ...p, pecas: [...novo.pecas, ...p.pecas.filter((x) => !fundosVelhos.has(x.id))] };
          }
          if (o.duracao !== undefined || o.movimento !== undefined) p = { ...p, animacao: { duracao: num(o.duracao, duracaoDaPagina(p)), fundo: (o.movimento as never) ?? p.animacao?.fundo ?? "parado" } };
          if (o.transicao !== undefined) p = { ...p, transicao: o.transicao === null || o.transicao === "corte" ? null : ((typeof o.transicao === "string" ? { tipo: o.transicao, duracao: 0.6 } : o.transicao) as never) };
          paginas[i] = p;
          feitos.push(`página ${i + 1} alterada`);
          break;
        }
        case "animar_pagina": {
          const i = indiceDaPagina(paginas, o.pagina);
          const estilo = (ESTILOS_DA_PAGINA.find((e) => e.chave === o.estilo)?.chave ?? "suave") as EstiloDaPagina;
          paginas[i] = animarPagina(paginas[i], estilo, (o.movimento as never) ?? "aproximar");
          feitos.push(`página ${i + 1} animada no estilo ${estilo}`);
          break;
        }
        case "tirar_animacao": {
          const i = indiceDaPagina(paginas, o.pagina);
          paginas[i] = { ...paginas[i], animacao: null, pecas: paginas[i].pecas.map((p) => ({ ...p, animacao: undefined }) as Peca) };
          feitos.push(`animação tirada da página ${i + 1}`);
          break;
        }
        case "trocar_audios": {
          const { total } = temposDoProjeto(paginas);
          audios = (Array.isArray(o.audios) ? o.audios : []).map((a) => audioDoAssistente(a as Bruto, total));
          feitos.push(`trilha de áudio trocada (${audios.length} áudio(s))`);
          break;
        }
        case "acrescentar_audio": {
          const { total } = temposDoProjeto(paginas);
          const a = audioDoAssistente(o, total);
          audios = [...audios, a];
          feitos.push(`áudio "${a.nome}" em ${a.inicio} s (id ${a.id})`);
          break;
        }
        case "remover_audio": {
          const antes = audios.length;
          audios = audios.filter((a) => a.id !== o.id);
          feitos.push(`${antes - audios.length} áudio(s) removido(s)`);
          break;
        }
        case "trocar_documento": {
          const novo = documentoDoAssistente(ehObjeto(o.documento) ? o.documento : recusar('Mande "documento" com { paginas, audios }.'), m);
          paginas = paginasDoDocumento(novo);
          audios = audiosDoProjeto(paginas);
          feitos.push(`arte inteira trocada (${paginas.length} página(s))`);
          break;
        }
        default:
          recusar(`Operação "${String(o.op)}" não existe. Use: ${OPERACOES.join(", ")}.`);
      }
    } catch (e) {
      throw new ErroNaArte(`${onde}: ${e instanceof Error ? e.message : e}`);
    }
  });
  return { doc: documentoLimpo(documentoDasPaginas(comAudios(paginas.map(comFontesComGlifos), audios)), m.largura, m.altura), feitos };
}

/* -------------------------------------------------- o que o assistente lê */

const r1 = (v: number) => Math.round(v * 10) / 10;
function resumoDaAnimacao(a: AnimacaoDaPeca | undefined) {
  if (!a) return undefined;
  return Object.fromEntries(
    Object.entries({
      entrada: a.entrada,
      inicio: a.inicio,
      duracao: a.entrada ? a.duracao : undefined,
      porPalavra: a.porPalavra,
      enfase: a.enfase,
      saida: a.saida,
      fim: a.fim,
      mover: a.mover,
      quadros: a.quadros?.length,
      contador: a.contador,
    }).filter(([, v]) => v !== undefined && v !== null && v !== false),
  );
}

/** A arte em resumo: cada página com as peças (id, tipo, nome, texto, caixa, animação) e a trilha de áudio. */
export function resumoDaArte(doc: DocumentoDoBanner) {
  const paginas = paginasDoDocumento(doc);
  const { inicios, total } = temposDoProjeto(paginas);
  return {
    largura: doc.largura,
    altura: doc.altura,
    duracao_total: r1(total),
    paginas: paginas.map((p, i) => ({
      pagina: i + 1,
      comeca_em: r1(inicios[i]),
      duracao: r1(duracaoDaPagina(p)),
      movimento_do_fundo: p.animacao?.fundo ?? "parado",
      transicao: p.transicao ?? null,
      pecas: p.pecas.map((x) => ({
        id: x.id,
        tipo: x.tipo,
        ...(x.nome ? { nome: x.nome } : {}),
        ...(x.tipo === "texto" ? { texto: x.texto.slice(0, 120), fonte: x.fonte, tamanho: r1(x.tamanho), cor: x.pintura.cor } : {}),
        ...(x.tipo === "imagem" ? { imagem: x.src.slice(-60), ...(x.moldura ? { moldura_vazia: true } : {}), ...(x.video ? { video: true } : {}) } : {}),
        ...(x.tipo === "forma" ? { forma: x.forma, cor: x.pintura.cor } : {}),
        ...(x.tipo === "icone" ? { icone: x.icone } : {}),
        ...(x.tipo === "textura" ? { estilo: x.estilo } : {}),
        caixa: [Math.round(x.x), Math.round(x.y), Math.round(x.w), Math.round(x.h)],
        ...(x.rot ? { rot: r1(x.rot) } : {}),
        ...(x.grupo ? { grupo: x.grupo } : {}),
        ...(x.bloqueada ? { travada: true } : {}),
        ...(x.animacao ? { animacao: resumoDaAnimacao(x.animacao) } : {}),
      })),
    })),
    audios: audiosDoProjeto(paginas).map((a) => ({ id: a.id, nome: a.nome, tipo: a.tipo, inicio: a.inicio, dura: a.dura, volume: a.volume })),
  };
}

/** Os textos da arte (os textos e o que vai nos QR), numa linha só por peça. */
export const textosDaArte = (doc: DocumentoDoBanner) =>
  paginasDoDocumento(doc)
    .flatMap((p) => p.pecas)
    .map((p) => (p.tipo === "texto" ? p.texto : p.tipo === "qr" ? p.conteudo : ""))
    .join("\n");

/**
 * A página parada no instante `t` (a foto da coreografia): cada peça no
 * lugar, tamanho, giro e transparência que a animação dá naquele segundo
 * (o número do contador no valor do instante). O 3D, as máscaras e o palavra
 * por palavra aparecem simplificados (a foto é para conferir o ritmo).
 */
export function poseNoInstante(pagina: DocumentoDoBanner, t: number): DocumentoDoBanner {
  const unidades = unidadesDaCena(pagina);
  const estados = estadosNoTempo(pagina, unidades, t);
  const mudas = new Map<string, Peca | null>();
  unidades.forEach((u, k) => {
    const e = estados[k];
    if (u.fundo || !e) return;
    const [cx, cy] = centroDa(u, pagina);
    const g = (e.giro * Math.PI) / 180;
    for (const id of u.ids) {
      const p = pagina.pecas.find((x) => x.id === id);
      if (!p) continue;
      const alfa = e.partes ? Math.max(...e.partes.map((x) => x.estado.alfa)) : e.alfa;
      if (alfa <= 0.02) {
        mudas.set(id, null);
        continue;
      }
      const [vx, vy] = [p.x + p.w / 2 - cx, p.y + p.h / 2 - cy];
      const [nx, ny] = [cx + e.dx + e.escala * (vx * Math.cos(g) - vy * Math.sin(g)), cy + e.dy + e.escala * (vx * Math.sin(g) + vy * Math.cos(g))];
      let nova = Math.abs(e.escala - 1) > 0.01 ? escalarPeca(p, e.escala) : p;
      nova = { ...nova, x: nx - nova.w / 2, y: ny - nova.h / 2, rot: p.rot + e.giro, opacidade: p.opacidade * Math.min(1, alfa) } as Peca;
      const c = u.animacao?.contador;
      if (c && nova.tipo === "texto") {
        const molde = numeroDoTexto(nova.texto);
        if (molde) nova = pecaNoValor(nova, valorNoTempo(c, molde, t));
      }
      mudas.set(id, nova);
    }
  });
  return { ...pagina, pecas: pagina.pecas.flatMap((p) => (mudas.has(p.id) ? (mudas.get(p.id) ? [mudas.get(p.id)!] : []) : [p])) };
}

/* --------------------------------------------- imagens que vêm de fora */

/* Os campos de imagem da arte (no jeito curto ou no completo) que ainda não estão no depósito. */
export function enderecosDeFora(v: unknown, noDeposito: (s: string) => boolean, achados = new Set<string>()): Set<string> {
  if (Array.isArray(v)) v.forEach((x) => enderecosDeFora(x, noDeposito, achados));
  else if (ehObjeto(v)) {
    for (const [k, x] of Object.entries(v)) {
      if ((k === "imagem" || (k === "src" && v.tipo === "imagem")) && typeof x === "string" && x && !noDeposito(x) && !/^data:/.test(x)) achados.add(x);
      else enderecosDeFora(x, noDeposito, achados);
    }
  }
  return achados;
}
/* Troca os endereços (o mesmo mapa vale no jeito curto e no completo). */
export function comEnderecos<T>(v: T, mapa: Map<string, string>): T {
  if (Array.isArray(v)) return v.map((x) => comEnderecos(x, mapa)) as T;
  if (ehObjeto(v)) return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, typeof x === "string" && (k === "imagem" || k === "src") && mapa.has(x) ? mapa.get(x) : comEnderecos(x, mapa)])) as T;
  return v;
}
/* "foto:<nome>", "figurinha:<chave>" e "recorte:<código>[/<variação>]": as fotos, figurinhas, recortes e ícones 3D da casa (já no depósito). */
export function enderecoDaCasa(v: string): string | null {
  if (/^recorte:/i.test(v)) {
    const [codigo, variacao] = v.slice(8).trim().toLowerCase().split("/");
    const c = codigo.replace(/[^a-z0-9-]/g, "");
    if (!c) return null;
    if (GRUPOS_DOS_ORNAMENTOS.some((g) => c.startsWith(`${g}-`))) return `${DEPOSITO_DA_BIBLIOTECA}/vetores/${c}.svg`;
    return c.startsWith("3d-") ? `${DEPOSITO_DA_BIBLIOTECA}/${c}/${(variacao || "dynamic_color").replace(/[^a-z_]/g, "")}.webp` : `${DEPOSITO_DA_BIBLIOTECA}/${c}.webp`;
  }
  if (/^foto:/i.test(v)) return `${DEPOSITO_DAS_FOTOS}/${v.slice(5).trim().replace(/[^a-z0-9-]/gi, "")}.webp`;
  if (/^figurinha:/i.test(v)) return `${DEPOSITO_DAS_FIGURINHAS}/${v.slice(10).trim().replace(/[^a-z0-9-]/gi, "")}.webp`;
  return null;
}

/** O pedido de um modelo pronto (a cor e o logo da loja, a foto se houver). */
export const pedidoDoModelo = (m: { largura: number; altura: number }, marca: { cor: string; logo: string | null }): PedidoDoModelo => ({ largura: m.largura, altura: m.altura, cor: marca.cor, logo: marca.logo, foto: null });
