// Shared utilities for Sarah's birthday minisite variations.
// - useConfetti: click-anywhere confetti cannon on a container.
// - sounds: Web Audio synthesized soundboard SFX (no asset deps).
// - WheelOfCompliments, CatchCake, SoundBoard: themable widgets.

// ── Confetti system ─────────────────────────────────────────
function useConfetti(containerRef, palette, opts = {}) {
  const partsRef = React.useRef([]);
  const rafRef = React.useRef(null);
  const burstRef = React.useRef(null);

  React.useEffect(() => {
    const el = containerRef.current;
    if (!el) return;
    const canvas = document.createElement('canvas');
    canvas.style.cssText =
      'position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:9999';
    el.appendChild(canvas);
    const ctx = canvas.getContext('2d');

    function resize() {
      const r = el.getBoundingClientRect();
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      canvas.width = Math.max(1, r.width * dpr);
      canvas.height = Math.max(1, r.height * dpr);
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    }
    resize();
    const ro = new ResizeObserver(resize);
    ro.observe(el);

    function tick() {
      const r = el.getBoundingClientRect();
      ctx.clearRect(0, 0, r.width, r.height);
      const ps = partsRef.current;
      for (let i = ps.length - 1; i >= 0; i--) {
        const p = ps[i];
        p.vy += 0.18;
        p.vx *= 0.995;
        p.x += p.vx;
        p.y += p.vy;
        p.rot += p.vr;
        p.life++;
        if (p.y > r.height + 40 || p.life > 320) { ps.splice(i, 1); continue; }
        ctx.save();
        ctx.translate(p.x, p.y);
        ctx.rotate(p.rot);
        ctx.fillStyle = p.color;
        if (p.shape === 'rect') ctx.fillRect(-p.s / 2, -p.s / 4, p.s, p.s / 2);
        else if (p.shape === 'circle') { ctx.beginPath(); ctx.arc(0, 0, p.s / 2, 0, 7); ctx.fill(); }
        else if (p.shape === 'star') {
          ctx.beginPath();
          for (let k = 0; k < 10; k++) {
            const a = (k / 10) * Math.PI * 2 - Math.PI / 2;
            const rr = k % 2 === 0 ? p.s / 2 : p.s / 4;
            ctx[k === 0 ? 'moveTo' : 'lineTo'](Math.cos(a) * rr, Math.sin(a) * rr);
          }
          ctx.closePath(); ctx.fill();
        } else ctx.fillRect(-p.s / 2, -p.s / 2, p.s, p.s);
        ctx.restore();
      }
      rafRef.current = requestAnimationFrame(tick);
    }
    rafRef.current = requestAnimationFrame(tick);

    const shapes = opts.shapes || ['rect', 'circle', 'square', 'star'];
    function burst(x, y, n = 56) {
      const r = el.getBoundingClientRect();
      const lx = x - r.left;
      const ly = y - r.top;
      for (let i = 0; i < n; i++) {
        const a = Math.random() * Math.PI * 2;
        const v = 4 + Math.random() * 9;
        partsRef.current.push({
          x: lx, y: ly,
          vx: Math.cos(a) * v,
          vy: Math.sin(a) * v - 3 - Math.random() * 4,
          rot: Math.random() * Math.PI * 2,
          vr: (Math.random() - 0.5) * 0.4,
          s: 6 + Math.random() * 12,
          color: palette[Math.floor(Math.random() * palette.length)],
          shape: shapes[Math.floor(Math.random() * shapes.length)],
          life: 0,
        });
      }
    }
    burstRef.current = burst;
    el.__burst = burst;

    function onClick(e) {
      // Don't fire for elements that opted out
      if (e.target && e.target.closest && e.target.closest('[data-no-confetti]')) return;
      burst(e.clientX, e.clientY, 44 + Math.random() * 20);
    }
    el.addEventListener('click', onClick);

    return () => {
      el.removeEventListener('click', onClick);
      cancelAnimationFrame(rafRef.current);
      ro.disconnect();
      canvas.remove();
      delete el.__burst;
    };
  }, []);

  return burstRef;
}

// ── Sound synthesizer (Web Audio API) ──────────────────────
let _ac = null;
function ac() {
  if (!_ac) _ac = new (window.AudioContext || window.webkitAudioContext)();
  if (_ac.state === 'suspended') _ac.resume();
  return _ac;
}

