/* kit/personagem.jsx — o vídeo GRAVADO em 3D: o set, os personagens e a câmera.
 *
 * {"tipo":"kit","peca":"personagem","partitura":{…},"largura":1920,"altura":1080}
 *
 * A partitura vem pronta do lib/estudio/personagem.js (quem fala quando, os
 * planos, a aparência, o cenário). Aqui só se desenha, quadro a quadro:
 *  - a boca segue a voz LETRA POR LETRA (a vogal abre, o "m" fecha, o "o" arredonda);
 *  - piscada, sobrancelha, sorriso e cabeça que vira para quem fala;
 *  - gesto de mão nas palavras fortes;
 *  - a câmera corta entre aberto, ombro, close e detalhe, com o balanço de mão;
 *  - por cima: legenda grossa palavra por palavra, o nome de quem fala, o
 *    cartão de destaque, o REC e o grão de filme.
 * Tudo é geometria do three.js: nada é baixado. */
import React, { useMemo } from 'react';
import { ThreeCanvas } from '@remotion/three';
import * as THREE from 'three';
import { useThree } from '@react-three/fiber';
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';

const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
const lerp = (a, b, k) => a + (b - a) * k;
const suave = (x) => { const k = clamp(x); return k * k * (3 - 2 * k); };
const mola = (x) => { const k = clamp(x); return 1 - Math.pow(1 - k, 3) * Math.cos(k * 4.2); };
const hash = (n) => { const s = Math.sin(n * 127.1 + 311.7) * 43758.5453; return s - Math.floor(s); };

function Ambiente() {
  const { gl, scene } = useThree();
  useMemo(() => {
    const pm = new THREE.PMREMGenerator(gl);
    scene.environment = pm.fromScene(new RoomEnvironment(), 0.04).texture;
    scene.environmentIntensity = 0.35;
    pm.dispose();
  }, [gl, scene]);
  return null;
}

function Camera({ pos, alvo, fov }) {
  const { camera } = useThree();
  camera.position.set(pos[0], pos[1], pos[2]);
  camera.fov = fov;
  camera.near = 0.05;
  camera.far = 60;
  camera.lookAt(alvo[0], alvo[1], alvo[2]);
  camera.updateProjectionMatrix();
  return null;
}

const Mat = ({ c, r = 0.6, m = 0, e, ei = 1, sheen, cc = 0 }) => (
  <meshPhysicalMaterial color={c} roughness={r} metalness={m} clearcoat={cc} clearcoatRoughness={0.3}
    {...(sheen ? { sheen: 0.6, sheenColor: sheen, sheenRoughness: 0.5 } : {})}
    {...(e ? { emissive: e, emissiveIntensity: ei } : {})} />
);

/* ── A BOCA ───────────────────────────────────────────────────────────────
   Cada letra vira um formato: a = abre (quanto) e w = largura (o "i" estica,
   o "o" e o "u" arredondam, m/b/p fecham). Dentro da letra a boca respira
   (fecha um pouco entre uma e outra), e três amostras seguidas suavizam. */
const VISEMA = (ch) => {
  if (/[aáàãâ]/.test(ch)) return [1, 0.6];
  if (/[eéê]/.test(ch)) return [0.6, 0.85];
  if (/[iíy]/.test(ch)) return [0.32, 1];
  if (/[oóôõ]/.test(ch)) return [0.78, 0.3];
  if (/[uúw]/.test(ch)) return [0.45, 0.15];
  if (/[mbp]/.test(ch)) return [0, 0.5];
  if (/[fv]/.test(ch)) return [0.12, 0.62];
  if (/[lnrdt]/.test(ch)) return [0.3, 0.6];
  return [0.22, 0.55];
};

function bocaEm(falas, quem, t) {
  for (const f of falas) {
    if (f.quem !== quem || t < f.de - 0.05 || t > f.ate + 0.05) continue;
    const w = f.palavras.find((x) => t >= x.de && t <= x.ate);
    if (!w) return [0, 0.55];
    const letras = String(w.t).toLowerCase().replace(/[^a-záàãâéêíóôõúçy]/g, '') || 'a';
    const p = clamp((t - w.de) / Math.max(0.05, w.ate - w.de), 0, 0.999);
    const pos = p * letras.length;
    const [a, l] = VISEMA(letras[Math.floor(pos)]);
    return [a * (0.62 + 0.38 * Math.sin(Math.PI * (pos % 1))), l];
  }
  return [0, 0.55];
}

function boca(falas, quem, t) {
  const amostras = [0, 1 / 60, 1 / 30].map((d) => bocaEm(falas, quem, t - d));
  return [amostras.reduce((s, x) => s + x[0], 0) / 3, amostras.reduce((s, x) => s + x[1], 0) / 3];
}

// O gesto: uma onda em cada palavra comprida (as fortes), alternando a mão.
function gesto(falas, quem, t) {
  let esq = 0, dir = 0;
  for (const f of falas) {
    if (f.quem !== quem || t < f.de - 0.5 || t > f.ate + 1) continue;
    f.palavras.forEach((w, k) => {
      if (String(w.t).replace(/[^\p{L}\d]/gu, '').length < 6 && !/\d/.test(w.t)) return;
      const x = (t - (w.de - 0.15)) / 0.9;
      if (x < 0 || x > 1) return;
      const b = Math.sin(Math.PI * x);
      if (k % 2) esq = Math.max(esq, b); else dir = Math.max(dir, b);
    });
  }
  return [esq, dir];
}

