/** Decode Android VoiceClip format ([be uint16 len][opus packet]…) for Web playback. */
const PICH_OPUS_SAMPLE_RATE = 16000;
let pichOpusDecoder = null;
let pichOpusReady = null;

function b64ToBytes(b64) {
  const bin = atob(b64);
  const out = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
  return out;
}

function looksLikePichOpus(bytes) {
  if (!bytes || bytes.length < 4) return false;
  const len = (bytes[0] << 8) | bytes[1];
  return len >= 1 && len <= 1275 && len + 2 <= bytes.length;
}

async function ensurePichOpusDecoder() {
  if (pichOpusReady) return pichOpusReady;
  pichOpusReady = (async () => {
    const lib = window['opus-decoder'];
    if (!lib || !lib.OpusDecoder) throw new Error('opus-decoder 未載入');
    pichOpusDecoder = new lib.OpusDecoder({ sampleRate: PICH_OPUS_SAMPLE_RATE, channels: 1 });
    await pichOpusDecoder.ready;
    return pichOpusDecoder;
  })();
  return pichOpusReady;
}

async function decodePichOpusToPcm(bytes) {
  const dec = await ensurePichOpusDecoder();
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
  const floats = [];
  let off = 0;
  while (off + 2 <= bytes.length) {
    const len = view.getUint16(off, false);
    off += 2;
    if (len <= 0 || off + len > bytes.length) break;
    const pkt = bytes.subarray(off, off + len);
    off += len;
    const { channelData, samplesDecoded } = dec.decodeFrame(pkt);
    if (samplesDecoded > 0 && channelData[0]) floats.push(...channelData[0]);
  }
  return new Float32Array(floats);
}

async function playPichOpusB64(b64) {
  const bytes = b64ToBytes(b64);
  if (!looksLikePichOpus(bytes)) throw new Error('非 APK Opus 格式');
  const pcm = await decodePichOpusToPcm(bytes);
  if (!pcm.length) throw new Error('無音訊資料');
  const ctx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: PICH_OPUS_SAMPLE_RATE });
  const buf = ctx.createBuffer(1, pcm.length, PICH_OPUS_SAMPLE_RATE);
  buf.getChannelData(0).set(pcm);
  const src = ctx.createBufferSource();
  src.buffer = buf;
  src.connect(ctx.destination);
  return new Promise((resolve, reject) => {
    src.onended = () => { ctx.close().catch(() => {}); resolve(); };
    src.onerror = reject;
    src.start(0);
  });
}

function isApkVoiceMime(mime) {
  return mime === 'audio/opus' || mime === 'application/x-pich-opus';
}

window.looksLikePichOpus = looksLikePichOpus;
window.isApkVoiceMime = isApkVoiceMime;
window.playPichOpusB64 = playPichOpusB64;