const sounds = {
  horn() {
    const a = ac(), t = a.currentTime;
    const o = a.createOscillator(), g = a.createGain();
    o.type = 'sawtooth';
    o.frequency.setValueAtTime(180, t);
    o.frequency.exponentialRampToValueAtTime(460, t + 0.05);
    o.frequency.setValueAtTime(460, t + 0.4);
    o.frequency.exponentialRampToValueAtTime(120, t + 0.7);
    g.gain.setValueAtTime(0.001, t);
    g.gain.exponentialRampToValueAtTime(0.28, t + 0.04);
    g.gain.setValueAtTime(0.28, t + 0.55);
    g.gain.exponentialRampToValueAtTime(0.001, t + 0.75);
    o.connect(g).connect(a.destination);
    o.start(t); o.stop(t + 0.78);
  },
  kazoo() {
    const a = ac(), t = a.currentTime;
    const o = a.createOscillator(), g = a.createGain();
    const lfo = a.createOscillator(), lg = a.createGain();
    o.type = 'square';
    lfo.frequency.value = 22; lg.gain.value = 28;
    lfo.connect(lg).connect(o.frequency);
    const notes = [392, 440, 494, 523, 587, 494, 523];
    notes.forEach((n, i) => o.frequency.setValueAtTime(n, t + i * 0.13));
    g.gain.setValueAtTime(0.001, t);
    g.gain.exponentialRampToValueAtTime(0.14, t + 0.02);
    g.gain.exponentialRampToValueAtTime(0.001, t + notes.length * 0.13 + 0.1);
    const f = a.createBiquadFilter(); f.type = 'lowpass'; f.frequency.value = 2400;
    o.connect(f).connect(g).connect(a.destination);
    o.start(t); lfo.start(t);
    o.stop(t + notes.length * 0.13 + 0.15);
    lfo.stop(t + notes.length * 0.13 + 0.15);
  },
  applause() {
    const a = ac(), t = a.currentTime, dur = 2.0;
    const buf = a.createBuffer(1, a.sampleRate * dur, a.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < d.length; i++) {
      const pos = i / d.length;
      const env = pos < 0.08 ? pos / 0.08 : Math.pow(1 - (pos - 0.08) / 0.92, 1.4);
      // Cluster of claps
      const clap = (Math.random() < 0.04) ? 1 : 0.25;
      d[i] = (Math.random() * 2 - 1) * env * 0.55 * clap;
    }
    const src = a.createBufferSource(); src.buffer = buf;
    const f = a.createBiquadFilter(); f.type = 'bandpass'; f.frequency.value = 2400; f.Q.value = 0.7;
    src.connect(f).connect(a.destination);
    src.start(t);
  },
  yay() {
    const a = ac(), t = a.currentTime;
    const notes = [523.25, 659.25, 783.99, 1046.5];
    notes.forEach((n, i) => {
      const o = a.createOscillator(), g = a.createGain();
      o.type = 'triangle';
      o.frequency.value = n;
      g.gain.setValueAtTime(0.0001, t + i * 0.06);
      g.gain.exponentialRampToValueAtTime(0.18, t + i * 0.06 + 0.02);
      g.gain.exponentialRampToValueAtTime(0.001, t + i * 0.06 + 0.6);
      o.connect(g).connect(a.destination);
      o.start(t + i * 0.06); o.stop(t + i * 0.06 + 0.65);
    });
  },
  ding() {
    const a = ac(), t = a.currentTime;
    [1318, 1976, 2637].forEach((n, i) => {
      const o = a.createOscillator(), g = a.createGain();
      o.type = 'sine'; o.frequency.value = n;
      g.gain.setValueAtTime(0.0001, t);
      g.gain.exponentialRampToValueAtTime(0.25 / (i + 1), t + 0.005);
      g.gain.exponentialRampToValueAtTime(0.001, t + 1.4);
      o.connect(g).connect(a.destination);
      o.start(t); o.stop(t + 1.45);
    });
  },
  airhorn() {
    const a = ac(), t = a.currentTime;
    const o1 = a.createOscillator(), o2 = a.createOscillator(), g = a.createGain();
    o1.type = 'sawtooth'; o2.type = 'sawtooth';
    o1.frequency.value = 220; o2.frequency.value = 223;
    g.gain.setValueAtTime(0.001, t);
    g.gain.exponentialRampToValueAtTime(0.22, t + 0.03);
    g.gain.setValueAtTime(0.22, t + 0.85);
    g.gain.exponentialRampToValueAtTime(0.001, t + 1.1);
    const f = a.createBiquadFilter(); f.type = 'lowpass'; f.frequency.value = 1400;
    o1.connect(f); o2.connect(f); f.connect(g).connect(a.destination);
    o1.start(t); o2.start(t); o1.stop(t + 1.15); o2.stop(t + 1.15);
  },
  boing() {
    const a = ac(), t = a.currentTime;
    const o = a.createOscillator(), g = a.createGain();
    o.type = 'sine';
    o.frequency.setValueAtTime(900, t);
    o.frequency.exponentialRampToValueAtTime(80, t + 0.45);
    g.gain.setValueAtTime(0.32, t);
    g.gain.exponentialRampToValueAtTime(0.001, t + 0.5);
    o.connect(g).connect(a.destination);
    o.start(t); o.stop(t + 0.5);
  },
  cheer() {
    const a = ac(), t = a.currentTime, dur = 1.5;
    const buf = a.createBuffer(1, a.sampleRate * dur, a.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < d.length; i++) {
      const env = Math.sin((i / d.length) * Math.PI) * 0.7;
      d[i] = (Math.random() * 2 - 1) * env * 0.5;
    }
    const src = a.createBufferSource(); src.buffer = buf;
    const f = a.createBiquadFilter(); f.type = 'highpass'; f.frequency.value = 900;
    src.connect(f).connect(a.destination);
    src.start(t);
    [311, 392, 466, 587].forEach((n, i) => {
      const o = a.createOscillator(), g = a.createGain();
      o.type = 'triangle';
      o.frequency.value = n + (Math.random() - 0.5) * 18;
      g.gain.setValueAtTime(0, t + i * 0.08);
      g.gain.linearRampToValueAtTime(0.09, t + i * 0.08 + 0.1);
      g.gain.exponentialRampToValueAtTime(0.001, t + i * 0.08 + 1.0);
      o.connect(g).connect(a.destination);
      o.start(t + i * 0.08); o.stop(t + i * 0.08 + 1.05);
    });
  },
  drumroll() {
    const a = ac(), t = a.currentTime, dur = 1.2;
    const buf = a.createBuffer(1, a.sampleRate * dur, a.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < d.length; i++) {
      const pos = i / d.length;
      const beat = Math.sin(pos * Math.PI * 80) > 0.7 ? 1 : 0.3;
      d[i] = (Math.random() * 2 - 1) * beat * 0.45 * (1 - Math.pow(pos, 3));
    }
    const src = a.createBufferSource(); src.buffer = buf;
    const f = a.createBiquadFilter(); f.type = 'lowpass'; f.frequency.value = 350;
    src.connect(f).connect(a.destination);
    src.start(t);
    // final clap
    const c = a.createBuffer(1, a.sampleRate * 0.12, a.sampleRate);
    const cd = c.getChannelData(0);
    for (let i = 0; i < cd.length; i++) cd[i] = (Math.random() * 2 - 1) * (1 - i / cd.length) * 0.9;
    const cs = a.createBufferSource(); cs.buffer = c;
    cs.connect(a.destination);
    cs.start(t + dur + 0.05);
  },
};

