"use client";

import { useId, useLayoutEffect, useMemo, useRef, useState } from "react";
import { PARADO, type EstadoNaCena } from "@/lib/banner-editor/animacao";
import { OBTURADOR, centroDa, comRastroDoMovimento, criarRelogio, cssDoEstado, desvioDoRastro, duracaoDaPagina, folgaDaParte, temRastro, estadosDaTransicao, estadosNoTempo, intervaloDoClipe, paginaNoTempo, transicaoNoTempo, unidadesDaCena, type RelogioDaCena, type UnidadeDaCena } from "@/lib/banner-editor/cena";
import { escreverComo, numeroDoTexto, valorNoTempo } from "@/lib/banner-editor/contador";
import { numeracaoDosPosts, velocidadeDo, type DocumentoDoBanner, type PecaDeImagem, type PecaDeTextura } from "@/lib/banner-editor/documento";
import { desenharParticulas, ehParticula, particulasNoTempo } from "@/lib/banner-editor/particulas";
import { exibicaoGuardada, htmlDoFundo } from "./desenho-guardado";
import { comPalavras } from "./palavras";

/*
 * O projeto tocando no palco (o play da linha do tempo): uma camada por cima
 * das peças com a cena do instante (e, na transição, a de antes parada no
 * último quadro). Cada unidade (o fundo, cada peça ou grupo) fica num quadro
 * do tamanho da página; o relógio chama a cada quadro e o estilo vai direto
 * nos quadros, sem passar pelo React. Só troca de cena re-renderiza.
 *
 * A peça de vídeo mostra a capa no editor e nas fotos; aqui a capa vira o
 * <video> de verdade, com o mesmo estilo, tocando junto com o relógio (parado
 * na linha do tempo, mostra o quadro do instante).
 */

type Montadas = { atual: number; antes: number | null };

/** O que o relógio pede: a cena do instante e a de antes durante a transição. */
function montadasNoTempo(paginas: DocumentoDoBanner[], t: number): Montadas {
  const { indice } = paginaNoTempo(paginas, t);
  const tr = transicaoNoTempo(paginas, t);
  return { atual: indice, antes: tr ? tr.de : null };
}

