///|
/// Bounded push/pull encoder. Input is interleaved signed scalar samples.
/// Internal PCM never exceeds block_size * channels. Drain next after push;
/// an accepted count of zero means the pending output/block must be drained.
struct Encoder {
  config : EncoderConfig
  pcm : Array[Int32]
  digest : Md5
  plan_scratch : EncoderPlanScratch
  seek_points : Array[SeekPoint]
  mut seek_cursor : Int32
  mut frame_offset : Int64
  mut queued : Bytes?
  mut ended : Bool
  mut failed : FlacError?
  mut summary : EncoderSummary?
  mut samples : Int64
  mut frames : Int64
  mut min_frame : Int32
  mut max_frame : Int32
  mut peak_pcm : Int32
}

///|
fn validate_encoder_config(config : EncoderConfig) -> Unit raise FlacError {
  let f = config.format
  if f.channels < 1 ||
    f.channels > 8 ||
    f.bits_per_sample < 4 ||
    f.bits_per_sample > 32 {
    raise Invalid(0L, "encoder requires 1-8 channels and 4-32-bit PCM")
  }
  if f.sample_rate < 0 ||
    f.sample_rate > 1048575 ||
    (f.sample_rate == 0 && !config.non_audio) {
    raise Invalid(0L, "invalid encoder rate; zero requires explicit non_audio")
  }
  if config.block_size < 16 || config.block_size > 65535 {
    raise Invalid(0L, "encoder block size must be 16-65535")
  }
  if config.effort < 0 || config.effort > 8 {
    raise Invalid(0L, "encoder effort must be 0-8")
  }
  if config.max_metadata_bytes < 42 {
    raise Limit("encoder metadata limit is below STREAMINFO size")
  }
}

///|
pub fn Encoder::new(config : EncoderConfig) -> Encoder raise FlacError {
  validate_encoder_config(config)
  let config = { ..config, metadata: config.metadata[:].to_owned(), }
  let header = encoder_header(config, 0L, Bytes::make(16, 0), 0, 0)
  let seek_points = if config.metadata.any(block => block.kind == 3) {
    let (metadata, _) = parse_metadata(
      header,
      max_metadata_bytes=config.max_metadata_bytes,
    )
    metadata.seek_points
  } else {
    []
  }
  {
    config,
    pcm: [],
    digest: Md5::new(),
    plan_scratch: EncoderPlanScratch::new(),
    seek_points,
    seek_cursor: 0,
    frame_offset: 0L,
    queued: Some(header),
    ended: false,
    failed: None,
    summary: None,
    samples: 0L,
    frames: 0L,
    min_frame: 0,
    max_frame: 0,
    peak_pcm: 0,
  }
}

///|
/// Accepted scalar count; each accepted sample is copied and range checked.
/// Partial interchannel groups may span pushes, but must be complete at finish.
pub fn Encoder::push(
  self : Encoder,
  samples : Array[Int32],
) -> Int32 raise FlacError {
  self.push_view(samples[:])
}

///|
/// Borrows only for this call; accepted scalars are checked before any copy.
/// Whole-buffer encoding uses this path to avoid a temporary owned slice.
fn Encoder::push_view(
  self : Encoder,
  samples : ArrayView[Int32],
) -> Int32 raise FlacError {
  if self.failed is Some(error) {
    raise error
  }
  if self.ended {
    raise Invalid(self.samples, "PCM supplied after encoder finish")
  }
  if self.queued is Some(_) {
    return 0
  }
  let capacity = self.config.block_size * self.config.format.channels
  let room = capacity - self.pcm.length()
  let count = if samples.length() < room { samples.length() } else { room }
  let bound = 1L << (self.config.format.bits_per_sample - 1)
  // Validate the accepted prefix before changing state, so a bad chunk cannot
  // leave a partially accepted frame that might later be mistaken for success.
  for i in 0..= bound {
      let error = Invalid(self.samples, "PCM sample outside declared bit depth")
      self.failed = Some(error)
      raise error
    }
  }
  self.pcm.append(samples[:count])
  if self.pcm.length() > self.peak_pcm {
    self.peak_pcm = self.pcm.length()
  }
  count
}

///|
/// Number of scalar PCM values retained, excluding caller-owned output.
pub fn Encoder::buffered_samples(self : Encoder) -> Int32 {
  self.pcm.length()
}

///|
/// Peak retained scalar values; bounded by configured block_size * channels.
pub fn Encoder::peak_buffered_samples(self : Encoder) -> Int32 {
  self.peak_pcm
}

///|
/// Declares PCM EOF. Empty streams and incomplete interchannel groups fail.
pub fn Encoder::finish(self : Encoder) -> Unit raise FlacError {
  if self.failed is Some(error) {
    raise error
  }
  if self.ended {
    return
  }
  let error = if self.pcm.length() % self.config.format.channels != 0 {
    Some(Invalid(self.samples, "incomplete interleaved PCM sample group"))
  } else if self.samples == 0L && self.pcm.is_empty() {
    Some(Invalid(0L, "cannot encode an empty FLAC stream"))
  } else {
    None
  }
  if error is Some(error) {
    self.failed = Some(error)
    raise error
  }
  self.ended = true
}

///|
/// Adaptive variable blocking observes a complete configured window. Strong
/// changes in roughness split it in half; the decision never depends on pushes.
fn Encoder::block_length(self : Encoder) -> Int32 {
  let available = self.pcm.length() / self.config.format.channels
  let maximum = self.config.block_size
  if available < maximum {
    return available
  }
  if !self.config.variable_block || maximum < 32 {
    return maximum
  }
  let channels = self.config.format.channels
  let half = maximum / 2
  let mut early = 0L
  let mut late = 0L
  for i in 1.. (late + 1L) * 4L || late > (early + 1L) * 4L {
    half
  } else {
    maximum
  }
}

