///|
/// Fractional filter positions, interpolated rather than sample-quantized.
let phase_count : Int32 = 512

///|
/// Continuous interleaved PCM rate conversion. Rate is input frames advanced
/// per output frame: 2 halves duration and doubles pitch. All channels share
/// the same phase and filter. Rate 1 passes samples through exactly.
pub struct Processor {
  channels : Int32
  rate : Double
  radius : Int32
  coefficients : Array[Array[Double]]
  mut samples : Array[Double]
  mut base_frame : Int64
  mut input_frames : Int64
  mut output_frames : Int64
  mut next_frame : Int64
  mut fraction : Double
  mut finished : Bool
}

///|
/// Original Blackman-windowed sinc converter. Downsampling lowers the cutoff
/// and widens the kernel in input frames, preserving its transition width.
pub fn new(
  channels : Int32,
  rate : Double,
) -> Processor raise @types.AudioError {
  if channels < 1 || channels > 8 {
    raise @types.AudioError("channels must be between 1 and 8")
  }
  @types.validate_ratio(rate, 0.25, 4.0, "rate")
  let stretch = if rate > 1.0 { rate } else { 1.0 }
  let radius = if rate == 1.0 { 0 } else { @math.ceil(48.0 * stretch).to_int() }
  let cutoff = 0.9 / stretch
  let coefficients : Array[Array[Double]] = []
  if radius > 0 {
    for phase = 0; phase <= phase_count; phase = phase + 1 {
      let fraction = phase.to_double() / phase_count.to_double()
      let row : Array[Double] = []
      let mut total = 0.0
      for tap = -radius; tap <= radius; tap = tap + 1 {
        let x = tap.to_double() - fraction
        let normalized = x / radius.to_double()
        let weight = if normalized.abs() >= 1.0 {
          0.0
        } else {
          let sinc_x = @math.PI * cutoff * x
          let sinc = if sinc_x.abs() < 1.0e-12 {
            1.0
          } else {
            @math.sin(sinc_x) / sinc_x
          }
          let window = 0.42 +
            0.5 * @math.cos(@math.PI * normalized) +
            0.08 * @math.cos(2.0 * @math.PI * normalized)
          cutoff * sinc * window
        }
        row.push(weight)
        total += weight
      }
      for i = 0; i < row.length(); i = i + 1 {
        row[i] = row[i] / total
      }
      coefficients.push(row)
    }
  }
  {
    channels,
    rate,
    radius,
    coefficients,
    samples: [],
    base_frame: 0L,
    input_frames: 0L,
    output_frames: 0L,
    next_frame: 0L,
    fraction: 0.0,
    finished: false,
  }
}

///|
/// Push complete frames. Invalid input leaves the processor unchanged.
/// Output is independent of how the input is divided among calls.
pub fn Processor::push(
  self : Processor,
  samples : Array[Double],
) -> Array[Double] raise @types.AudioError {
  if self.finished {
    raise @types.AudioError(
      "cannot push after finish; reset the processor first",
    )
  }
  @types.validate_samples(samples, self.channels)
  let frames = (samples.length() / self.channels).to_int64()
  if self.input_frames > 281474976710656L - frames {
    raise @types.AudioError("stream exceeds the supported frame counter range")
  }
  self.input_frames += frames
  if self.rate == 1.0 {
    self.output_frames += frames
    return samples.copy()
  }
  for sample in samples {
    self.samples.push(sample)
  }
  self.drain(false)
}

///|
/// Finish using endpoint extension. Final length is nearest(input_frames /
/// rate), with half frames rounded upward. Repeated finish calls return empty.
pub fn Processor::finish(self : Processor) -> Array[Double] {
  if self.finished {
    return []
  }
  self.finished = true
  let result = self.drain(true)
  self.samples = []
  result
}

///|
/// Discard sample history and counters, retaining configuration and filters.
pub fn Processor::reset(self : Processor) -> Unit {
  self.samples = []
  self.base_frame = 0L
  self.input_frames = 0L
  self.output_frames = 0L
  self.next_frame = 0L
  self.fraction = 0.0
  self.finished = false
}

///|
/// Input frames retained after a call, excluding returned output.
pub fn Processor::buffered_frames(self : Processor) -> Int32 {
  self.samples.length() / self.channels
}

///|
/// Lookahead in input frames. A shorter stream is emitted by finish.
pub fn Processor::latency_frames(self : Processor) -> Int32 {
  self.radius
}

///|
fn Processor::drain(self : Processor, finishing : Bool) -> Array[Double] {
  let output : Array[Double] = []
  if self.rate == 1.0 || self.input_frames == 0L {
    return output
  }
  let target = (self.input_frames.to_double() / self.rate + 0.5)
    .floor()
    .to_int64()
  while self.output_frames < target &&
        (
          finishing ||
          self.next_frame + self.radius.to_int64() < self.input_frames
        ) {
    let table_position = self.fraction * phase_count.to_double()
    let phase = table_position.floor().to_int()
    let mix = table_position - phase.to_double()
    let low = self.coefficients[phase]
    let high = self.coefficients[phase + 1]
    let center = (self.next_frame - self.base_frame).to_int()
    let last = self.samples.length() / self.channels - 1
    for channel = 0; channel < self.channels; channel = channel + 1 {
      let mut value = 0.0
      for tap = 0; tap < low.length(); tap = tap + 1 {
        let source_frame = center + tap - self.radius
        let source_frame = if source_frame < 0 {
          0
        } else if source_frame > last {
          last
        } else {
          source_frame
        }
        let weight = low[tap] + mix * (high[tap] - low[tap])
        value += self.samples[source_frame * self.channels + channel] * weight
      }
      output.push(value)
    }
    self.output_frames += 1L
    // Fractional phase stays small, avoiding loss of precision from a growing
    // global floating-point timestamp.
    let advanced = self.fraction + self.rate
    let whole = advanced.floor().to_int64()
    self.next_frame += whole
    self.fraction = advanced - whole.to_double()
  }
  if !finishing {
    let earliest = self.next_frame - self.radius.to_int64()
    if earliest > self.base_frame {
      let removable = (earliest - self.base_frame).to_int()
      let available = self.samples.length() / self.channels
      // Retain the endpoint for finish, including after a large rate advance.
      let drop_frames = if removable < available {
        removable
      } else {
        available - 1
      }
      if drop_frames > 0 {
        let replacement : Array[Double] = []
        for i = drop_frames * self.channels
            i < self.samples.length()
            i = i + 1 {
          replacement.push(self.samples[i])
        }
        self.samples = replacement
        self.base_frame += drop_frames.to_int64()
      }
    }
  }
  output
}