function piscada(quem, t) {
  // A cada 2,5 a 5 s, uma piscada de 0,16 s (às vezes dupla).
  let base = 0.6 + quem * 1.3, k = 0;
  while (base < t + 1) {
    const d = t - base;
    if (d >= 0 && d < 0.16) return Math.sin((d / 0.16) * Math.PI);
    if (hash(quem * 31 + k) < 0.2) { const d2 = t - (base + 0.3); if (d2 >= 0 && d2 < 0.16) return Math.sin((d2 / 0.16) * Math.PI); }
    base += 2.5 + hash(quem * 17 + k) * 2.5;
    k++;
  }
  return 0;
}

const EMO = {
  neutro: { sobr: 0, sorri: 0.15, palp: 0 },
  feliz: { sobr: 0.25, sorri: 0.9, palp: 0.1 },
  animado: { sobr: 0.55, sorri: 0.7, palp: 0 },
  serio: { sobr: -0.35, sorri: 0, palp: 0.3 },
  surpreso: { sobr: 0.9, sorri: 0, palp: -0.2 },
  pensativo: { sobr: -0.1, sorri: 0.05, palp: 0.35, inclina: 0.12 },
  curioso: { sobr: 0.45, sorri: 0.3, palp: 0, inclina: 0.1 },
};

/* ── A PESSOA ────────────────────────────────────────────────────────────── */
function Cabelo({ estilo, cor }) {
  const m = <Mat c={cor} r={0.85} sheen="#ffffff" />;
  const cap = (
    <mesh position={[0, 0.03, -0.02]} scale={[0.97, 1.02, 1.0]} rotation={[-0.3, 0, 0]}>
      <sphereGeometry args={[0.445, 48, 32, 0, Math.PI * 2, 0, Math.PI * 0.45]} />{m}
    </mesh>
  );
  if (estilo === 'careca') return null;
  if (estilo === 'cacheado') {
    const pts = [];
    for (let i = 0; i < 150; i++) {
      const y = 1 - (i / 149) * 2, r = Math.sqrt(Math.max(0, 1 - y * y)), a = i * 2.39996;
      const z = Math.sin(a) * r;
      if (y < 0.38 + 0.36 * z) continue; // a testa e o rosto ficam livres; atrás desce mais
      pts.push([Math.cos(a) * r * 0.44, y * 0.44 + 0.03, z * 0.44 - 0.02]);
    }
    return <group>{cap}{pts.map((p, i) => <mesh key={i} position={p}><sphereGeometry args={[0.068 + hash(i) * 0.022, 14, 10]} />{m}</mesh>)}</group>;
  }
  return (
    <group>
      {cap}
      {estilo === 'topete' && <mesh position={[0.02, 0.4, 0.2]} rotation={[0.5, 0, 0.2]} scale={[1.5, 0.7, 1.1]}><sphereGeometry args={[0.15, 24, 16]} />{m}</mesh>}
      {estilo === 'coque' && <mesh position={[0, 0.42, -0.22]}><sphereGeometry args={[0.15, 24, 16]} />{m}</mesh>}
      {estilo === 'longo' && (
        <>
          <mesh position={[0, -0.28, -0.2]} scale={[1.05, 1, 0.55]}><capsuleGeometry args={[0.38, 0.55, 8, 24]} />{m}</mesh>
          {[-1, 1].map((s) => <mesh key={s} position={[s * 0.38, -0.12, 0.06]} scale={[0.5, 1, 0.6]}><capsuleGeometry args={[0.12, 0.45, 8, 16]} />{m}</mesh>)}
        </>
      )}
    </group>
  );
}

function Olho({ x, olhar, pisca, palp, corIris, pele }) {
  // A pálpebra: meia esfera que gira por cima do olho (aberta = -0,15; fechada = 1,5).
  const a = lerp(-0.5 + palp * 0.7, 1.5, clamp(pisca));
  return (
    <group position={[x, 0.08, 0.335]}>
      <mesh><sphereGeometry args={[0.083, 32, 24]} /><Mat c="#f7f5f2" r={0.15} cc={1} /></mesh>
      <group rotation={[olhar[1], olhar[0], 0]}>
        <mesh position={[0, 0, 0.058]}><sphereGeometry args={[0.042, 24, 16]} /><Mat c={corIris} r={0.2} cc={1} /></mesh>
        <mesh position={[0, 0, 0.083]}><sphereGeometry args={[0.022, 16, 12]} /><Mat c="#050505" r={0.1} /></mesh>
        <mesh position={[0.018, 0.02, 0.098]}><sphereGeometry args={[0.009, 8, 8]} /><meshBasicMaterial color="#ffffff" /></mesh>
      </group>
      <mesh rotation={[a, 0, 0]}><sphereGeometry args={[0.091, 32, 16, 0, Math.PI * 2, 0, Math.PI / 2]} /><Mat c={pele} r={0.6} sheen="#ff9a8a" /></mesh>
    </group>
  );
}

