import { spawn } from "node:child_process";

const FFMPEG = process.env.FFMPEG_PATH || "ffmpeg";

function decodeAll(args) {
  return new Promise((resolve, reject) => {
    const ff = spawn(FFMPEG, ["-hide_banner", "-loglevel", "error", ...args]);
    const chunks = [];
    const err = [];
    ff.stdout.on("data", (d) => chunks.push(d));
    ff.stderr.on("data", (d) => err.push(d));
    ff.on("error", reject);
    ff.on("close", (code) => {
      if (code === 0) return resolve(Buffer.concat(chunks));
      reject(new Error(`ffmpeg exited ${code}: ${Buffer.concat(err).toString().slice(0, 400)}`));
    });
  });
}

// Decode any audio file to raw PCM and push it into an RTCAudioSource at 10ms cadence.
// Returns { done: Promise, stop() }.
export async function streamAudioFile(source, filePath, opts = {}) {
  const sampleRate = opts.sampleRate ?? 48000;
  const channels = opts.channels ?? 1;
  const samplesPerFrame = (sampleRate / 100) * channels; // 10ms
  const bytesPerFrame = samplesPerFrame * 2;
  const pcm = await decodeAll(["-i", filePath, "-f", "s16le", "-acodec", "pcm_s16le", "-ac", String(channels), "-ar", String(sampleRate), "pipe:1"]);

  let offset = 0;
  let timer = null;
  let resolveDone;
  const done = new Promise((r) => (resolveDone = r));
  const tick = () => {
    if (offset >= pcm.length) {
      clearInterval(timer);
      resolveDone({ frames: Math.ceil(pcm.length / bytesPerFrame) });
      return;
    }
    const samples = new Int16Array(samplesPerFrame); // own 960-byte buffer, no aliasing
    Buffer.from(samples.buffer).set(pcm.subarray(offset, Math.min(offset + bytesPerFrame, pcm.length)));
    offset += bytesPerFrame;
    source.onData({ samples, sampleRate, bitsPerSample: 16, channelCount: channels, numberOfFrames: sampleRate / 100 });
  };
  timer = setInterval(tick, 10);
  return { done, stop: () => { if (timer) clearInterval(timer); resolveDone?.({ stopped: true }); } };
}

// Decode any video file to raw I420 frames and push them into an RTCVideoSource at the given fps.
export async function streamVideoFile(source, filePath, opts = {}) {
  const width = opts.width ?? 320;
  const height = opts.height ?? 240;
  const fps = opts.fps ?? 15;
  const frameBytes = (width * height * 3) / 2;
  const raw = await decodeAll(["-i", filePath, "-f", "rawvideo", "-pix_fmt", "yuv420p", "-s", `${width}x${height}`, "-r", String(fps), "pipe:1"]);

  let offset = 0;
  let timer = null;
  let resolveDone;
  const done = new Promise((r) => (resolveDone = r));
  const tick = () => {
    if (offset + frameBytes > raw.length) {
      clearInterval(timer);
      resolveDone({ frames: Math.floor(raw.length / frameBytes) });
      return;
    }
    const data = new Uint8ClampedArray(raw.subarray(offset, offset + frameBytes));
    offset += frameBytes;
    source.onFrame({ width, height, data });
  };
  timer = setInterval(tick, Math.round(1000 / fps));
  return { done, stop: () => { if (timer) clearInterval(timer); resolveDone?.({ stopped: true }); } };
}

// Pipe a received RTCAudioSink's PCM frames into a file (wav by default). Returns { stop() }.
export function recordAudioSink(sink, filePath, opts = {}) {
  let ff = null;
  let sampleRate = opts.sampleRate ?? 48000;
  let channels = opts.channels ?? 1;
  let frames = 0;
  const start = (sr, ch) => {
    sampleRate = sr;
    channels = ch;
    ff = spawn(FFMPEG, ["-hide_banner", "-loglevel", "error", "-y", "-f", "s16le", "-ar", String(sr), "-ac", String(ch), "-i", "pipe:0", filePath]);
  };
  sink.ondata = (d) => {
    if (!ff) start(d.sampleRate, d.channelCount);
    frames++;
    ff.stdin.write(Buffer.from(d.samples.buffer, d.samples.byteOffset, d.samples.byteLength));
  };
  return {
    get frames() { return frames; },
    stop: () => new Promise((res) => {
      sink.stop?.();
      if (!ff) return res({ frames: 0 });
      ff.stdin.end();
      ff.on("close", () => res({ frames, sampleRate, channels }));
    }),
  };
}

// Pipe a received RTCVideoSink's I420 frames into a file (mp4 by default). Returns { stop() }.
export function recordVideoSink(sink, filePath, opts = {}) {
  let ff = null;
  let fps = opts.fps ?? 15;
  let frames = 0;
  let dims = null;
  const start = (w, h) => {
    dims = { w, h };
    ff = spawn(FFMPEG, ["-hide_banner", "-loglevel", "error", "-y", "-f", "rawvideo", "-pix_fmt", "yuv420p", "-s", `${w}x${h}`, "-r", String(fps), "-i", "pipe:0", "-pix_fmt", "yuv420p", filePath]);
  };
  sink.onframe = ({ frame }) => {
    if (!ff) start(frame.width, frame.height);
    if (frame.width !== dims.w || frame.height !== dims.h) return;
    frames++;
    ff.stdin.write(Buffer.from(frame.data.buffer, frame.data.byteOffset, frame.data.byteLength));
  };
  return {
    get frames() { return frames; },
    get dims() { return dims; },
    stop: () => new Promise((res) => {
      sink.stop?.();
      if (!ff) return res({ frames: 0 });
      ff.stdin.end();
      ff.on("close", () => res({ frames, dims }));
    }),
  };
}