///|
/// Pull owned compressed bytes. Initial bytes contain unknown streaming
/// STREAMINFO fields; EncoderDone returns the same-length finalized header.
pub fn Encoder::next(self : Encoder) -> EncodeEvent raise FlacError {
  if self.failed is Some(error) {
    raise error
  }
  self.read_encoded() catch {
    error => {
      self.failed = Some(error)
      raise error
    }
  }
}

///|
fn Encoder::read_encoded(self : Encoder) -> EncodeEvent raise FlacError {
  if self.queued is Some(bytes) {
    self.queued = None
    return EncodedBytes(bytes)
  }
  if self.summary is Some(summary) {
    return EncoderDone(summary)
  }
  let format = self.config.format
  let full = self.pcm.length() >= self.config.block_size * format.channels
  if !full && !self.ended {
    return NeedPcm
  }
  if self.pcm.is_empty() {
    if self.seek_cursor < self.seek_points.length() &&
      self.seek_points[self.seek_cursor].sample != 0xffffffffffffffffUL {
      raise Invalid(
        self.samples,
        "encoder SEEKTABLE point is outside encoded audio",
      )
    }
    let md5 = self.digest.digest()
    let final_header = encoder_header(
      self.config,
      self.samples,
      md5,
      self.min_frame,
      self.max_frame,
    )
    let summary : EncoderSummary = {
      total_samples: self.samples,
      frames: self.frames,
      min_frame_size: self.min_frame,
      max_frame_size: self.max_frame,
      md5,
      final_header,
    }
    self.summary = Some(summary)
    return EncoderDone(summary)
  }
  let count = self.block_length()
  if self.samples > 68719476735L - count.to_int64() {
    raise Limit("FLAC sample count exceeds 36 bits")
  }
  if !self.config.variable_block && self.frames >= 2147483648L {
    raise Limit("FLAC frame number exceeds 31 bits")
  }
  let planes = Array::makei(format.channels, c => {
    Array::makei(count, i => self.pcm[i * format.channels + c])
  })
  let number = if self.config.variable_block {
    self.samples
  } else {
    self.frames
  }
  // new validates the format/effort/window, push_view atomically validates
  // every stored scalar, and these owned planes have the same positive count.
  let bytes = encode_frame_validated(
    format,
    planes,
    number,
    self.config.variable_block,
    self.config.effort,
    plan_scratch=self.plan_scratch,
  )
  if bytes.length() > 16777215 {
    raise Limit("encoded frame exceeds STREAMINFO size field")
  }
  if self.seek_cursor < self.seek_points.length() {
    let point = self.seek_points[self.seek_cursor]
    if point.sample != 0xffffffffffffffffUL &&
      point.sample < (self.samples + count.to_int64()).to_uint64() {
      if point.sample != self.samples.to_uint64() ||
        point.offset != self.frame_offset.to_uint64() ||
        point.samples != count {
        raise Invalid(
          self.samples,
          "encoder SEEKTABLE point does not match encoded frame",
        )
      }
      self.seek_cursor += 1
    }
  }
  md5_update_frame(self.digest, {
    sample_number: self.samples,
    number,
    variable_block: self.config.variable_block,
    sample_rate: format.sample_rate,
    bits_per_sample: format.bits_per_sample,
    channels: planes,
    block_size: count,
    byte_offset: 0L,
    byte_length: bytes.length(),
  })
  self.samples += count.to_int64()
  self.frames += 1L
  self.frame_offset += bytes.length().to_int64()
  if self.min_frame == 0 || bytes.length() < self.min_frame {
    self.min_frame = bytes.length()
  }
  if bytes.length() > self.max_frame {
    self.max_frame = bytes.length()
  }
  let consumed = count * format.channels
  if consumed == self.pcm.length() {
    // Fixed windows and the final partial window consume all retained PCM.
    self.pcm.clear()
  } else {
    // Adaptive variable blocking may emit half a full window; preserve the
    // remaining validated samples for the next push/next cycle.
    let retained = self.pcm[consumed:].to_owned()
    self.pcm.clear()
    for sample in retained {
      self.pcm.push(sample)
    }
  }
  EncodedBytes(bytes)
}

///|
/// Whole-buffer convenience API. Interleaved PCM is not modified. The returned
/// native FLAC has final STREAMINFO counts, frame bounds and source PCM MD5.
pub fn encode_pcm(
  samples : Array[Int32],
  config : EncoderConfig,
) -> Bytes raise FlacError {
  let encoder = Encoder::new(config)
  let chunks : Array[Bytes] = []
  let mut cursor = 0
  let mut length = 0L
  while true {
    match encoder.next() {
      EncodedBytes(bytes) => {
        length += bytes.length().to_int64()
        if length > 2147483647L {
          raise Limit("whole encoded buffer exceeds maximum Bytes length")
        }
        chunks.push(bytes)
      }
      NeedPcm =>
        if cursor == samples.length() {
          encoder.finish()
        } else {
          let room = config.block_size * config.format.channels
          let remaining = samples.length() - cursor
          let count = if remaining < room { remaining } else { room }
          cursor += encoder.push_view(samples[cursor:cursor + count])
        }
      EncoderDone(summary) => {
        let out = @buffer.Buffer()
        out.write_bytes(summary.final_header)
        for i in 1..