function Rosto({ p, est }) {
  const pele = p.pele;
  const labio = new THREE.Color(pele).lerp(new THREE.Color('#8a3b3b'), 0.35).getStyle();
  const e = EMO[est.emocao] || EMO.neutro;
  const sobr = e.sobr + est.enfase * 0.35;
  const [abre, larg] = est.boca;
  const sorri = e.sorri * (1 - abre * 0.6);
  const corIris = new THREE.Color(p.corCabelo).lerp(new THREE.Color('#5a3a22'), 0.5).getStyle();
  const olho = (x) => <Olho key={x} x={x} olhar={est.olhar} pisca={est.pisca} palp={e.palp} corIris={corIris} pele={pele} />;
  return (
    <group>
      <mesh scale={[0.92, 1.02, 0.95]}><sphereGeometry args={[0.42, 64, 48]} /><Mat c={pele} r={0.55} sheen="#ff9a8a" /></mesh>
      {[-1, 1].map((s) => <mesh key={s} position={[s * 0.385, -0.01, -0.02]} scale={[0.45, 1, 0.75]}><sphereGeometry args={[0.09, 24, 16]} /><Mat c={pele} r={0.6} sheen="#ff9a8a" /></mesh>)}
      <mesh position={[0, -0.04, 0.4]} scale={[0.85, 1.1, 1]}><sphereGeometry args={[0.062, 24, 16]} /><Mat c={pele} r={0.5} sheen="#ff9a8a" /></mesh>
      {olho(-0.14)}{olho(0.14)}
      {[-1, 1].map((s) => (
        <mesh key={s} position={[s * 0.145, 0.205 + sobr * 0.035 + (s > 0 && e.inclina ? e.inclina * 0.1 : 0), 0.37]}
          rotation={[0, 0, Math.PI / 2 + s * (-0.12 + (est.emocao === 'serio' ? 0.25 : 0) - sobr * 0.12)]}>
          <capsuleGeometry args={[0.019, 0.1, 6, 12]} /><Mat c={p.corCabelo} r={0.9} />
        </mesh>
      ))}
      <group position={[0, -0.175, 0.37]} rotation={[0.42, 0, 0]}>
        <mesh scale={[0.06 + 0.035 * larg, 0.008 + 0.06 * abre, 1]}><circleGeometry args={[1, 32]} /><meshBasicMaterial color="#2a0d10" /></mesh>
        {abre > 0.25 && <mesh position={[0, 0.025 + 0.02 * abre, 0.002]} scale={[0.05 + 0.025 * larg, 0.012, 1]}><circleGeometry args={[1, 24]} /><meshBasicMaterial color="#f3efe9" /></mesh>}
        <mesh scale={[0.062 + 0.036 * larg, 0.012 + 0.062 * abre, 0.04]}><torusGeometry args={[1, 0.15, 10, 40]} /><Mat c={labio} r={0.45} /></mesh>
        {sorri > 0.2 && (
          <mesh position={[0, 0.01, 0.004]} rotation={[0, 0, Math.PI]} scale={[0.09, 0.04 * sorri, 0.04]}>
            <torusGeometry args={[1, 0.16, 8, 32, Math.PI]} /><Mat c={labio} r={0.45} />
          </mesh>
        )}
      </group>
      {p.barba && <mesh scale={[0.95, 1.03, 0.97]}><sphereGeometry args={[0.43, 48, 24, 0, Math.PI * 2, Math.PI * 0.69, Math.PI * 0.31]} /><Mat c={p.corCabelo} r={0.95} /></mesh>}
      {p.oculos && (
        <group position={[0, 0.08, 0.425]}>
          {[-1, 1].map((s) => <mesh key={s} position={[s * 0.14, 0, 0]}><torusGeometry args={[0.095, 0.011, 10, 40]} /><Mat c="#15161a" r={0.3} m={0.5} /></mesh>)}
          <mesh rotation={[0, 0, Math.PI / 2]}><cylinderGeometry args={[0.008, 0.008, 0.09, 8]} /><Mat c="#15161a" r={0.3} m={0.5} /></mesh>
        </group>
      )}
      <group position={[0, 0, 0]}><Cabelo estilo={p.cabelo} cor={p.corCabelo} /></group>
    </group>
  );
}

/* ── O ROBÔ ──────────────────────────────────────────────────────────────
   Cabeça branca, o visor escuro de vidro (uma segunda elipse que fura a
   frente), os olhos acesos que piscam e a boca de barras que dança com a voz. */
function RostoRobo({ p, est, acento }) {
  const [abre] = est.boca;
  const e = EMO[est.emocao] || EMO.neutro;
  const olhoY = 1 - clamp(est.pisca) * 0.88;
  const feliz = e.sorri > 0.5 && abre < 0.2;
  return (
    <group>
      <mesh scale={[1.1, 0.9, 0.95]}><sphereGeometry args={[0.42, 64, 48]} /><Mat c={p.cor} r={0.25} m={0.1} cc={1} /></mesh>
      <mesh position={[0, 0, 0.04]} scale={[1.0, 0.75, 0.95]}><sphereGeometry args={[0.4, 64, 48]} /><Mat c="#07090d" r={0.08} m={0.4} cc={1} /></mesh>
      {[-1, 1].map((s) => (
        <group key={s} position={[s * 0.13, 0.05 + e.sobr * 0.02, 0.405]} rotation={[0, s * 0.18, 0]}>
          <mesh scale={[1, olhoY * (feliz ? 0.55 : 1), 1]} rotation={[0, 0, Math.PI / 2 * (feliz ? 1 : 0)]}>
            <capsuleGeometry args={[0.038, feliz ? 0.07 : 0.05, 6, 16]} /><meshBasicMaterial color={acento} />
          </mesh>
        </group>
      ))}
      <group position={[0, -0.1, 0.392]}>
        {[-2, -1, 0, 1, 2].map((k) => {
          const h = 0.012 + abre * 0.085 * (1 - Math.abs(k) * 0.18) * (0.75 + 0.25 * Math.sin(est.t * 23 + k * 1.7));
          return <mesh key={k} position={[k * 0.034, 0, -Math.abs(k) * 0.006]}><boxGeometry args={[0.02, h, 0.02]} /><meshBasicMaterial color={acento} /></mesh>;
        })}
      </group>
      {[-1, 1].map((s) => (
        <group key={s} position={[s * 0.47, 0, 0]} rotation={[0, 0, Math.PI / 2]}>
          <mesh><cylinderGeometry args={[0.11, 0.11, 0.07, 32]} /><Mat c={p.cor} r={0.3} m={0.2} cc={1} /></mesh>
          <mesh position={[0, s * 0.037, 0]}><torusGeometry args={[0.075, 0.012, 8, 32]} /><meshBasicMaterial color={acento} /></mesh>
        </group>
      ))}
      <mesh position={[0, 0.42, 0]}><cylinderGeometry args={[0.012, 0.012, 0.16, 8]} /><Mat c="#9aa0aa" r={0.3} m={0.8} /></mesh>
      <mesh position={[0, 0.51, 0]}><sphereGeometry args={[0.035, 16, 12]} /><Mat c={acento} e={acento} ei={1 + (est.falando ? 1.5 + Math.sin(est.t * 9) : 0)} /></mesh>
    </group>
  );
}

