/* ============================================================
   combo.codes — TypeFX
   Tiny Web Audio keystroke engine for the logo typewriter.
   No audio files: each keypress is a short filtered noise burst
   + a low "thock"; the closing ";" gets a bright two-tone ding.
   Exposed on window.TypeFX. Respects browser autoplay policy —
   call unlock() from a user gesture before sound will play.
   ============================================================ */
window.TypeFX = (function () {
  var ctx = null;

  function ensure() {
    if (!ctx) {
      var AC = window.AudioContext || window.webkitAudioContext;
      if (!AC) return null;
      try { ctx = new AC(); } catch (e) { return null; }
    }
    return ctx;
  }

  function unlock() {
    var c = ensure();
    if (!c) return false;
    if (c.state === 'suspended' && c.resume) { c.resume(); }
    return c.state === 'running';
  }

  function running() {
    return !!ctx && ctx.state === 'running';
  }

  // one mechanical key click
  function click(opts) {
    var c = ensure();
    if (!c || c.state !== 'running') return;
    var o = opts || {};
    var t = c.currentTime;
    var gain = o.gain != null ? o.gain : 0.14;

    // high-frequency click (the "tick" of the key cap)
    var dur = 0.03;
    var frames = Math.max(1, Math.ceil(c.sampleRate * dur));
    var buf = c.createBuffer(1, frames, c.sampleRate);
    var data = buf.getChannelData(0);
    for (var i = 0; i < frames; i++) {
      data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / frames, 2.6);
    }
    var src = c.createBufferSource();
    src.buffer = buf;
    var bp = c.createBiquadFilter();
    bp.type = 'bandpass';
    bp.frequency.value = o.freq || 2400;
    bp.Q.value = 0.9;
    var g = c.createGain();
    g.gain.value = gain;
    src.connect(bp); bp.connect(g); g.connect(c.destination);
    src.start(t); src.stop(t + dur);

    // low "thock" (the key bottoming out)
    var osc = c.createOscillator();
    osc.type = 'triangle';
    osc.frequency.setValueAtTime(o.thock || 175, t);
    var g2 = c.createGain();
    g2.gain.setValueAtTime(gain * 0.7, t);
    g2.gain.exponentialRampToValueAtTime(0.0008, t + 0.05);
    osc.connect(g2); g2.connect(c.destination);
    osc.start(t); osc.stop(t + 0.06);
  }

  // bright confirmation tone for the closing ";" (fallback if no TTS)
  function ding() {
    var c = ensure();
    if (!c || c.state !== 'running') return;
    var t = c.currentTime;
    var osc = c.createOscillator();
    osc.type = 'square';
    osc.frequency.setValueAtTime(740, t);
    osc.frequency.exponentialRampToValueAtTime(1180, t + 0.05);
    var g = c.createGain();
    g.gain.setValueAtTime(0.0001, t);
    g.gain.exponentialRampToValueAtTime(0.10, t + 0.012);
    g.gain.exponentialRampToValueAtTime(0.0006, t + 0.20);
    osc.connect(g); g.connect(c.destination);
    osc.start(t); osc.stop(t + 0.22);
  }

  // robotic computer-voice announcing the name (game-synth style).
  // Uses the browser speech synth with a low pitch for a mechanical tone.
  function speak(text, opts) {
    try {
      if (!('speechSynthesis' in window)) { ding(); return; }
      var o = opts || {};
      var ss = window.speechSynthesis;
      ss.cancel();
      var u = new SpeechSynthesisUtterance(text);
      u.lang = 'en-US';
      u.rate = o.rate != null ? o.rate : 0.9;
      u.pitch = o.pitch != null ? o.pitch : 0.4;
      u.volume = o.volume != null ? o.volume : 1;
      var voices = ss.getVoices() || [];
      function byName(list) {
        for (var i = 0; i < list.length; i++) {
          var needle = list[i];
          var hit = voices.find(function (v) { return v.name.toLowerCase().indexOf(needle) !== -1; });
          if (hit) return hit;
        }
        return null;
      }
      // classic 80s synth-chip voices first (macOS), then generic English
      var pref =
        (o.voiceName ? byName([String(o.voiceName).toLowerCase()]) : null) ||
        byName(['zarvox', 'trinoids', 'albert', 'ralph', 'fred']) ||
        voices.find(function (v) { return /en[-_]US/i.test(v.lang) && /google/i.test(v.name); }) ||
        voices.find(function (v) { return /en[-_]US/i.test(v.lang); }) ||
        voices.find(function (v) { return /^en/i.test(v.lang); });
      if (pref) u.voice = pref;
      ss.speak(u);
    } catch (e) { ding(); }
  }

  function silence() {
    try { if ('speechSynthesis' in window) window.speechSynthesis.cancel(); } catch (e) {}
  }

  // prime the voice list (populated async in some browsers)
  try {
    if ('speechSynthesis' in window) {
      window.speechSynthesis.getVoices();
      window.speechSynthesis.onvoiceschanged = function () { window.speechSynthesis.getVoices(); };
    }
  } catch (e) {}

  return { unlock: unlock, running: running, click: click, ding: ding, speak: speak, silence: silence };
})();
