// === voz.jsx — los beats de cada escena y, de reserva, la voz del propio navegador ===
// Con audio/align.js (la toma de ElevenLabs, voz oficial, 09/10/2026) manda el mp3 de cada escena
// y B() devuelve el segundo real en que la voz empieza la frase. Sin él, lo de abajo: voz del
// navegador y reloj nominal.
//
// Copiado de derechos-de-ejecucion/voz.jsx. La voz del navegador es solo la reserva: con align.js
// manda el mp3. El motor compartido no se toca. Sin audio, el Stage corre con su reloj por `dur`; aquí lo sustituimos por el de la
// voz mandando `__deckSetTime` en cada frame, que el Stage obedece y congela su propio reloj.
//
// Cómo se sincroniza sin saber cuánto tarda la voz de cada ordenador:
// - El tiempo de la escena es NOMINAL: un carácter del guion = 1/CPS segundos. B(texto, frase)
//   da el segundo nominal en que empieza esa frase, y las escenas ponen sus beats con B().
// - La voz habla frase a frase. Al empezar cada frase (y en cada palabra, si el navegador avisa
//   con `onboundary`) sabemos en qué carácter va, y el reloj salta a ese punto nominal. Entre dos
//   avisos avanza al ritmo que la voz ha llevado hasta ahora, sin pasar del final de la frase.
// Así una voz lenta o rápida estira o encoge la animación, pero cada beat cae con su frase.
// Si no hay voz en español, el Stage corre solo con `dur` y la pieza se ve en mudo.

const CPS = 14.5;            // caracteres por segundo nominales (español, velocidad 1)
const COLA = 1.4;            // segundos de respiro al final de cada escena

const ALIGN = window.ALIGN || null;

function B(texto, frase){
  const i = texto.indexOf(frase);
  if (i < 0) { console.warn('[voz] beat sin su frase en el guion:', frase); return 0; }
  if (!ALIGN) return i / CPS;
  const palabras = ALIGN[window.GUION.indexOf(texto)];
  return palabras.find(p => p[0] >= i)[1];
}
function durDe(texto){
  if (!ALIGN) return texto.length / CPS + COLA;
  const palabras = ALIGN[window.GUION.indexOf(texto)];
  return palabras[palabras.length - 1][1] + COLA;
}

// «Dinaup» se dice «dinaap» (GENERAR-AUDIOS.md §2). Misma longitud: no mueve los índices.
function paraLaVoz(s){ return s.replace(/Dinaup/g, 'Dinaap').replace(/dinaup/g, 'dinaap'); }

function frasesDe(texto){
  const out = []; const re = /[^.?!:]+[.?!:]+(\s+|$)|[^.?!:]+$/g; let m;
  while ((m = re.exec(texto))) { if (m[0].trim()) out.push({ ini: m.index, fin: m.index + m[0].length, txt: m[0] }); }
  return out;
}

function elegirVoz(){
  const vs = (window.speechSynthesis && speechSynthesis.getVoices()) || [];
  const es = vs.filter(v => /^es/i.test(v.lang));
  if (!es.length) return null;
  const nota = v => (/^es-ES/i.test(v.lang) ? 10 : 0) + (/natural|online|neural|premium|enhanced/i.test(v.name) ? 6 : 0)
                  + (/google/i.test(v.name) ? 3 : 0) + (v.localService ? 0 : 1);
  return es.sort((a,b) => nota(b) - nota(a))[0];
}