/* ── O CORPO, OS BRAÇOS E O PERSONAGEM INTEIRO ───────────────────────────── */
function Braco({ lado, g, cor, pele, robo, t }) {
  // Repouso: o antebraço deitado na mesa. Gesto: a mão sobe e mexe um pouco.
  const ombroX = lerp(-0.3, -0.62, g) + Math.sin(t * 6 + lado) * 0.05 * g;
  const ombroZ = lado * lerp(0.08, 0.28, g);
  const cotovelo = lerp(-1.17, -1.75, g) + Math.sin(t * 7.3 + lado * 2) * 0.12 * g;
  const pulso = Math.sin(t * 9 + lado) * 0.25 * g;
  const tecido = robo ? <Mat c={cor} r={0.3} m={0.1} cc={1} /> : <Mat c={cor} r={0.85} sheen="#ffffff" />;
  return (
    <group position={[lado * 0.44, 1.18, 0]} rotation={[ombroX, 0, ombroZ]}>
      <mesh position={[0, -0.16, 0]}><capsuleGeometry args={[0.095, 0.24, 8, 16]} />{tecido}</mesh>
      <group position={[0, -0.32, 0]} rotation={[cotovelo, 0, 0]}>
        <mesh position={[0, -0.15, 0]}><capsuleGeometry args={[0.082, 0.22, 8, 16]} />{tecido}</mesh>
        <group position={[0, -0.33, 0]} rotation={[pulso, 0, 0]}>
          <mesh scale={[1, 1.15, 0.7]}><sphereGeometry args={[0.083, 20, 14]} />{robo ? <Mat c="#2a2d35" r={0.35} m={0.6} /> : <Mat c={pele} r={0.55} sheen="#ff9a8a" />}</mesh>
        </group>
      </group>
    </group>
  );
}

function Personagem({ p, est, acento, pos, giro }) {
  const robo = p.tipo === 'robo';
  const corpo = robo ? p.cor : p.roupa;
  const resp = 1 + Math.sin(est.t * 1.6 + pos[0]) * 0.012;
  const [cy, cp, cr] = est.cabeca;
  return (
    <group position={pos} rotation={[0, giro, 0]}>
      <group scale={[1, resp, 1]}>
        <mesh position={[0, 0.92, 0]} scale={[1.18, 1, 0.78]} castShadow>
          <capsuleGeometry args={[0.34, 0.42, 12, 32]} />
          {robo ? <Mat c={corpo} r={0.28} m={0.1} cc={1} /> : <Mat c={corpo} r={0.85} sheen="#ffffff" />}
        </mesh>
        {robo
          ? <mesh position={[0, 1.0, 0.265]}><circleGeometry args={[0.07, 32]} /><meshBasicMaterial color={acento} /></mesh>
          : <mesh position={[0, 1.27, 0]} rotation={[Math.PI / 2, 0, 0]}><torusGeometry args={[0.15, 0.04, 12, 32]} /><Mat c={new THREE.Color(corpo).multiplyScalar(0.7).getStyle()} r={0.85} /></mesh>}
        <mesh position={[0, 1.3, 0]}><cylinderGeometry args={[0.12, 0.13, 0.25, 24]} />{robo ? <Mat c="#3a3e48" r={0.4} m={0.6} /> : <Mat c={p.pele} r={0.6} sheen="#ff9a8a" />}</mesh>
      </group>
      <Braco lado={-1} g={est.gesto[0]} cor={corpo} pele={p.pele} robo={robo} t={est.t} />
      <Braco lado={1} g={est.gesto[1]} cor={corpo} pele={p.pele} robo={robo} t={est.t} />
      <group position={[0, 1.62, 0]} rotation={[cp, cy, cr]} castShadow>
        {robo ? <RostoRobo p={p} est={est} acento={acento} /> : <Rosto p={p} est={est} />}
      </group>
    </group>
  );
}