// ── Wheel of Compliments ────────────────────────────────────
function WheelOfCompliments({ compliments, colors, fontFamily, accent, textColor, btnStyle, size = 280 }) {
  const [rotation, setRotation] = React.useState(0);
  const [spinning, setSpinning] = React.useState(false);
  const [result, setResult] = React.useState(null);
  const n = compliments.length;
  const seg = 360 / n;
  const r = size / 2;

  function spin() {
    if (spinning) return;
    setSpinning(true);
    setResult(null);
    const idx = Math.floor(Math.random() * n);
    const target = 360 * 5 + (360 - idx * seg - seg / 2);
    setRotation(prev => prev + target);
    setTimeout(() => { setSpinning(false); setResult(compliments[idx]); sounds.ding(); }, 4200);
  }

  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 14 }}>
      <div style={{ position: 'relative', width: size, height: size }}>
        <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}
          style={{
            transform: `rotate(${rotation}deg)`,
            transition: spinning ? 'transform 4s cubic-bezier(0.18,0.85,0.18,1)' : 'none',
            filter: 'drop-shadow(0 8px 20px rgba(0,0,0,0.25))',
          }}>
          {compliments.map((c, i) => {
            const a0 = (i * seg - 90) * Math.PI / 180;
            const a1 = ((i + 1) * seg - 90) * Math.PI / 180;
            const x0 = r + r * Math.cos(a0), y0 = r + r * Math.sin(a0);
            const x1 = r + r * Math.cos(a1), y1 = r + r * Math.sin(a1);
            const large = seg > 180 ? 1 : 0;
            const path = `M${r},${r} L${x0},${y0} A${r},${r} 0 ${large} 1 ${x1},${y1} Z`;
            const segCenterDeg = i * seg + seg / 2;
            const ta = (segCenterDeg - 90) * Math.PI / 180;
            const tr = r * 0.58;
            const tx = r + tr * Math.cos(ta), ty = r + tr * Math.sin(ta);
            const text = c.length > 22 ? c.slice(0, 20) + '…' : c;
            // Radial orientation: each label runs along its own spoke so adjacent labels can't collide.
            // Smart-flip bottom-half segments so every label stays within ±90° of horizontal.
            let textRot = segCenterDeg - 90;
            if (textRot > 90) textRot -= 180;
            else if (textRot < -90) textRot += 180;
            return (
              <g key={i}>
                <path d={path} fill={colors[i % colors.length]} stroke="rgba(0,0,0,0.18)" strokeWidth="1.5" />
                <text x={tx} y={ty} textAnchor="middle" dominantBaseline="middle"
                  transform={`rotate(${textRot} ${tx} ${ty})`}
                  fontFamily={fontFamily} fontSize="12" fontWeight="800" fill="#fff"
                  letterSpacing="0.4" style={{ paintOrder: 'stroke', stroke: 'rgba(0,0,0,0.55)', strokeWidth: '1px' }}>
                  {text}
                </text>
              </g>
            );
          })}
          <circle cx={r} cy={r} r={22} fill={accent} stroke="#fff" strokeWidth="3" />
          <text x={r} y={r} textAnchor="middle" dominantBaseline="middle" fontSize="18">🎂</text>
        </svg>
        <div style={{
          position: 'absolute', top: -6, left: '50%', transform: 'translateX(-50%)',
          width: 0, height: 0,
          borderLeft: '15px solid transparent', borderRight: '15px solid transparent',
          borderTop: `26px solid ${accent}`,
          filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.35))', zIndex: 2,
        }} />
      </div>
      <button onClick={spin} disabled={spinning} style={btnStyle}>
        {spinning ? '✨ spinning ✨' : '🎡 SPIN'}
      </button>
      <div style={{
        minHeight: 60, fontFamily, fontWeight: 700, fontSize: 16, textAlign: 'center',
        padding: '8px 12px', color: textColor, maxWidth: size + 30,
      }}>
        {result && (
          <div style={{ animation: 'sarahPop 0.6s cubic-bezier(0.2,1.4,0.4,1)' }}>
            "{result}"
          </div>
        )}
      </div>
    </div>
  );
}

