import { onBeforeUnmount, ref } from "vue";

export function useVoiceRecorder(onComplete: (audio: Blob) => Promise<void>) {
  const recording = ref(false);
  const elapsedSeconds = ref(0);
  let stream: MediaStream | null = null;
  let context: AudioContext | null = null;
  let source: MediaStreamAudioSourceNode | null = null;
  let processor: ScriptProcessorNode | null = null;
  let mutedOutput: GainNode | null = null;
  let timer: number | null = null;
  let buffers: Float32Array[] = [];
  let sourceSampleRate = 48000;
  let cancelled = false;

  async function start(limitSeconds: number) {
    if (!navigator.mediaDevices?.getUserMedia || typeof AudioContext === "undefined") {
      throw new Error("当前浏览器不支持麦克风录音");
    }
    stream = await navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true } });
    context = new AudioContext();
    await context.resume();
    sourceSampleRate = context.sampleRate;
    source = context.createMediaStreamSource(stream);
    processor = context.createScriptProcessor(4096, 1, 1);
    mutedOutput = context.createGain();
    mutedOutput.gain.value = 0;
    buffers = [];
    cancelled = false;
    elapsedSeconds.value = 0;
    processor.onaudioprocess = (event) => buffers.push(new Float32Array(event.inputBuffer.getChannelData(0)));
    source.connect(processor);
    processor.connect(mutedOutput);
    mutedOutput.connect(context.destination);
    recording.value = true;
    const maxSeconds = Math.max(5, Math.min(60, limitSeconds));
    timer = window.setInterval(() => {
      elapsedSeconds.value += 1;
      if (elapsedSeconds.value >= maxSeconds) void stop();
    }, 1000);
  }

  async function stop() {
    if (!recording.value) return;
    recording.value = false;
    const captured = buffers;
    cleanup();
    if (!cancelled && captured.length) {
      await onComplete(encodeWav(captured, sourceSampleRate));
    }
  }

  function cancel() {
    cancelled = true;
    recording.value = false;
    cleanup();
  }

  function cleanup() {
    if (timer !== null) window.clearInterval(timer);
    timer = null;
    processor?.disconnect();
    source?.disconnect();
    mutedOutput?.disconnect();
    stream?.getTracks().forEach((track) => track.stop());
    void context?.close();
    processor = null;
    source = null;
    mutedOutput = null;
    stream = null;
    context = null;
    buffers = [];
  }

  onBeforeUnmount(cancel);
  return { recording, elapsedSeconds, start, stop, cancel };
}

function encodeWav(buffers: Float32Array[], inputRate: number) {
  const samples = merge(buffers);
  const outputRate = 16000;
  const ratio = inputRate / outputRate;
  const outputLength = Math.floor(samples.length / ratio);
  const buffer = new ArrayBuffer(44 + outputLength * 2);
  const view = new DataView(buffer);
  writeText(view, 0, "RIFF");
  view.setUint32(4, 36 + outputLength * 2, true);
  writeText(view, 8, "WAVEfmt ");
  view.setUint32(16, 16, true);
  view.setUint16(20, 1, true);
  view.setUint16(22, 1, true);
  view.setUint32(24, outputRate, true);
  view.setUint32(28, outputRate * 2, true);
  view.setUint16(32, 2, true);
  view.setUint16(34, 16, true);
  writeText(view, 36, "data");
  view.setUint32(40, outputLength * 2, true);
  for (let index = 0; index < outputLength; index += 1) {
    const start = Math.floor(index * ratio);
    const end = Math.max(start + 1, Math.floor((index + 1) * ratio));
    let sum = 0;
    for (let cursor = start; cursor < end && cursor < samples.length; cursor += 1) sum += samples[cursor];
    const value = Math.max(-1, Math.min(1, sum / (end - start)));
    view.setInt16(44 + index * 2, value < 0 ? value * 0x8000 : value * 0x7fff, true);
  }
  return new Blob([buffer], { type: "audio/wav" });
}

function merge(buffers: Float32Array[]) {
  const result = new Float32Array(buffers.reduce((total, item) => total + item.length, 0));
  let offset = 0;
  buffers.forEach((item) => { result.set(item, offset); offset += item.length; });
  return result;
}

function writeText(view: DataView, offset: number, value: string) {
  for (let index = 0; index < value.length; index += 1) view.setUint8(offset + index, value.charCodeAt(index));
}