/* ── O SET ───────────────────────────────────────────────────────────────── */
function Palco({ c, dois, t }) {
  const parede = -2.6;
  const paineis = [-4.2, -2.8, -1.4, 0, 1.4, 2.8, 4.2];
  return (
    <group>
      <mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow><planeGeometry args={[40, 40]} /><Mat c={c.chao} r={0.55} /></mesh>
      <mesh position={[0, 4, parede]} receiveShadow><planeGeometry args={[40, 10]} /><Mat c={c.parede} r={0.9} /></mesh>
      {paineis.map((x, i) => (
        <group key={i}>
          <mesh position={[x, 1.9, parede + 0.06]} receiveShadow><boxGeometry args={[1.2, 3.2, 0.1]} /><Mat c={c.parede2} r={0.95} /></mesh>
          <mesh position={[x + 0.66, 1.9, parede + 0.08]}><boxGeometry args={[0.03, 3.2, 0.03]} /><meshBasicMaterial color={c.acento} /></mesh>
        </group>
      ))}
      {/* o neon da parede: um anel e um traço na cor de acento, piscando de leve */}
      <mesh position={[1.7, 2.55, parede + 0.14]}><torusGeometry args={[0.32, 0.025, 12, 64]} /><meshBasicMaterial color={c.acento} toneMapped={false} /></mesh>
      <mesh position={[1.7, 2.55, parede + 0.14]}><circleGeometry args={[0.06, 24]} /><meshBasicMaterial color={hash(Math.floor(t * 1.5)) > 0.1 ? '#ff3b3b' : '#551515'} toneMapped={false} /></mesh>
      <mesh position={[-1.9, 2.75, parede + 0.14]}><boxGeometry args={[1.3, 0.04, 0.03]} /><meshBasicMaterial color={c.acento} toneMapped={false} /></mesh>
      {/* a estante com livros e a planta */}
      <group position={[-3.0, 0, parede + 0.45]}>
        {[0.9, 1.6].map((y) => <mesh key={y} position={[0, y, 0]} castShadow receiveShadow><boxGeometry args={[1.6, 0.05, 0.4]} /><Mat c="#5b4632" r={0.7} /></mesh>)}
        {[0, 1, 2, 3, 4, 5].map((k) => <mesh key={k} position={[-0.6 + k * 0.17, 1.05 + hash(k) * 0.04, 0]} castShadow><boxGeometry args={[0.12, 0.26 + hash(k + 9) * 0.12, 0.3]} /><Mat c={['#c94f3d', '#2f6db5', '#e8c35a', '#3d8a5f', '#ece6da', '#4a3d6e'][k]} r={0.8} /></mesh>)}
      </group>
      <group position={[3.1, 0, parede + 0.6]}>
        <mesh position={[0, 0.3, 0]} castShadow><cylinderGeometry args={[0.28, 0.22, 0.6, 32]} /><Mat c="#d8d2c8" r={0.6} /></mesh>
        {Array.from({ length: 9 }, (_, k) => {
          const a = k * 2.4, h = 0.75 + hash(k) * 0.7;
          return <mesh key={k} position={[Math.cos(a) * 0.18, h, Math.sin(a) * 0.18]} rotation={[Math.cos(a) * 0.4, 0, Math.sin(a) * 0.4]} castShadow><sphereGeometry args={[0.2 + hash(k + 3) * 0.08, 16, 12]} /><Mat c={k % 2 ? '#2f7a43' : '#3f9a55'} r={0.7} /></mesh>;
        })}
      </group>
      {/* a luminária de chão, acesa */}
      <group position={[-3.6, 0, 0.2]}>
        <mesh position={[0, 0.9, 0]}><cylinderGeometry args={[0.02, 0.02, 1.8, 8]} /><Mat c="#222" r={0.4} m={0.6} /></mesh>
        <mesh position={[0, 1.85, 0]}><sphereGeometry args={[0.18, 24, 16]} /><meshBasicMaterial color={c.luz} toneMapped={false} /></mesh>
      </group>
      {/* a mesa */}
      <group position={[0, 0, 0.42]}>
        <mesh position={[0, 0.76, 0]} scale={[dois ? 1.75 : 1.1, 1, 0.62]} castShadow receiveShadow><cylinderGeometry args={[1, 1, 0.05, 64]} /><Mat c="#3b2a1f" r={0.35} cc={0.6} /></mesh>
        <mesh position={[0, 0.38, -0.05]} scale={[dois ? 1.5 : 0.92, 1, 0.5]} castShadow receiveShadow><cylinderGeometry args={[1, 1, 0.76, 64]} /><Mat c="#16171b" r={0.5} m={0.2} /></mesh>
        <mesh position={[0, 0.05, -0.05]} scale={[dois ? 1.51 : 0.93, 1, 0.51]}><cylinderGeometry args={[1, 1, 0.02, 64, 1, true]} /><meshBasicMaterial color={c.acento} toneMapped={false} /></mesh>
        {/* a xícara e o papel: a mesa de quem está gravando */}
        <mesh position={[dois ? 0.15 : 0.55, 0.85, 0.22]} castShadow><cylinderGeometry args={[0.055, 0.045, 0.13, 24]} /><Mat c="#ece6da" r={0.3} cc={1} /></mesh>
        <mesh position={[dois ? -0.2 : -0.5, 0.79, 0.25]} rotation={[-Math.PI / 2, 0, 0.3]}><planeGeometry args={[0.3, 0.4]} /><Mat c="#f4f1ea" r={0.9} /></mesh>
      </group>
    </group>
  );
}