function PaginaNaCena({ pagina, indice, unidades, contagem }: { pagina: DocumentoDoBanner; indice: number; unidades: UnidadeDaCena[]; contagem: { pagina: number; total: number } }) {
  const raiz = useRef<HTMLDivElement | null>(null);
  const ordem = useMemo(() => new Map(pagina.pecas.map((p, i) => [p.id, i])), [pagina]);
  const porId = new Map(pagina.pecas.map((p) => [p.id, p]));

  /* A capa do vídeo vira o <video> (mesmas classes e estilo da imagem). Ao
     desmontar, a capa volta: a página muda de objeto sem o React redesenhar
     a peça (o desenho é guardado), e sem a capa de volta a vez seguinte não
     achava o <img>, ficava o <video> sem arquivo e só a capa aparecia. */
  useLayoutEffect(() => {
    const el = raiz.current;
    if (!el) return;
    const criados: Array<{ video: HTMLVideoElement; capa: HTMLImageElement }> = [];
    el.querySelectorAll<HTMLElement>("[data-peca-com-video]").forEach((caixa) => {
      const peca = porId.get(caixa.getAttribute("data-peca-com-video") || "") as PecaDeImagem | undefined;
      const capa = caixa.querySelector("img");
      if (!peca?.video || !capa) return;
      const video = document.createElement("video");
      video.className = capa.className;
      video.setAttribute("style", capa.getAttribute("style") || "");
      video.style.objectFit = peca.ajuste === "cobrir" ? "cover" : "contain";
      video.crossOrigin = "anonymous";
      video.muted = true;
      video.playsInline = true;
      video.preload = "auto";
      video.poster = peca.src;
      video.src = peca.video.src;
      video.setAttribute("data-video-da-peca", peca.id);
      capa.replaceWith(video);
      criados.push({ video, capa });
    });
    /* As partículas viram um <canvas> (desenhado a cada quadro em aplicarNaPagina). */
    el.querySelectorAll<HTMLElement>("[data-peca-particulas]").forEach((caixa) => {
      const peca = porId.get(caixa.getAttribute("data-peca-particulas") || "");
      if (!peca) return;
      const tela = document.createElement("canvas");
      const k = Math.min(1, 1600 / Math.max(peca.w, peca.h));
      tela.width = Math.max(1, Math.round(peca.w * k));
      tela.height = Math.max(1, Math.round(peca.h * k));
      tela.style.cssText = "position:absolute;inset:0;width:100%;height:100%";
      tela.setAttribute("data-tela-das-particulas", peca.id);
      caixa.replaceChildren(tela);
    });
    return () =>
      criados.forEach(({ video, capa }) => {
        video.pause();
        video.removeAttribute("src");
        video.load();
        if (video.isConnected) video.replaceWith(capa);
      });
    // eslint-disable-next-line react-hooks/exhaustive-deps -- porId sai da própria página
  }, [pagina]);

  return (
    <div ref={raiz} data-pagina-da-cena={indice} className="absolute left-0 top-0 overflow-hidden bg-white" style={{ width: pagina.largura, height: pagina.altura, transformOrigin: "0 0" }}>
      {unidades.map((u, k) => (
        <div key={u.ids.join(",") || "fundo"} data-cena-unidade={k} className="absolute inset-0" style={{ willChange: "transform, opacity" }}>
          <div data-parte-base className="absolute inset-0">
          {u.fundo ? <div className="absolute inset-0" dangerouslySetInnerHTML={htmlDoFundo(pagina.fundo, pagina.largura, pagina.altura)} /> : null}
          {[...u.ids]
            .sort((a, b) => (ordem.get(a) ?? 0) - (ordem.get(b) ?? 0))
            .map((id) => {
              const p = porId.get(id);
              const ex = p ? exibicaoGuardada(p, pagina.pecas, contagem) : null;
              if (!p || !ex) return null;
              const comVideo = p.tipo === "imagem" && p.video ? { "data-peca-com-video": p.id } : {};
              const comParticulas = p.tipo === "textura" && ehParticula(p.estilo) ? { "data-peca-particulas": p.id } : {};
              return <div key={id} style={{ position: "absolute", ...ex.estilo }} dangerouslySetInnerHTML={ex.html} {...comVideo} {...comParticulas} />;
            })}
          </div>
        </div>
      ))}
    </div>
  );
}

/*
 * O rastro do movimento no palco: um filtro SVG por camada que anda, com o
 * borrão só na direção do movimento (o vídeo faz o mesmo com cópias).
 */
type Rastros = (chave: string, e: EstadoNaCena) => string | undefined;
function criarRastros(svg: SVGSVGElement | null, prefixo: string): Rastros {
  return (chave, e) => {
    if (!svg || !temRastro(e)) return undefined;
    const id = `${prefixo}-${chave}`;
    let filtro = svg.querySelector<SVGFilterElement>(`[id="${id}"]`);
    if (!filtro) {
      filtro = document.createElementNS("http://www.w3.org/2000/svg", "filter");
      filtro.id = id;
      for (const [k, v] of Object.entries({ x: "-40%", y: "-40%", width: "180%", height: "180%", "color-interpolation-filters": "sRGB" })) filtro.setAttribute(k, v);
      filtro.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "feGaussianBlur"));
      svg.appendChild(filtro);
    }
    const [sx, sy] = desvioDoRastro(e);
    filtro.firstElementChild?.setAttribute("stdDeviation", `${sx.toFixed(1)} ${sy.toFixed(1)}`);
    return id;
  };
}

const poligonoDaCaixa = (c: { x: number; y: number; w: number; h: number }) => `polygon(${c.x}px ${c.y}px, ${c.x + c.w}px ${c.y}px, ${c.x + c.w}px ${c.y + c.h}px, ${c.x}px ${c.y + c.h}px)`;