const Voz = {
  clicado: false,       // el usuario ya ha pulsado (sin gesto el navegador no deja hablar)
  activa: false,        // hay voz en español: si no, la escena corre en mudo con el reloj nominal
  escena: -1, sesion: 0, t: 0,
  frases: [], i: 0, anclaChar: 0, anclaMs: 0, finChar: 0, cps: CPS, hablados: 0, msHablando: 0,
  pausada: false, acabada: false, raf: 0, timer: 0,

  empezar(n){
    this.parar();
    this.escena = n; this.sesion++; this.t = 0; this.acabada = false; this.pausada = false;
    if (!this.clicado) { setTimeout(() => window.dispatchEvent(new CustomEvent('__deckSetTime', { detail: { t: 0 } })), 0); return; }
    const texto = window.GUION[n];
    this.i = 0; this.anclaChar = 0; this.anclaMs = performance.now(); this.finChar = 0; this.cps = this.cps || CPS;
    if (this.activa) { this.frases = frasesDe(texto); this.decir(this.sesion); }
    else { this.frases = []; this.finChar = Infinity; this.cps = CPS; const ses = this.sesion; this.timer = setTimeout(() => { this.anclaChar = texto.length; this.anclaMs = performance.now(); this.terminar(ses); }, texto.length / CPS * 1000); }
    const loop = () => { this.tick(); this.raf = requestAnimationFrame(loop); };
    this.raf = requestAnimationFrame(loop);
  },

  decir(ses){
    if (ses !== this.sesion || this.i >= this.frases.length) return;
    const f = this.frases[this.i];
    const u = new SpeechSynthesisUtterance(paraLaVoz(f.txt));
    const v = elegirVoz(); if (v) { u.voice = v; u.lang = v.lang; } else u.lang = 'es-ES';
    u.rate = (typeof window.__deckSpeed === 'number' && window.__deckSpeed > 0) ? window.__deckSpeed : 1;
    let ini = 0;
    u.onstart = () => { if (ses !== this.sesion) return; ini = performance.now(); this.anclaChar = f.ini; this.anclaMs = ini; this.finChar = f.fin; };
    u.onboundary = (e) => { if (ses !== this.sesion || typeof e.charIndex !== 'number') return; this.anclaChar = f.ini + e.charIndex; this.anclaMs = performance.now(); };
    u.onend = () => {
      if (ses !== this.sesion) return;
      if (ini) { this.hablados += f.txt.length; this.msHablando += performance.now() - ini; this.cps = clamp(this.hablados / (this.msHablando / 1000), 8, 24); }
      this.anclaChar = f.fin; this.anclaMs = performance.now(); this.i++;
      if (this.i < this.frases.length) this.decir(ses); else this.terminar(ses);
    };
    u.onerror = u.onend;
    speechSynthesis.speak(u);
    // Algunos navegadores aceptan la frase y no la dicen nunca: sin voz en 2,5 s, la escena sigue en mudo.
    setTimeout(() => { if (ses === this.sesion && !ini && !this.pausada) { this.activa = false; this.empezar(this.escena); } }, 2500);
  },

  terminar(ses){
    this.acabada = true;
    this.timer = setTimeout(() => {
      if (ses !== this.sesion || this.pausada) return;
      const total = window.DECK.scenes.length;
      if (this.escena < total - 1 && window.__deckGoTo) window.__deckGoTo(this.escena + 1);
    }, COLA * 1000);
  },

  tick(){
    const dur = durDe(window.GUION[this.escena]);
    let c;
    if (this.acabada) c = window.GUION[this.escena].length + (performance.now() - this.anclaMs) / 1000 * CPS;
    else if (this.pausada) c = this.anclaChar;
    else c = Math.min(this.anclaChar + (performance.now() - this.anclaMs) / 1000 * this.cps, Math.max(this.finChar, this.anclaChar));
    this.t = Math.min(dur, Math.max(this.t, c / CPS));
    window.dispatchEvent(new CustomEvent('__deckSetTime', { detail: { t: this.t } }));
  },

  pausar(p){
    if (!this.clicado || p === this.pausada) return;
    this.pausada = p;
    if (p) { if (this.activa) speechSynthesis.pause(); this.anclaChar = this.t * CPS; }
    else { if (this.activa) speechSynthesis.resume(); this.anclaMs = performance.now(); if (this.acabada) this.terminar(this.sesion); }
  },

  parar(){
    this.sesion++; cancelAnimationFrame(this.raf); clearTimeout(this.timer);
    if (window.speechSynthesis) speechSynthesis.cancel();
  },
};

// Lo llama cada escena: arranca su locución al montarse, la corta al salir y sigue la pausa del deck.
function useVoz(n){
  if (ALIGN) return;
  const { playing } = React.useContext(StageCtx);
  React.useEffect(() => { Voz.empezar(n); return () => Voz.parar(); }, []);
  React.useEffect(() => { Voz.pausar(playing === false); }, [playing]);
}

// Portada «pulsa para escuchar»: el navegador no deja hablar sin un gesto antes.
(function(){
  if (ALIGN) return;
  if (!window.speechSynthesis) { Voz.clicado = true; return; }
  const capa = document.createElement('div');
  capa.style.cssText = 'position:fixed;inset:0;z-index:90;background:rgba(8,10,16,0.88);backdrop-filter:blur(14px);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:26px;cursor:pointer';
  capa.innerHTML = '<div style="width:120px;height:120px;border-radius:100px;background:#0085FF;display:flex;align-items:center;justify-content:center;box-shadow:0 20px 70px rgba(0,133,255,0.45)"><svg width="44" height="44" viewBox="0 0 24 24"><polygon points="7,4 21,12 7,20" fill="#fff"/></svg></div>'
    + '<div style="text-align:center"><div style="font-family:IBM Plex Sans,sans-serif;font-size:26px;font-weight:700;color:#fff;letter-spacing:-0.02em">Alta de cliente o proveedor</div>'
    + '<div style="font-family:JetBrains Mono,monospace;font-size:12px;color:rgba(255,255,255,0.55);letter-spacing:2px;margin-top:10px">CLIC PARA REPRODUCIR CON VOZ</div></div>';
  const listo = () => { document.body.appendChild(capa); window.dispatchEvent(new CustomEvent('__deckSetTime', { detail: { t: 0 } })); };
  if (document.body) listo(); else addEventListener('DOMContentLoaded', listo);
  const ir = () => {
    capa.remove(); removeEventListener('pointerdown', ir); removeEventListener('keydown', ir);
    Voz.clicado = true; Voz.activa = !!elegirVoz();
    Voz.empezar(Voz.escena < 0 ? 0 : Voz.escena);
    // Las voces pueden llegar tarde (Chrome las carga en segundo plano): si llegan, la escena se repite con voz.
    if (!Voz.activa) speechSynthesis.onvoiceschanged = () => { if (!Voz.activa && elegirVoz()) { Voz.activa = true; Voz.empezar(Voz.escena); } };
  };
  addEventListener('pointerdown', ir); addEventListener('keydown', ir);
  speechSynthesis.getVoices();
})();

Object.assign(window, { B, durDe, Voz, useVoz, CPS });