function Microfone({ x, z, giro }) {
  return (
    <group position={[x, 0, z]} rotation={[0, giro, 0]}>
      <mesh position={[0.26, 0.8, 0.62]}><cylinderGeometry args={[0.07, 0.08, 0.02, 24]} /><Mat c="#1b1c1f" r={0.3} m={0.8} /></mesh>
      <mesh position={[0.26, 1.0, 0.62]}><cylinderGeometry args={[0.01, 0.01, 0.4, 8]} /><Mat c="#1b1c1f" r={0.3} m={0.8} /></mesh>
      <mesh position={[0.22, 1.24, 0.57]} rotation={[-0.9, 0, 0.35]} castShadow><capsuleGeometry args={[0.042, 0.11, 8, 16]} /><Mat c="#24262b" r={0.35} m={0.7} /></mesh>
    </group>
  );
}

/* ── A CÂMERA ────────────────────────────────────────────────────────────── */
function cameraDe(pl, t, part, aspecto) {
  const vert = aspecto < 1;
  const el = part.elenco;
  const dois = el.length > 1;
  const quem = el[pl.alvo] ? pl.alvo : 0;
  const lugar = (i) => (dois ? [(el[i].lado || 0) * 0.95, 0, 0] : [0, 0, 0]);
  const giroDe = (i) => (dois ? (el[i].lado < 0 ? 0.95 : -0.95) : 0);
  const cabeca = (i) => { const p = lugar(i); return [p[0], 1.6, p[2]]; };
  const frente = (i) => [Math.sin(giroDe(i)), 0, Math.cos(giroDe(i))];
  const k = clamp((t - pl.de) / Math.max(0.5, pl.ate - pl.de));
  const fovK = vert ? 1.5 : 1;
  let pos, alvo, fov;
  if (pl.tipo === 'aberto' || pl.tipo === 'largo') {
    fov = (pl.tipo === 'largo' ? 40 : 36) * fovK;
    const dist = Math.max(pl.tipo === 'largo' ? 5.4 : 4.4, 1.9 / (Math.tan((fov * Math.PI) / 360) * aspecto));
    pos = [0, 1.75, dist]; alvo = [0, 1.25, 0];
  } else if (pl.tipo === 'ombro' && dois) {
    // Atrás do ombro de quem ouve, afastada para o lado da câmera: a nuca dele fica na borda do quadro.
    // Na linha de quem fala até quem ouve, 1,25 m além dele e 1 m para o lado da câmera: só a borda dele entra no quadro.
    const o = 1 - quem, h = cabeca(o), s = cabeca(quem);
    const d = [h[0] - s[0], h[2] - s[2]], n = Math.hypot(d[0], d[1]) || 1;
    const lat = [-d[1] / n, d[0] / n];
    const latZ = lat[1] >= 0 ? lat : [-lat[0], -lat[1]];
    pos = [h[0] + (d[0] / n) * 1.25 + latZ[0] * 1.0, 1.92, h[2] + (d[1] / n) * 1.25 + latZ[1] * 1.0];
    alvo = cabeca(quem); alvo = [alvo[0], 1.5, alvo[2]]; fov = 30 * fovK;
  } else {
    // No vertical o quadro é estreito: a câmera recua para o rosto caber de lado a lado.
    const dist = (pl.tipo === 'detalhe' ? 1.15 : pl.tipo === 'close' ? 1.75 : 2.6) * (vert ? 1.55 : 1);
    fov = (pl.tipo === 'detalhe' ? 24 : pl.tipo === 'close' ? 28 : 32) * fovK;
    const h = cabeca(quem), f = frente(quem);
    // Um pouco de lado (o "três quartos") e da altura dos olhos.
    const lado = pl.lado * 0.35;
    const dir = [f[0] * Math.cos(lado) - f[2] * Math.sin(lado), 0, f[0] * Math.sin(lado) + f[2] * Math.cos(lado)];
    pos = [h[0] + dir[0] * dist, pl.tipo === 'medio' ? 1.62 : 1.66, h[2] + dir[2] * dist];
    alvo = [h[0], pl.tipo === 'medio' ? 1.35 : pl.tipo === 'close' ? 1.52 : 1.58, h[2]];
  }
  // O movimento dentro do plano: entra, sai, desliza — e o balanço de câmera na mão.
  const m = suave(k);
  if (pl.mov === 'entra' || pl.mov === 'sai') {
    const s = pl.mov === 'entra' ? m : 1 - m;
    pos = pos.map((v, i) => lerp(v, alvo[i], 0.1 * s));
  } else if (pl.mov === 'lateral') {
    pos = [pos[0] + (m - 0.5) * 0.3 * pl.lado, pos[1], pos[2]];
  }
  const mao = [Math.sin(t * 1.3) * 0.012 + Math.sin(t * 3.1) * 0.005, Math.sin(t * 1.1 + 1) * 0.009 + Math.sin(t * 2.7) * 0.004, 0];
  return { pos: pos.map((v, i) => v + (mao[i] || 0)), alvo: alvo.map((v, i) => v + (mao[i] || 0) * 0.5), fov };
}

/* ── POR CIMA: legenda, nome, destaque, REC e grão ──────────────────────── */
const FONTE = "'Archivo Black','Arial Black','Helvetica Neue',Impact,system-ui,sans-serif";

function pedacos(palavras) {
  const out = [];
  let cur = [];
  for (const w of palavras) {
    cur.push(w);
    const letras = cur.reduce((n, x) => n + String(x.t).length + 1, 0);
    if (cur.length >= 4 || letras > 22 || /[.,!?;:]$/.test(w.t)) { out.push(cur); cur = []; }
  }
  if (cur.length) out.push(cur);
  return out;
}