/* Palavra por palavra: a unidade vira uma cópia por palavra (recortada nela, no estado dela); terminada a entrada, volta a ser uma só. */
function aplicarNaUnidade(q: HTMLElement, k: number, u: UnidadeDaCena, estado: EstadoNaCena, pagina: DocumentoDoBanner, rastros: Rastros) {
  const base = q.querySelector<HTMLElement>(":scope > [data-parte-base]");
  const copias = [...q.querySelectorAll<HTMLElement>(":scope > [data-parte-copia]")];
  Object.assign(q.style, cssDoEstado({ ...estado, partes: null }, centroDa(u, pagina), u.caixa, rastros(String(k), estado)));
  if (!estado.partes || !base) {
    if (base) base.style.visibility = "";
    copias.forEach((c) => c.remove());
    return;
  }
  while (copias.length < estado.partes.length) {
    const c = base.cloneNode(true) as HTMLElement;
    c.removeAttribute("data-parte-base");
    c.setAttribute("data-parte-copia", "");
    q.appendChild(c);
    copias.push(c);
  }
  base.style.visibility = "hidden";
  estado.partes.forEach((parte, i) => {
    const folga = folgaDaParte(parte.caixa);
    const css = cssDoEstado(parte.estado, [parte.caixa.x + parte.caixa.w / 2, parte.caixa.y + parte.caixa.h / 2], folga, rastros(`${k}-${i}`, parte.estado));
    Object.assign(copias[i].style, css, { clipPath: css.clipPath || poligonoDaCaixa(folga), display: "" });
  });
  copias.slice(estado.partes.length).forEach((c) => (c.style.display = "none"));
}

/* Põe a cena `pagina` no instante `local` (cada unidade e cada vídeo). */
/* O número que sobe: o primeiro número do texto (no DOM da unidade) mostra o valor do instante; o texto original fica guardado no nó. */
const textoOriginal = new WeakMap<Text, string>();
function aplicarContador(q: HTMLElement, u: UnidadeDaCena, pagina: DocumentoDoBanner, local: number) {
  const c = u.animacao?.contador;
  const peca = u.ids.length === 1 ? pagina.pecas.find((p) => p.id === u.ids[0]) : null;
  if (!c || !peca || peca.tipo !== "texto") return;
  const molde = numeroDoTexto(peca.texto);
  if (!molde) return;
  const escrito = escreverComo(valorNoTempo(c, molde, local), molde);
  /* Em cada cópia (palavra por palavra) e no original. */
  q.querySelectorAll<HTMLElement>(":scope > [data-parte-base], :scope > [data-parte-copia]").forEach((camada) => {
    const andador = document.createTreeWalker(camada, NodeFilter.SHOW_TEXT);
    for (let no = andador.nextNode() as Text | null; no; no = andador.nextNode() as Text | null) {
      const original = textoOriginal.get(no) ?? no.data;
      if (!/\d/.test(original)) continue;
      if (!textoOriginal.has(no)) textoOriginal.set(no, original);
      const novo = original.replace(/\d{1,3}(?:\.\d{3})+(?:,\d+)?|\d+(?:,\d+)?/, escrito);
      if (no.data !== novo) no.data = novo;
      break;
    }
  });
}

/* As partículas do instante no <canvas> delas. */
function aplicarParticulas(el: HTMLElement, pagina: DocumentoDoBanner, local: number) {
  el.querySelectorAll<HTMLCanvasElement>("canvas[data-tela-das-particulas]").forEach((tela) => {
    const p = pagina.pecas.find((x) => x.id === tela.getAttribute("data-tela-das-particulas")) as PecaDeTextura | undefined;
    const ctx = tela.getContext("2d");
    if (!p || !ctx || !ehParticula(p.estilo)) return;
    ctx.setTransform(tela.width / p.w, 0, 0, tela.height / p.h, 0, 0);
    ctx.clearRect(0, 0, p.w, p.h);
    desenharParticulas(ctx, particulasNoTempo({ ...p, estilo: p.estilo }, local));
    tela.setAttribute("data-instante", local.toFixed(2));
  });
}

