// Pure audio resampling + channel downmixing utilities.
//
// These are dependency-free numeric helpers used by the decode pipeline to get
// arbitrary PCM into the 16 kHz mono shape Whisper expects. No platform code,
// no Node builtins — just Float32Array math.

import { WHISPER_SAMPLE_RATE } from '../types';

/**
 * Resample `input` from `fromRate` to `toRate` using linear interpolation.
 *
 * - If `fromRate === toRate`, returns a *copy* (never the same instance).
 * - Output length is `Math.round(input.length * toRate / fromRate)`.
 * - Empty input yields an empty Float32Array.
 *
 * Linear interpolation maps each output index `i` back to the continuous source
 * position `i * fromRate / toRate`, then blends the two neighboring input
 * samples. The last source sample is clamped so we never read past the end.
 */
export function resampleLinear(
  input: Float32Array,
  fromRate: number,
  toRate: number,
): Float32Array {
  if (input.length === 0) {
    return new Float32Array(0);
  }
  if (fromRate === toRate) {
    // Copy: callers rely on getting a distinct array instance.
    return input.slice();
  }

  const outLength = Math.round((input.length * toRate) / fromRate);
  if (outLength <= 0) {
    return new Float32Array(0);
  }

  const output = new Float32Array(outLength);
  const ratio = fromRate / toRate;
  const lastIndex = input.length - 1;

  for (let i = 0; i < outLength; i++) {
    const srcPos = i * ratio;
    let i0 = Math.floor(srcPos);
    if (i0 > lastIndex) {
      i0 = lastIndex;
    }
    const frac = srcPos - i0;
    const i1 = i0 < lastIndex ? i0 + 1 : lastIndex;

    // Guarded indexed access for noUncheckedIndexedAccess: i0/i1 are clamped to
    // [0, lastIndex], so these are always defined, but we coalesce to be safe.
    const s0 = input[i0] ?? 0;
    const s1 = input[i1] ?? 0;
    output[i] = s0 + (s1 - s0) * frac;
  }

  return output;
}

/**
 * Average a set of equal-length channels into a single mono channel.
 *
 * - `[]` -> empty Float32Array.
 * - A single channel -> a copy (distinct instance).
 * - Otherwise the per-sample mean across all channels.
 *
 * Channels are assumed to share the same length; the first channel's length
 * defines the output length.
 */
export function downmixToMono(channels: Float32Array[]): Float32Array {
  if (channels.length === 0) {
    return new Float32Array(0);
  }

  const first = channels[0]!;
  if (channels.length === 1) {
    return first.slice();
  }

  const length = first.length;
  const out = new Float32Array(length);
  const channelCount = channels.length;

  for (let c = 0; c < channelCount; c++) {
    const channel = channels[c]!;
    for (let i = 0; i < length; i++) {
      out[i] = (out[i] ?? 0) + (channel[i] ?? 0);
    }
  }

  for (let i = 0; i < length; i++) {
    out[i] = (out[i] ?? 0) / channelCount;
  }

  return out;
}

/**
 * Downmix the given channels to mono, then resample to 16 kHz (Whisper rate).
 * Convenience composition of {@link downmixToMono} and {@link resampleLinear}.
 */
export function toMono16k(
  channels: Float32Array[],
  fromRate: number,
): Float32Array {
  const mono = downmixToMono(channels);
  return resampleLinear(mono, fromRate, WHISPER_SAMPLE_RATE);
}