function Legenda({ part, t, W, H, acento }) {
  const f = part.falas.find((x) => t >= x.de - 0.1 && t <= x.ate + 0.25);
  if (!f) return null;
  const grupos = pedacos(f.palavras);
  const g = grupos.find((gr) => t <= gr[gr.length - 1].ate + 0.15) || grupos[grupos.length - 1];
  const t0 = g[0].de;
  const tam = Math.round(Math.min(W, H) * (H > W ? 0.075 : 0.058));
  return (
    <div style={{ position: 'absolute', left: 0, right: 0, top: H * (H > W ? 0.7 : 0.8), display: 'flex', justifyContent: 'center', flexWrap: 'wrap', gap: `0 ${tam * 0.42}px`, padding: `0 ${W * 0.08}px` }}>
      {g.map((w, i) => {
        const on = t >= w.de;
        const agora = t >= w.de && t < w.ate;
        const k = mola((t - Math.max(t0, w.de - 0.06)) / 0.22);
        return (
          <span key={i} style={{ fontFamily: FONTE, fontWeight: 900, fontSize: tam, lineHeight: 1.05, textTransform: 'uppercase', letterSpacing: '-0.01em',
            color: agora ? acento : '#ffffff', opacity: on ? 1 : 0.0, transform: `translateY(${(1 - k) * tam * 0.35}px) scale(${agora ? 1.04 : 1})`,
            textShadow: '0 4px 0 rgba(0,0,0,0.55), 0 0 24px rgba(0,0,0,0.45)', WebkitTextStroke: `${Math.max(2, tam * 0.05)}px rgba(0,0,0,0.65)`, paintOrder: 'stroke fill' }}>{w.t}</span>
        );
      })}
    </div>
  );
}

function Nome({ part, t, W, H, acento }) {
  if (!part.elenco.some((e) => e.rotulo)) return null;
  // O nome entra na primeira fala de cada um, por 2,4 s.
  const vistos = new Set();
  let tag = null;
  for (const f of part.falas) {
    if (vistos.has(f.quem)) continue;
    vistos.add(f.quem);
    if (t >= f.de && t <= f.de + 2.4) tag = { f, k: t - f.de };
  }
  if (!tag) return null;
  const p = part.elenco[tag.f.quem];
  const entra = suave(tag.k / 0.35), sai = 1 - suave((tag.k - 2.0) / 0.4);
  const tam = Math.round(Math.min(W, H) * 0.032);
  return (
    <div style={{ position: 'absolute', left: W * 0.06, top: H * (H > W ? 0.6 : 0.66), opacity: entra * sai, transform: `translateX(${(1 - entra) * -40}px)`, display: 'flex', alignItems: 'stretch' }}>
      <div style={{ width: tam * 0.3, background: acento }} />
      <div style={{ background: 'rgba(8,10,14,0.82)', padding: `${tam * 0.35}px ${tam * 0.7}px`, fontFamily: FONTE, fontSize: tam, color: '#fff', textTransform: 'uppercase', letterSpacing: '0.04em' }}>{p.rotulo}</div>
    </div>
  );
}

function Destaque({ part, t, W, H, acento }) {
  const f = part.falas.find((x) => x.destaque && t >= x.de + 0.3 && t <= Math.min(x.ate + 0.3, x.de + 3.2));
  if (!f) return null;
  const k = t - (f.de + 0.3);
  const fim = Math.min(f.ate + 0.3, f.de + 3.2) - (f.de + 0.3);
  const s = mola(k / 0.4) * (1 - suave((k - fim + 0.3) / 0.3));
  const tam = Math.round(Math.min(W, H) * (H > W ? 0.085 : 0.075));
  return (
    <div style={{ position: 'absolute', left: 0, right: 0, top: H * (H > W ? 0.1 : 0.07), display: 'flex', justifyContent: 'center' }}>
      <div style={{ transform: `scale(${s}) rotate(${(1 - s) * -4}deg)`, opacity: clamp(s * 1.4), background: 'rgba(8,10,14,0.78)', border: `${Math.max(3, tam * 0.06)}px solid ${acento}`,
        borderRadius: tam * 0.3, padding: `${tam * 0.25}px ${tam * 0.55}px`, fontFamily: FONTE, fontSize: tam, color: '#fff', textTransform: 'uppercase', boxShadow: `0 0 ${tam}px ${acento}55` }}>{f.destaque}</div>
    </div>
  );
}

function Rec({ t, W, H }) {
  const tam = Math.round(Math.min(W, H) * 0.024);
  const s = Math.floor(t), q = Math.floor((t % 1) * 30);
  const tc = `00:${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}:${String(q).padStart(2, '0')}`;
  return (
    <div style={{ position: 'absolute', left: W * 0.05, top: H * 0.045, display: 'flex', alignItems: 'center', gap: tam * 0.5, fontFamily: "'SF Mono',Menlo,monospace", fontSize: tam, color: 'rgba(255,255,255,0.85)', letterSpacing: '0.08em' }}>
      <div style={{ width: tam * 0.7, height: tam * 0.7, borderRadius: '50%', background: '#ff3030', opacity: Math.floor(t * 1.2) % 2 ? 0.25 : 1 }} />
      <span>REC</span><span style={{ opacity: 0.7 }}>{tc}</span>
    </div>
  );
}

function Filme({ t, W, H, grao }) {
  return (
    <>
      <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse at center, rgba(0,0,0,0) 55%, rgba(0,0,0,0.5) 100%)' }} />
      {grao && (
        <svg width={W} height={H} style={{ position: 'absolute', inset: 0, opacity: 0.08, mixBlendMode: 'overlay' }}>
          <filter id="grao"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed={Math.floor(t * 24) % 50} /></filter>
          <rect width="100%" height="100%" filter="url(#grao)" />
        </svg>
      )}
    </>
  );
}

