// Audio engine for the walkie-talkie:
//  - mic capture via MediaRecorder
//  - radio-style FX chain (bandpass + waveshaper + noise mix) applied on playback
//  - beep tones (TX start, TX end / roger)
//  - persistent low static hiss while powered on

const RadioAudio = (() => {
  const STREAM_BUFFER_SIZE = 1024;
  const STREAM_START_BUFFER_SEC = 0.04;
  const STREAM_UNDERRUN_GUARD_SEC = 0.006;
  const STREAM_END_TAIL_MS = 20;

  let ctx = null;
  let masterGain = null;
  let hissNode = null;     // background hiss source
  let hissGain = null;
  let muted = false;
  let volume = 0.7;
  let squelch = 0.2; // 0..1 — higher = quieter background noise

  function squelchFactor(){
    // exponential: SQL 0 -> 1.0 (full hiss), SQL 1 -> ~0.0
    return Math.pow(1 - squelch, 2.2);
  }
  function setSquelch(s){
    squelch = Math.max(0, Math.min(1, s));
    if(hissGain) hissGain.gain.value = (muted ? 0 : volume) * 0.025 * squelchFactor();
  }

  function ensure(){
    if(ctx) return ctx;
    ctx = new (window.AudioContext || window.webkitAudioContext)();
    masterGain = ctx.createGain();
    masterGain.gain.value = volume;
    masterGain.connect(ctx.destination);
    return ctx;
  }

  function resume(){ ensure(); if(ctx.state === 'suspended') ctx.resume(); }

  function setVolume(v){
    volume = Math.max(0, Math.min(1, v));
    if(masterGain) masterGain.gain.value = muted ? 0 : volume;
    if(hissGain) hissGain.gain.value = (muted ? 0 : volume) * 0.025 * squelchFactor();
  }
  function setMuted(m){
    muted = m;
    if(masterGain) masterGain.gain.value = muted ? 0 : volume;
    if(hissGain) hissGain.gain.value = (muted ? 0 : volume) * 0.025 * squelchFactor();
  }

  // ---- background hiss (white-ish noise, gentle) ----
  function startHiss(){
    ensure();
    if(hissNode) return;
    const bufSize = 2 * ctx.sampleRate;
    const buf = ctx.createBuffer(1, bufSize, ctx.sampleRate);
    const data = buf.getChannelData(0);
    for(let i=0;i<bufSize;i++){
      // pinkish noise — softer than pure white
      data[i] = (Math.random()*2 - 1) * 0.4;
    }
    const src = ctx.createBufferSource();
    src.buffer = buf;
    src.loop = true;

    const hp = ctx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value = 1200;
    const lp = ctx.createBiquadFilter(); lp.type='lowpass';  lp.frequency.value = 4500;

    hissGain = ctx.createGain();
    hissGain.gain.value = (muted ? 0 : volume) * 0.025 * squelchFactor();

    src.connect(hp).connect(lp).connect(hissGain).connect(masterGain);
    src.start();
    hissNode = src;
  }
  function stopHiss(){
    if(hissNode){ try{ hissNode.stop(); }catch(e){} hissNode.disconnect(); hissNode = null; }
  }

  // ---- beeps ----
  function beep({freq=1200, dur=0.09, type='square', gain=0.18}={}){
    ensure(); resume();
    const o = ctx.createOscillator();
    const g = ctx.createGain();
    o.type = type;
    o.frequency.value = freq;
    g.gain.setValueAtTime(0, ctx.currentTime);
    g.gain.linearRampToValueAtTime(gain, ctx.currentTime + 0.005);
    g.gain.linearRampToValueAtTime(0, ctx.currentTime + dur);
    o.connect(g).connect(masterGain);
    o.start();
    o.stop(ctx.currentTime + dur + 0.02);
  }
  function txStartTone(){ beep({freq:1400, dur:0.08}); }
  function txEndTone(){ beep({freq:900, dur:0.12, type:'square'}); }
  function rxStartTone(){ beep({freq:1800, dur:0.05, gain:0.10}); }
  function rxEndTone(){ beep({freq:700, dur:0.08, gain:0.12}); }
  function clickTone(){ beep({freq:2400, dur:0.02, gain:0.08, type:'square'}); }
  function errorTone(){
    beep({freq:300, dur:0.12, type:'sawtooth', gain:0.14});
    setTimeout(()=>beep({freq:240, dur:0.18, type:'sawtooth', gain:0.14}), 130);
  }

  // ---- waveshaper curve for "radio crunch" ----
  function makeDistortionCurve(amount){
    const n = 1024;
    const curve = new Float32Array(n);
    const k = amount;
    for(let i=0;i<n;i++){
      const x = (i*2)/n - 1;
      curve[i] = (1 + k) * x / (1 + k * Math.abs(x));
    }
    return curve;
  }

  // Play an audio blob through the radio FX chain.
  // returns a promise that resolves when playback ends.
  async function playRadio(blob, {opts}={}){
    ensure(); resume();
    const arr = await blob.arrayBuffer();
    let audioBuf;
    try{ audioBuf = await ctx.decodeAudioData(arr.slice(0)); }
    catch(e){ console.warn('decode failed', e); return; }

    const src = ctx.createBufferSource();
    src.buffer = audioBuf;

    // bandpass to mimic narrow radio bandwidth (~300–3000 Hz)
    const hp = ctx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value = 350;
    const lp = ctx.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value = 3200;

    // a tilt/peaking filter for the "tinny" mid bump
    const peak = ctx.createBiquadFilter(); peak.type='peaking'; peak.frequency.value=1800; peak.Q.value=1.2; peak.gain.value=6;

    // soft clipping for grit
    const shaper = ctx.createWaveShaper();
    shaper.curve = makeDistortionCurve(8);
    shaper.oversample = '2x';

    const voiceGain = ctx.createGain();
    voiceGain.gain.value = 1.0;

    // a burst of louder hiss layered behind transmission
    const noiseBuf = ctx.createBuffer(1, ctx.sampleRate * (audioBuf.duration + 0.1), ctx.sampleRate);
    const nd = noiseBuf.getChannelData(0);
    for(let i=0;i<nd.length;i++) nd[i] = (Math.random()*2-1)*0.6;
    const noiseSrc = ctx.createBufferSource();
    noiseSrc.buffer = noiseBuf;
    const noiseHP = ctx.createBiquadFilter(); noiseHP.type='highpass'; noiseHP.frequency.value=1500;
    const noiseLP = ctx.createBiquadFilter(); noiseLP.type='lowpass'; noiseLP.frequency.value=4000;
    const noiseGain = ctx.createGain();
    noiseGain.gain.value = 0.045;

    src.connect(hp).connect(lp).connect(peak).connect(shaper).connect(voiceGain).connect(masterGain);
    noiseSrc.connect(noiseHP).connect(noiseLP).connect(noiseGain).connect(masterGain);

    return new Promise(resolve => {
      src.onended = () => {
        try{ noiseSrc.stop(); }catch(e){}
        resolve();
      };
      src.start();
      noiseSrc.start();
    });
  }

  // ---- streaming mic capture (real-time, low-latency) ----
  // Emits Float32 PCM chunks (~23ms each at 44.1kHz with 1024 buffer).
  let streamProc = null;
  let streamSilent = null;
  let streamSrcNode = null;
  async function startStreaming({onMeta, onChunk}){
    ensure(); resume();
    if(!mediaStream){
      mediaStream = await navigator.mediaDevices.getUserMedia({audio:{
        echoCancellation:true, noiseSuppression:true, autoGainControl:true
      }});
    }
    const sampleRate = ctx.sampleRate;
    onMeta && onMeta({sampleRate});
    streamSrcNode = ctx.createMediaStreamSource(mediaStream);
    streamProc = ctx.createScriptProcessor(STREAM_BUFFER_SIZE, 1, 1);
    streamProc.onaudioprocess = (e) => {
      const ch = e.inputBuffer.getChannelData(0);
      const copy = new Float32Array(ch.length);
      copy.set(ch);
      onChunk(copy);
    };
    // ScriptProcessor only runs when connected to destination — route through silent gain.
    streamSilent = ctx.createGain(); streamSilent.gain.value = 0;
    streamSrcNode.connect(streamProc);
    streamProc.connect(streamSilent);
    streamSilent.connect(ctx.destination);
  }
  function stopStreaming(){
    try{ streamSrcNode && streamSrcNode.disconnect(); }catch(e){}
    try{ streamProc && streamProc.disconnect(); }catch(e){}
    try{ streamSilent && streamSilent.disconnect(); }catch(e){}
    if(streamProc) streamProc.onaudioprocess = null;
    streamProc = null; streamSilent = null; streamSrcNode = null;
  }

  // Streaming player — schedule incoming PCM chunks back-to-back through
  // optional FX chain. Returns { feed(pcm), end() }.
  function makeStreamPlayer({sampleRate, applyFX=true}){
    ensure(); resume();
    let nextTime = ctx.currentTime + STREAM_START_BUFFER_SEC;

    const voiceGain = ctx.createGain();
    voiceGain.gain.value = applyFX ? 1.0 : 0.9;

    let noiseSrc = null;
    if(applyFX){
      const hp = ctx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value = 350;
      const lp = ctx.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value = 3200;
      const peak = ctx.createBiquadFilter(); peak.type='peaking'; peak.frequency.value=1800; peak.Q.value=1.2; peak.gain.value=6;
      const shaper = ctx.createWaveShaper();
      shaper.curve = makeDistortionCurve(8);
      shaper.oversample = '2x';
      voiceGain.connect(hp).connect(lp).connect(peak).connect(shaper).connect(masterGain);

      // layered hiss during transmission
      const noiseBuf = ctx.createBuffer(1, ctx.sampleRate, ctx.sampleRate);
      const nd = noiseBuf.getChannelData(0);
      for(let i=0;i<nd.length;i++) nd[i] = (Math.random()*2-1)*0.6;
      noiseSrc = ctx.createBufferSource();
      noiseSrc.buffer = noiseBuf;
      noiseSrc.loop = true;
      const noiseHP = ctx.createBiquadFilter(); noiseHP.type='highpass'; noiseHP.frequency.value=1500;
      const noiseLP = ctx.createBiquadFilter(); noiseLP.type='lowpass'; noiseLP.frequency.value=4200;
      const noiseGain = ctx.createGain(); noiseGain.gain.value = 0.05;
      noiseSrc.connect(noiseHP).connect(noiseLP).connect(noiseGain).connect(masterGain);
      noiseSrc.start();
    } else {
      voiceGain.connect(masterGain);
    }

    return {
      feed(pcm){
        if(!pcm || !pcm.length) return;
        const buf = ctx.createBuffer(1, pcm.length, sampleRate);
        buf.getChannelData(0).set(pcm);
        const src = ctx.createBufferSource();
        src.buffer = buf;
        src.connect(voiceGain);
        const startAt = Math.max(ctx.currentTime + STREAM_UNDERRUN_GUARD_SEC, nextTime);
        src.start(startAt);
        nextTime = startAt + buf.duration;
      },
      end(){
        const tail = Math.max(0, (nextTime - ctx.currentTime)*1000 + STREAM_END_TAIL_MS);
        return new Promise(r => setTimeout(() => {
          try{ noiseSrc && noiseSrc.stop(); }catch(e){}
          r();
        }, tail));
      },
    };
  }

  // ---- mic recording (legacy blob mode — kept for fallback) ----
  let mediaStream = null;
  let recorder = null;
  let chunks = [];

  async function startRecording(){
    ensure(); resume();
    if(!mediaStream){
      mediaStream = await navigator.mediaDevices.getUserMedia({audio:{
        echoCancellation:true, noiseSuppression:true, autoGainControl:true
      }});
    }
    chunks = [];
    const mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus'
               : MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm'
               : '';
    recorder = new MediaRecorder(mediaStream, mime ? {mimeType:mime} : undefined);
    recorder.ondataavailable = e => { if(e.data && e.data.size) chunks.push(e.data); };
    recorder.start();
    return mediaStream;
  }
  async function stopRecording(){
    return new Promise(resolve => {
      if(!recorder || recorder.state === 'inactive') return resolve(null);
      recorder.onstop = () => {
        const type = recorder.mimeType || 'audio/webm';
        const blob = new Blob(chunks, {type});
        resolve(blob);
      };
      recorder.stop();
    });
  }
  function releaseMic(){
    if(mediaStream){
      mediaStream.getTracks().forEach(t=>t.stop());
      mediaStream = null;
    }
  }

  // mic VU meter — attach analyser to mediaStream and call cb with level [0,1]
  let analyser = null;
  let vuRaf = 0;
  function startVU(cb){
    if(!mediaStream) return;
    ensure();
    const srcNode = ctx.createMediaStreamSource(mediaStream);
    analyser = ctx.createAnalyser();
    analyser.fftSize = 512;
    srcNode.connect(analyser);
    const data = new Uint8Array(analyser.fftSize);
    const tick = () => {
      analyser.getByteTimeDomainData(data);
      let sum=0;
      for(let i=0;i<data.length;i++){ const v = (data[i]-128)/128; sum += v*v; }
      const rms = Math.sqrt(sum/data.length);
      cb(Math.min(1, rms*3));
      vuRaf = requestAnimationFrame(tick);
    };
    tick();
  }
  function stopVU(){
    if(vuRaf) cancelAnimationFrame(vuRaf);
    vuRaf = 0;
    analyser = null;
  }

  return {
    resume, setVolume, setMuted, setSquelch,
    startHiss, stopHiss,
    txStartTone, txEndTone, rxStartTone, rxEndTone, clickTone, errorTone,
    playRadio,
    startStreaming, stopStreaming, makeStreamPlayer,
    startRecording, stopRecording, releaseMic,
    startVU, stopVU,
  };
})();

window.RadioAudio = RadioAudio;