// ── Catch the cake mini-game ────────────────────────────────
function CatchCake({ duration = 25, emoji = ['🎂', '🧁', '🍰'], badEmoji = ['🥦', '🥬'],
                    fontFamily, accent, fieldStyle, btnStyle, textColor = '#111', width = 320, height = 360 }) {
  const [state, setState] = React.useState('idle');
  const [score, setScore] = React.useState(0);
  const [time, setTime] = React.useState(duration);
  const [drops, setDrops] = React.useState([]);
  const idRef = React.useRef(0);

  React.useEffect(() => {
    if (state !== 'playing') return;
    const tick = setInterval(() => {
      setTime(t => {
        if (t <= 1) { setState('done'); return 0; }
        return t - 1;
      });
    }, 1000);
    let spawnDelay = 520;
    const spawn = setInterval(() => {
      const bad = Math.random() < 0.20;
      const emo = bad ? badEmoji[Math.floor(Math.random() * badEmoji.length)]
                      : emoji[Math.floor(Math.random() * emoji.length)];
      setDrops(ds => [...ds, {
        id: idRef.current++,
        x: 16 + Math.random() * (width - 64),
        emo, bad,
        start: performance.now(),
        dur: 2100 + Math.random() * 1200,
        rot: (Math.random() - 0.5) * 60,
      }]);
    }, spawnDelay);
    const cleanup = setInterval(() => {
      const now = performance.now();
      setDrops(ds => ds.filter(d => now - d.start < d.dur));
    }, 240);
    return () => { clearInterval(tick); clearInterval(spawn); clearInterval(cleanup); };
  }, [state]);

  function start() { setScore(0); setTime(duration); setDrops([]); setState('playing'); }

  function tap(e, d) {
    e.stopPropagation();
    setDrops(ds => ds.filter(x => x.id !== d.id));
    if (d.bad) { setScore(s => Math.max(0, s - 2)); sounds.boing(); }
    else { setScore(s => s + 1); sounds.ding(); }
    // small confetti at the catch site
    const host = e.currentTarget.closest('[data-confetti-root]');
    if (host && host.__burst) host.__burst(e.clientX, e.clientY, 14);
  }

  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
      <div data-no-confetti style={{
        position: 'relative', width, height, overflow: 'hidden',
        ...fieldStyle,
      }}>
        {drops.map(d => (
          <div key={d.id} onClick={(e) => tap(e, d)}
            style={{
              position: 'absolute', left: d.x, top: -44, fontSize: 38, cursor: 'pointer',
              animation: `sarahFall ${d.dur}ms linear forwards`,
              userSelect: 'none', WebkitUserSelect: 'none',
              transform: `rotate(${d.rot}deg)`,
              filter: d.bad ? 'grayscale(0.3) hue-rotate(20deg)' : 'none',
              willChange: 'transform',
            }}>{d.emo}</div>
        ))}
        {state === 'idle' && (
          <div style={{
            position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column',
            alignItems: 'center', justifyContent: 'center', gap: 12, padding: 24, textAlign: 'center',
            fontFamily, color: textColor,
          }}>
            <div style={{ fontSize: 13, fontWeight: 600, opacity: 0.85, lineHeight: 1.4 }}>
              Tap the cakes 🎂<br />Dodge the sprouts 🥬
            </div>
            <button onClick={start} style={btnStyle}>▶ START</button>
          </div>
        )}
        {state === 'done' && (
          <div style={{
            position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column',
            alignItems: 'center', justifyContent: 'center', gap: 10,
            fontFamily, background: 'rgba(255,255,255,0.94)', color: textColor,
          }}>
            <div style={{ fontSize: 64, fontWeight: 900, color: accent, lineHeight: 1 }}>{score}</div>
            <div style={{ fontSize: 14, fontWeight: 700, letterSpacing: 0.5 }}>
              {score >= 30 ? '🏆 CAKE LEGEND' : score >= 18 ? '🎉 GREAT JOB' : score >= 8 ? '🎂 NICE CATCH' : '🍰 ONE MORE TRY?'}
            </div>
            <button onClick={start} style={{ ...btnStyle, marginTop: 6 }}>↻ AGAIN</button>
          </div>
        )}
      </div>
      <div style={{
        display: 'flex', gap: 18, fontFamily, fontWeight: 800, fontSize: 14,
        color: textColor, minHeight: 22, visibility: state === 'playing' ? 'visible' : 'hidden',
      }}>
        <span>🎂 {score}</span>
        <span>⏱ {time}s</span>
      </div>
    </div>
  );
}

// ── Soundboard ──────────────────────────────────────────────
function SoundBoard({ buttons, fontFamily, baseStyle, cols = 3 }) {
  return (
    <div style={{
      display: 'grid', gridTemplateColumns: `repeat(${cols},1fr)`, gap: 10, width: '100%',
    }}>
      {buttons.map((b, i) => (
        <button key={i} onClick={(e) => { sounds[b.sound](); }}
          style={{
            ...baseStyle, padding: '14px 6px', fontFamily, fontWeight: 800, fontSize: 11,
            textTransform: 'uppercase', letterSpacing: 0.8, cursor: 'pointer',
            display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6,
          }}>
          <span style={{ fontSize: 26 }}>{b.icon}</span>
          {b.label}
        </button>
      ))}
    </div>
  );
}

Object.assign(window, { useConfetti, sounds, WheelOfCompliments, CatchCake, SoundBoard });