function aplicarNaPagina(el: HTMLElement, pagina: DocumentoDoBanner, unidades: UnidadeDaCena[], local: number, tocando: boolean, rastros: Rastros) {
  /* Quem anda rápido ganha o borrão do movimento (o que andou no último meio quadro). */
  const estados = comRastroDoMovimento(estadosNoTempo(pagina, unidades, local), local > OBTURADOR ? estadosNoTempo(pagina, unidades, local - OBTURADOR) : []);
  el.querySelectorAll<HTMLElement>(":scope > [data-cena-unidade]").forEach((q) => {
    const k = Number(q.getAttribute("data-cena-unidade"));
    const u = unidades[k];
    if (u && estados[k]) aplicarNaUnidade(q, k, u, estados[k], pagina, rastros);
    if (u?.animacao?.contador) aplicarContador(q, u, pagina, local);
  });
  aplicarParticulas(el, pagina, local);
  const duracao = duracaoDaPagina(pagina);
  el.querySelectorAll<HTMLVideoElement>("video[data-video-da-peca]").forEach((v) => {
    const p = pagina.pecas.find((x) => x.id === v.getAttribute("data-video-da-peca"));
    if (!p || p.tipo !== "imagem" || !p.video) return;
    const { comeco, fim } = intervaloDoClipe(p.animacao, duracao);
    const vel = velocidadeDo(p.video);
    const quer = Math.min(Math.max(0, p.video.duracao - 0.05), p.video.corte + Math.max(0, local - comeco) * vel);
    const naTela = local >= comeco && local < fim;
    if (tocando && naTela && vel > 0) {
      v.muted = p.video.mudo;
      v.volume = p.video.volume;
      if (v.playbackRate !== vel) v.playbackRate = vel;
      if (v.paused) {
        v.currentTime = quer;
        void v.play().catch(() => {
          /* Sem permissão de som: toca mudo. */
          v.muted = true;
          void v.play().catch(() => undefined);
        });
      } else if (Math.abs(v.currentTime - quer) > 0.3) v.currentTime = quer;
    } else {
      if (!v.paused) v.pause();
      if (Math.abs(v.currentTime - quer) > 0.04) v.currentTime = quer;
    }
  });
}

export function CenaNoPalco({ paginas, relogio, largura, altura }: { paginas: DocumentoDoBanner[]; relogio: RelogioDaCena; largura: number; altura: number }) {
  const [montadas, setMontadas] = useState<Montadas>(() => montadasNoTempo(paginas, relogio.tempo()));
  const raiz = useRef<HTMLDivElement | null>(null);
  const filtros = useRef<SVGSVGElement | null>(null);
  const prefixo = `rastro${useId().replace(/[^a-zA-Z0-9]/g, "")}`;
  const unidades = useMemo(() => paginas.map((p) => comPalavras(p, unidadesDaCena(p))), [paginas]);
  const contagens = useMemo(() => numeracaoDosPosts(paginas), [paginas]);
  useLayoutEffect(() => {
    /* Página de outro tamanho cabe inteira no palco. */
    const encaixe = (p: DocumentoDoBanner) => {
      const f = Math.min(largura / p.largura, altura / p.altura);
      return { f, x: (largura - p.largura * f) / 2, y: (altura - p.altura * f) / 2 };
    };
    const el = raiz.current;
    if (!el) return;
    const quadroDa = (k: number | null) => (k === null ? null : el.querySelector<HTMLElement>(`[data-pagina-da-cena="${k}"]`));
    const atual = quadroDa(montadas.atual);
    const antes = quadroDa(montadas.antes);
    const clarao = el.querySelector<HTMLElement>("[data-clarao]");
    const rastrosDa = (k: number) => criarRastros(filtros.current, `${prefixo}-${k}`);
    return relogio.assinar((t) => {
      const quer = montadasNoTempo(paginas, t);
      if (quer.atual !== montadas.atual || quer.antes !== montadas.antes) {
        setMontadas(quer);
        return;
      }
      const { indice, local } = paginaNoTempo(paginas, t);
      const pagina = paginas[indice];
      if (!pagina || !atual) return;
      const tr = transicaoNoTempo(paginas, t);
      const estados = tr ? estadosDaTransicao(tr.tipo, tr.fracao, largura) : { antes: PARADO, depois: PARADO, clarao: 0, corDoClarao: "#FFFFFF" };
      const caixa = { x: 0, y: 0, w: largura, h: altura };
      const [cx, cy] = [largura / 2, altura / 2];
      /* A cena no palco: encaixada (página de outro tamanho) e com o movimento da transição, que cresce do centro. */
      const posicionar = (q: HTMLElement, p: DocumentoDoBanner, estado: typeof PARADO, k: number) => {
        const e = encaixe(p);
        const css = cssDoEstado(estado, [cx, cy], caixa, criarRastros(filtros.current, `${prefixo}-cena`)(String(k), estado));
        Object.assign(q.style, {
          opacity: css.opacity,
          clipPath: css.clipPath,
          filter: css.filter,
          transformOrigin: "0 0",
          transform: `translate(${estado.dx}px, ${estado.dy}px) translate(${cx}px, ${cy}px) scale(${estado.escala}) translate(${-cx}px, ${-cy}px) translate(${e.x}px, ${e.y}px) scale(${e.f})`,
        });
      };
      aplicarNaPagina(atual, pagina, unidades[indice], local, relogio.tocando, rastrosDa(indice));
      posicionar(atual, pagina, estados.depois, indice);
      if (antes && tr) {
        const anterior = paginas[tr.de];
        aplicarNaPagina(antes, anterior, unidades[tr.de], duracaoDaPagina(anterior), false, rastrosDa(tr.de));
        posicionar(antes, anterior, estados.antes, tr.de);
      }
      if (clarao) {
        clarao.style.opacity = String(estados.clarao);
        clarao.style.background = estados.corDoClarao;
      }
      /* Atrás da cena que vira em 3D, preto (como no MP4). */
      el.style.background = tr?.tipo === "virar" ? "#000000" : "";
    });
  }, [paginas, unidades, relogio, montadas, largura, altura, prefixo]);

  const lista = montadas.antes !== null ? [montadas.antes, montadas.atual] : [montadas.atual];
  return (
    <div ref={raiz} data-cena className="pointer-events-none absolute left-0 top-0 z-[5] overflow-hidden bg-white" style={{ width: largura, height: altura }}>
      {lista.map((k) =>
        paginas[k] ? <PaginaNaCena key={`${k}-${paginas[k].pecas.length}`} pagina={paginas[k]} indice={k} unidades={unidades[k]} contagem={contagens[k] ?? { pagina: k + 1, total: paginas.length }} /> : null,
      )}
      <div data-clarao className="absolute inset-0 bg-white" style={{ opacity: 0 }} />
      <svg ref={filtros} width="0" height="0" className="absolute" aria-hidden="true" />
    </div>
  );
}