/* ── A PEÇA ──────────────────────────────────────────────────────────────── */
export function PersonagemCena({ el, t }) {
  const part = el.partitura || {};
  const W = el.largura || part.largura || 1920, H = el.altura || part.altura || 1080;
  const c = part.cenario || {};
  const acento = c.acento || '#3ddc97';
  const elenco = part.elenco || [];
  const falas = part.falas || [];
  const planos = part.planos || [{ de: 0, ate: part.duracao || 10, tipo: 'aberto', alvo: 0, mov: 'fixo', lado: 1 }];
  const dois = elenco.length > 1;
  const pl = planos.find((x) => t >= x.de && t < x.ate) || planos[planos.length - 1];
  const cam = cameraDe(pl, t, part, W / H);

  const falandoAgora = (i) => falas.some((f) => f.quem === i && t >= f.de && t <= f.ate);
  const emocaoDe = (i) => {
    let e = 'neutro';
    for (const f of falas) if (f.quem === i && f.de <= t + 0.2) e = t <= f.ate + 2.5 ? f.emocao : 'neutro';
    return e;
  };
  const estado = (i) => {
    const fala = falandoAgora(i);
    const outro = dois && falandoAgora(1 - i);
    const enfase = gesto(falas, i, t);
    const nod = Math.sin(t * (fala ? 5.2 : 1.4) + i) * (fala ? 0.035 : 0.015) + (outro ? Math.max(0, Math.sin(t * 2.1 + i)) * 0.04 : 0);
    const emo = emocaoDe(i);
    const e = EMO[emo] || EMO.neutro;
    const micro = (hash(Math.floor(t * 1.5) + i * 7) - 0.5) * 0.08;
    return {
      t, falando: fala, emocao: emo, boca: boca(falas, i, t), pisca: piscada(i, t),
      gesto: enfase, enfase: Math.max(enfase[0], enfase[1]),
      olhar: dois ? [micro, 0.02] : [micro * 0.5, 0.04],
      cabeca: [Math.sin(t * 0.7 + i * 2) * 0.08 + (outro ? 0.05 : 0), nod + (fala ? 0 : 0.03), (e.inclina || 0) * (i ? -1 : 1) + Math.sin(t * 0.5 + i) * 0.02],
    };
  };
  const posDe = (i) => (dois ? [(elenco[i].lado || 0) * 0.95, 0, 0] : [0, 0, 0]);
  const giroDe = (i) => (dois ? (elenco[i].lado < 0 ? 0.95 : -0.95) : 0);
  const fadeIn = clamp(t / 0.5), fadeOut = clamp((part.duracao - t) / 0.6);

  return (
    <div style={{ position: 'absolute', left: el.x || 0, top: el.y || 0, width: W, height: H, overflow: 'hidden', background: '#000' }}>
      <div style={{ position: 'absolute', inset: 0, opacity: Math.min(fadeIn, fadeOut) }}>
        <ThreeCanvas width={W} height={H} shadows camera={{ position: [0, 1.7, 4.5], fov: 36 }}
          gl={{ antialias: true, preserveDrawingBuffer: true, toneMapping: THREE.ACESFilmicToneMapping, toneMappingExposure: 1.05 }}>
          <Camera pos={cam.pos} alvo={cam.alvo} fov={cam.fov} />
          <Ambiente />
          <color attach="background" args={[c.parede || '#111']} />
          <fog attach="fog" args={[c.parede || '#111', 7, 16]} />
          <hemisphereLight args={['#dfe8ff', c.chao || '#222', 0.45]} />
          <ambientLight intensity={0.15} />
          <spotLight position={[2.4, 4.6, 4.2]} angle={0.55} penumbra={0.7} intensity={95} distance={20} decay={2} color={c.luz || '#fff'} castShadow
            shadow-mapSize-width={2048} shadow-mapSize-height={2048} shadow-bias={-0.0004} />
          <directionalLight position={[-3, 2.5, 3]} intensity={0.7} color="#cfe0ff" />
          <pointLight position={[-2.2, 2.4, -1.4]} intensity={14} distance={7} color={acento} />
          <pointLight position={[2.2, 2.4, -1.4]} intensity={14} distance={7} color={acento} />
          <pointLight position={[-3.6, 1.85, 0.2]} intensity={6} distance={5} color={c.luz || '#ffd9a8'} />
          <Palco c={c} dois={dois} t={t} />
          {elenco.map((p, i) => <Personagem key={i} p={p} est={estado(i)} acento={acento} pos={posDe(i)} giro={giroDe(i)} />)}
          {c.microfones && elenco.map((p, i) => <Microfone key={'m' + i} x={posDe(i)[0]} z={posDe(i)[2]} giro={giroDe(i)} />)}
        </ThreeCanvas>
        <Filme t={t} W={W} H={H} grao={!part.filme || part.filme.grao !== false} />
        <Rec t={t} W={W} H={H} />
        <Nome part={part} t={t} W={W} H={H} acento={acento} />
        <Destaque part={part} t={t} W={W} H={H} acento={acento} />
        {part.legenda !== false && <Legenda part={part} t={t} W={W} H={H} acento={acento} />}
      </div>
    </div>
  );
}

export const PERSONAGEM = { personagem: PersonagemCena };
