// Motor de audio sintético — Ultimo Verso · Àltima
// Sintetiza una pieza piano + pad usando Web Audio API.
// Reemplazable por una integración Suno/ElevenLabs en producción (vía backend).

const UV_AUDIO = {
  ctx: null,
  master: null,
  active: [],     // nodos en curso
  rafId: null,
  startedAt: 0,
  totalDur: 0,
  paused: false,
  pausedAt: 0,
  onTick: null,
  onEnd: null,

  ensure() {
    if (this.ctx) return this.ctx;
    const C = window.AudioContext || window.webkitAudioContext;
    this.ctx = new C();
    // Master con compresor para sonar más "producido"
    this.master = this.ctx.createGain();
    this.master.gain.value = 0.85;
    const comp = this.ctx.createDynamicsCompressor();
    comp.threshold.value = -18;
    comp.knee.value = 20;
    comp.ratio.value = 4;
    comp.attack.value = 0.005;
    comp.release.value = 0.25;
    this.master.connect(comp).connect(this.ctx.destination);
    return this.ctx;
  },

  // Una nota tipo "piano": sine + triángulo + envolvente
  noteAt(freq, startOffset, dur, vol = 0.18, type = "sine") {
    const ctx = this.ctx, t = ctx.currentTime + startOffset;
    const osc1 = ctx.createOscillator();
    const osc2 = ctx.createOscillator();
    const gain = ctx.createGain();
    const filt = ctx.createBiquadFilter();
    filt.type = "lowpass";
    filt.frequency.setValueAtTime(2400, t);
    filt.Q.value = 0.6;

    osc1.type = type;
    osc1.frequency.value = freq;
    osc2.type = "triangle";
    osc2.frequency.value = freq * 2.001; // ligero detune
    const subGain = ctx.createGain();
    subGain.gain.value = 0.35;
    osc2.connect(subGain).connect(gain);
    osc1.connect(gain);
    gain.connect(filt).connect(this.master);

    // Envolvente ADSR
    gain.gain.setValueAtTime(0, t);
    gain.gain.linearRampToValueAtTime(vol, t + 0.02);
    gain.gain.exponentialRampToValueAtTime(vol * 0.4, t + 0.25);
    gain.gain.exponentialRampToValueAtTime(0.0001, t + dur);

    osc1.start(t); osc2.start(t);
    osc1.stop(t + dur + 0.05); osc2.stop(t + dur + 0.05);
    this.active.push(osc1, osc2);
  },

  // Pad sostenido (cuerdas/órgano suave)
  padAt(freqs, startOffset, dur, vol = 0.06) {
    const ctx = this.ctx, t = ctx.currentTime + startOffset;
    const gain = ctx.createGain();
    const filt = ctx.createBiquadFilter();
    filt.type = "lowpass";
    filt.frequency.value = 1400;
    gain.connect(filt).connect(this.master);

    gain.gain.setValueAtTime(0, t);
    gain.gain.linearRampToValueAtTime(vol, t + 1.0);
    gain.gain.linearRampToValueAtTime(vol, t + dur - 0.8);
    gain.gain.exponentialRampToValueAtTime(0.0001, t + dur);

    freqs.forEach(f => {
      const o = ctx.createOscillator();
      o.type = "sawtooth";
      o.frequency.value = f;
      const og = ctx.createGain();
      og.gain.value = 0.18;
      o.connect(og).connect(gain);
      o.start(t); o.stop(t + dur + 0.1);
      this.active.push(o);
    });
  },

  stop() {
    if (this.rafId) cancelAnimationFrame(this.rafId);
    this.rafId = null;
    this.active.forEach(n => { try { n.stop(); } catch(e){} });
    this.active = [];
    this.paused = false;
    this.totalDur = 0;
  },

  // Devuelve nº de segundos de la duración elegida
  parseDur(s) {
    const m = /(\d+):(\d+)/.exec(s || "2:30");
    return m ? (+m[1])*60 + (+m[2]) : 150;
  },

  // Compone y reproduce. data = perfil del difunto.
  play(data, { onTick, onEnd } = {}) {
    this.stop();
    const ctx = this.ensure();
    if (ctx.state === "suspended") ctx.resume();

    const totalSec = this.parseDur(data.duracion);
    this.totalDur = totalSec;
    this.startedAt = ctx.currentTime;
    this.onTick = onTick;
    this.onEnd = onEnd;

    const tempo = data.tempo === "Vivo" ? 96 : data.tempo === "Medio" ? 80 : 64;
    const beatSec = 60 / tempo;

    // Detectar tonalidad: melancólico/copla/bolero/flamenco → menor
    const minorKeys = ["Melancólico","Copla","Bolero","Flamenco","Clásica"];
    const isMinor = minorKeys.some(k =>
      (data.emocion || "").includes(k) || (data.genero || "").includes(k)
    ) || !data.genero;

    // Escala (frecuencias en Hz) — A menor o C mayor
    // Pentatónica + 7ª para color
    const A_MIN = { root: 220, scale: [220, 261.63, 293.66, 329.63, 392, 440, 523.25, 587.33, 659.25, 783.99, 880] };
    const C_MAJ = { root: 261.63, scale: [261.63, 293.66, 329.63, 349.23, 392, 440, 493.88, 523.25, 587.33, 659.25, 783.99] };
    const K = isMinor ? A_MIN : C_MAJ;

    // Progresión de acordes (índices dentro de la escala) — 4 acordes, 4 compases cada uno
    // Menor: i - VI - III - VII  (Am-F-C-G en Am)
    // Mayor: I - V - vi - IV     (C-G-Am-F)
    const progMinor = [
      [220, 261.63, 329.63],     // Am
      [174.61, 220, 261.63],     // F
      [196, 246.94, 293.66],     // G  → usado como VII
      [261.63, 329.63, 392],     // C
    ];
    const progMajor = [
      [261.63, 329.63, 392],     // C
      [196, 246.94, 293.66],     // G
      [220, 261.63, 329.63],     // Am
      [174.61, 220, 261.63],     // F
    ];
    const prog = isMinor ? progMinor : progMajor;

    // Estructura: intro 4 bars + 2 verses + chorus + verse + outro
    const beatsPerBar = 4;
    let cursor = 0; // en segundos relativos
    let bar = 0;

    while (cursor < totalSec - 1) {
      const chord = prog[bar % prog.length];
      const barDur = beatsPerBar * beatSec;

      // PAD sostenido del acorde (octava baja)
      this.padAt(chord, cursor, barDur, 0.055);

      // ARPEGIO de piano (8 notas por compás = corcheas)
      const arp = [chord[0], chord[1], chord[2], chord[1], chord[0]*2, chord[1], chord[2], chord[1]];
      for (let i = 0; i < 8; i++) {
        const t = cursor + i * (barDur / 8);
        const noteDur = barDur / 8 * 1.6; // ligeramente solapado
        const vol = 0.10 + (i % 4 === 0 ? 0.04 : 0); // acento en 1
        this.noteAt(arp[i], t, noteDur, vol, "sine");
      }

      // MELODÍA tipo "voz líder" — cada 2 compases una frase
      if (bar % 2 === 0) {
        const phraseLen = beatsPerBar * 2 * beatSec;
        // 4 notas largas en la escala, alrededor de las del acorde
        const melNotes = (() => {
          const base = K.scale.filter(f => f > 350 && f < 900);
          const pick = () => base[Math.floor(Math.random() * base.length)];
          return [pick(), pick(), pick(), pick()];
        })();
        melNotes.forEach((f, i) => {
          const t = cursor + i * (phraseLen / 4);
          this.noteAt(f, t, phraseLen / 4 * 1.1, 0.13, "sine");
        });
      }

      cursor += barDur;
      bar++;
    }

    // Loop de progreso
    const tick = () => {
      if (this.paused) { this.rafId = requestAnimationFrame(tick); return; }
      const elapsed = ctx.currentTime - this.startedAt;
      const p = Math.min(1, elapsed / totalSec);
      this.onTick?.(p, elapsed);
      if (p >= 1) {
        this.onEnd?.();
        return;
      }
      this.rafId = requestAnimationFrame(tick);
    };
    this.rafId = requestAnimationFrame(tick);
  },

  pause() {
    if (!this.ctx || this.paused) return;
    this.ctx.suspend();
    this.paused = true;
  },
  resume() {
    if (!this.ctx || !this.paused) return;
    this.ctx.resume();
    this.paused = false;
  },
};

Object.assign(window, { UV_AUDIO });