/**
 * A cena pequena dos quadradinhos de efeito: uma página (ou só algumas peças
 * dela, recortada em volta delas) tocando num relógio próprio, enquanto o
 * mouse está em cima (ou sempre, `sempre`).
 */
export function MiniCena({ paginas, lado, alto = lado, recorte, tocar }: { paginas: DocumentoDoBanner[]; lado: number; alto?: number; recorte?: { x: number; y: number; w: number; h: number } | null; tocar: boolean }) {
  const relogio = useMemo(() => criarRelogio(), []);
  const total = paginas.reduce((s, p) => s + duracaoDaPagina(p), 0);
  useLayoutEffect(() => {
    if (!tocar) {
      relogio.mudar(total);
      return;
    }
    const comeco = performance.now();
    let quadro = 0;
    const volta = Math.min(total, 4) + 0.6;
    const passo = () => {
      const t = ((performance.now() - comeco) / 1000) % volta;
      relogio.mudar(Math.min(t, total));
      quadro = requestAnimationFrame(passo);
    };
    quadro = requestAnimationFrame(passo);
    return () => cancelAnimationFrame(quadro);
  }, [tocar, relogio, total]);
  const primeira = paginas[0];
  if (!primeira) return null;
  const area = recorte ?? { x: 0, y: 0, w: primeira.largura, h: primeira.altura };
  const f = Math.min(lado / area.w, alto / area.h) * (recorte ? 0.82 : 1);
  return (
    <span className="relative block overflow-hidden" style={{ width: lado, height: alto }}>
      <span className="absolute left-1/2 top-1/2" style={{ width: primeira.largura, height: primeira.altura, transformOrigin: "0 0", transform: `scale(${f}) translate(${-area.x - area.w / 2}px, ${-area.y - area.h / 2}px)` }}>
        <CenaNoPalco paginas={paginas} relogio={relogio} largura={primeira.largura} altura={primeira.altura} />
      </span>
    </span>
  );
}
