///|
/// Incremental ISO BMFF/fMP4 demux for the shared container stream.
///
/// The state keeps metadata and bounded sample descriptors only. Media payload
/// bytes remain in ContainerStreamInput until a complete selected sample is
/// available, then are copied into an owned packet and consumed immediately.
priv struct Mp4StreamBoxHeader {
  typ : Int
  header : Int
  size : Int64?
}

///|
priv struct Mp4StreamMdat {
  end : Int64?
}

///|
/// A finite or size-zero non-media top-level box being discarded. Padding and
/// unknown boxes are streamed through the input buffer instead of being
/// assembled as one retained unit.
priv struct Mp4StreamSkip {
  end : Int64?
}

///|
priv struct Av1Mp4StreamState {
  limits : DecodeLimits
  requested_track_id : Int64?
  mut track : Av1DemuxedTrack?
  mut mp4_track : Mp4Track?
  mut trex : Mp4Trex?
  mut descriptors : Array[Mp4Sample]
  mut next_sample : Int
  mut sample_count : Int
  mut last_sample : Mp4Sample?
  mut fragment_state : Mp4FragmentState
  mut current_mdat : Mp4StreamMdat?
  mut current_skip : Mp4StreamSkip?
  mut parsed_moov : Bool
  mut saw_mdat : Bool
  mut saw_selected_moof : Bool
  mut table_samples : Bool
  mut fragment_samples : Bool
  mut media_before_metadata : Bool
  mut replay_available : Bool
  mut replay_mode : Bool
  mut consumed_through : Int64
}

///|
fn Av1Mp4StreamState::new(
  track_id : Int64?,
  limits : DecodeLimits,
) -> Av1Mp4StreamState {
  {
    limits,
    requested_track_id: track_id,
    track: None,
    mp4_track: None,
    trex: None,
    descriptors: [],
    next_sample: 0,
    sample_count: 0,
    last_sample: None,
    fragment_state: { next_decode_time: 0L, },
    current_mdat: None,
    current_skip: None,
    parsed_moov: false,
    saw_mdat: false,
    saw_selected_moof: false,
    table_samples: false,
    fragment_samples: false,
    media_before_metadata: false,
    replay_available: false,
    replay_mode: false,
    consumed_through: 0L,
  }
}

///|
/// Logical retained compressed bytes. Expanded numeric sample descriptors,
/// allocator overhead, parser scratch copies and already-emitted output are
/// intentionally excluded; max_frames bounds descriptors independently.
fn Av1Mp4StreamState::retained_bytes(self : Av1Mp4StreamState) -> Int {
  match self.track {
    Some(track) => track.codec_initialization.length()
    None => 0
  }
}

///|
fn mp4_stream_header(
  input : ContainerStreamInput,
  eof : Bool,
) -> Result[Mp4StreamBoxHeader?, DecodeError] {
  let available = input.available()
  if available < 8 {
    if eof && available != 0 {
      return Err(
        decode_error(TruncatedInput, "mp4.stream", "Incomplete MP4 box header"),
      )
    }
    return Ok(None)
  }
  let data = input.bytes
  let p = input.start
  let size32 = mp4_u32_wide(data, p, p + 8) catch {
    Mp4Error::Diagnostic(error) => return Err(error)
  }
  let typ = mp4_bits32(data, p + 4, p + 8) catch {
    Mp4Error::Diagnostic(error) => return Err(error)
  }
  if size32 == 1L {
    if available < 16 {
      if eof {
        return Err(
          decode_error(
            TruncatedInput,
            "mp4.stream",
            "Incomplete 64-bit MP4 box header",
          ),
        )
      }
      return Ok(None)
    }
    let size = mp4_u64(data, p + 8, p + 16) catch {
      Mp4Error::Diagnostic(error) => return Err(error)
    }
    if size < 16L {
      return Err(
        decode_error(InvalidData, "mp4.stream", "Invalid 64-bit MP4 box size"),
      )
    }
    return Ok(Some({ typ, header: 16, size: Some(size), }))
  }
  if size32 == 0L {
    if eof {
      return Ok(Some({ typ, header: 8, size: Some(available.to_int64()), }))
    }
    return Ok(Some({ typ, header: 8, size: None, }))
  }
  if size32 < 8L {
    return Err(decode_error(InvalidData, "mp4.stream", "Invalid MP4 box size"))
  }
  Ok(Some({ typ, header: 8, size: Some(size32), }))
}

///|
fn mp4_stream_add(a : Int64, b : Int64) -> Result[Int64, DecodeError] {
  if b < 0L || a < 0L || a > mp4_i64_max - b {
    Err(decode_error(InvalidData, "mp4.stream", "MP4 file offset overflows"))
  } else {
    Ok(a + b)
  }
}

///|
fn Av1Mp4StreamState::mp4_stream_append_samples(
  self : Av1Mp4StreamState,
  samples : Array[Mp4Sample],
  fragment : Bool,
) -> Result[Unit, DecodeError] {
  if samples.length() == 0 {
    return Ok(())
  }
  if fragment && self.table_samples {
    return Err(
      decode_error(
        Unsupported,
        "mp4.stream",
        "Mixed ordinary and fragmented samples are unsupported",
      ),
    )
  }
  if !fragment && self.fragment_samples {
    return Err(
      decode_error(
        Unsupported,
        "mp4.stream",
        "Mixed ordinary and fragmented samples are unsupported",
      ),
    )
  }
  let mut previous = if self.descriptors.length() != 0 {
    let item = self.descriptors[self.descriptors.length() - 1]
    Some(item)
  } else {
    self.last_sample
  }
  for sample in samples {
    if self.sample_count >= self.limits.max_frames {
      return Err(
        decode_error(
          LimitExceeded,
          "mp4.stream",
          "MP4 sample count exceeds max_frames",
        ),
      )
    }
    if sample.size <= 0 {
      return Err(
        decode_error(
          InvalidData,
          "mp4.stream",
          "MP4 sample has an empty payload",
        ),
      )
    }
    if sample.size > self.limits.max_input_bytes {
      return Err(
        decode_error(
          LimitExceeded,
          "mp4.stream",
          "MP4 sample exceeds max_input_bytes",
        ),
      )
    }
    if sample.offset < self.consumed_through &&
      !(self.media_before_metadata && !self.replay_mode) {
      return Err(
        decode_error(
          InvalidData,
          "mp4.stream",
          "Sample offsets precede consumed media",
        ),
      )
    }
    match previous {
      Some(last) => {
        let last_end = match mp4_stream_add(last.offset, last.size.to_int64()) {
          Ok(value) => value
          Err(error) => return Err(error)
        }
        if sample.offset < last.offset || sample.offset < last_end {
          return Err(
            decode_error(
              Unsupported,
              "mp4.stream",
              "Selected sample offsets are not decode-order compatible",
            ),
          )
        }
      }
      None => ()
    }
    self.descriptors.push(sample)
    self.sample_count += 1
    previous = Some(sample)
  }
  if fragment {
    self.fragment_samples = true
    self.saw_selected_moof = true
  } else {
    self.table_samples = true
  }
  Ok(())
}

///|
fn Av1Mp4StreamState::mp4_stream_parse_moov(
  self : Av1Mp4StreamState,
  data : Array[Byte],
) -> Result[Unit, DecodeError] {
  let (parsed, trex) = mp4_parse_moov_metadata(
    data,
    self.requested_track_id,
    self.limits,
  ) catch {
    Mp4Error::Diagnostic(error) => return Err(error)
  }
  let mut table_descriptors : Array[Mp4Sample] = []
  if parsed.has_sample_tables {
    let boxes = match mp4_boxes(data) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    match mp4_child(boxes, -1, mp4_moov) {
      Some(_) => ()
      None =>
        return Err(
          decode_error(
            InvalidData,
            "mp4.stream",
            "MP4 metadata has no moov box",
          ),
        )
    }
    table_descriptors = mp4_table_sample_descriptors(
      data,
      boxes,
      parsed,
      self.limits,
    ) catch {
      Mp4Error::Diagnostic(error) => return Err(error)
    }
  }
  let public_track : Av1DemuxedTrack = {
    format: Mp4,
    track_id: parsed.id,
    width: parsed.width,
    height: parsed.height,
    timescale: parsed.timescale,
    movie_timescale: parsed.movie_timescale,
    edits: Array::makei(parsed.edits.length(), i => parsed.edits[i]),
    // Internal views share this owned configuration; track_info copies it at
    // the public boundary. Retain and charge the compressed bytes only once.
    codec_initialization: parsed.av1c,
    packets: [],
  }
  self.mp4_track = Some(parsed)
  self.trex = trex
  self.track = Some(public_track)
  self.parsed_moov = true
  match self.mp4_stream_append_samples(table_descriptors, false) {
    Err(error) => Err(error)
    Ok(_) => Ok(())
  }
}

///|
fn Av1Mp4StreamState::mp4_stream_parse_moof(
  self : Av1Mp4StreamState,
  data : Array[Byte],
  file_offset : Int64,
) -> Result[Unit, DecodeError] {
  let track = match self.mp4_track {
    Some(value) => value
    None => return Ok(())
  }
  let boxes = match mp4_boxes(data) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let moof = match mp4_child(boxes, -1, mp4_moof) {
    Some(value) => value
    None =>
      return Err(
        decode_error(InvalidData, "mp4.stream", "MP4 fragment has no moof box"),
      )
  }
  let samples : Array[Mp4Sample] = []
  let local_state = self.fragment_state
  mp4_fragment_moof_samples(
    data,
    boxes,
    moof,
    track,
    self.trex,
    local_state,
    file_offset,
    self.limits,
    samples,
  ) catch {
    Mp4Error::Diagnostic(error) => return Err(error)
  }
  self.fragment_state = local_state
  self.mp4_stream_append_samples(samples, true)
}

///|
fn Av1Mp4StreamState::mp4_stream_step_skip(
  self : Av1Mp4StreamState,
  input : ContainerStreamInput,
  eof : Bool,
) -> Result[ContainerStreamStep, DecodeError] {
  let skip = match self.current_skip {
    Some(value) => value
    None =>
      return Err(
        decode_error(InvalidState, "mp4.stream", "Missing skipped-box state"),
      )
  }
  let pos = input.offset()
  match skip.end {
    Some(end) if pos >= end => {
      self.current_skip = None
      return self.step(input, eof)
    }
    _ => ()
  }
  let available = input.available()
  match skip.end {
    Some(end) => {
      let remaining = end - pos
      if available == 0 {
        if eof {
          return Err(
            decode_error(TruncatedInput, "mp4.stream", "Truncated MP4 box"),
          )
        }
        return Ok(Wait)
      }
      let amount = if remaining < available.to_int64() {
        remaining.to_int()
      } else {
        available
      }
      input.consume(amount)
      if amount.to_int64() == remaining {
        self.current_skip = None
      }
      Ok(Progress)
    }
    None => {
      if available > 0 {
        input.consume(available)
        return Ok(Progress)
      }
      if eof {
        self.current_skip = None
        return self.step(input, eof)
      }
      Ok(Wait)
    }
  }
}

///|
fn Av1Mp4StreamState::mp4_stream_step_mdat(
  self : Av1Mp4StreamState,
  input : ContainerStreamInput,
  eof : Bool,
) -> Result[ContainerStreamStep, DecodeError] {
  let mdat = match self.current_mdat {
    Some(value) => value
    None =>
      return Err(decode_error(InvalidState, "mp4.stream", "Missing mdat state"))
  }
  let pos = input.offset()
  match mdat.end {
    Some(end) if pos >= end => {
      self.current_mdat = None
      return self.step(input, eof)
    }
    _ => ()
  }
  let available = input.available()
  if self.track is None || (self.media_before_metadata && !self.replay_mode) {
    self.media_before_metadata = true
    match mdat.end {
      Some(end) => {
        let remaining = end - pos
        if available == 0 {
          if eof {
            return Err(
              decode_error(
                TruncatedInput,
                "mp4.stream",
                "Truncated mdat payload",
              ),
            )
          }
          return Ok(Wait)
        }
        let amount = if remaining < available.to_int64() {
          remaining.to_int()
        } else {
          available
        }
        input.consume(amount)
        self.consumed_through = input.offset()
        if amount.to_int64() == remaining {
          self.current_mdat = None
        }
        Ok(Progress)
      }
      None => {
        if available > 0 {
          input.consume(available)
          self.consumed_through = input.offset()
          return Ok(Progress)
        }
        if eof {
          self.current_mdat = None
          return self.mp4_stream_finish(eof)
        }
        Ok(Wait)
      }
    }
  } else if self.next_sample >= self.descriptors.length() {
    match mdat.end {
      Some(end) => {
        let remaining = end - pos
        if available == 0 {
          if eof {
            return Err(
              decode_error(
                TruncatedInput,
                "mp4.stream",
                "Truncated mdat payload",
              ),
            )
          }
          return Ok(Wait)
        }
        let amount = if remaining < available.to_int64() {
          remaining.to_int()
        } else {
          available
        }
        input.consume(amount)
        self.consumed_through = input.offset()
        if amount.to_int64() == remaining {
          self.current_mdat = None
        }
        Ok(Progress)
      }
      None => {
        if available > 0 {
          input.consume(available)
          self.consumed_through = input.offset()
          return Ok(Progress)
        }
        if eof {
          self.current_mdat = None
          return self.mp4_stream_finish(eof)
        }
        Ok(Wait)
      }
    }
  } else {
    let sample = self.descriptors[self.next_sample]
    if sample.offset < pos {
      return Err(
        decode_error(
          InvalidData,
          "mp4.stream",
          "Sample range was passed before its mdat payload",
        ),
      )
    }
    match mdat.end {
      Some(end) if sample.offset >= end => {
        let remaining = end - pos
        if available == 0 {
          if eof {
            return Err(
              decode_error(
                TruncatedInput,
                "mp4.stream",
                "Truncated mdat payload",
              ),
            )
          }
          return Ok(Wait)
        }
        let amount = if remaining < available.to_int64() {
          remaining.to_int()
        } else {
          available
        }
        input.consume(amount)
        self.consumed_through = input.offset()
        if amount.to_int64() == remaining {
          self.current_mdat = None
        }
        return Ok(Progress)
      }
      _ => ()
    }
    let sample_end = match
      mp4_stream_add(sample.offset, sample.size.to_int64()) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    if mdat.end is None && eof {
      let eof_end = match mp4_stream_add(pos, available.to_int64()) {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      if sample.offset >= eof_end || sample_end > eof_end {
        return Err(
          decode_error(
            InvalidData,
            "mp4.stream",
            "Sample range is outside the size-zero mdat",
          ),
        )
      }
    }
    match mdat.end {
      Some(end) if sample.offset < pos || sample_end > end =>
        return Err(
          decode_error(
            InvalidData,
            "mp4.stream",
            "Sample range is outside its mdat box",
          ),
        )
      _ => ()
    }
    if sample.offset > pos {
      let gap = sample.offset - pos
      if available == 0 {
        if eof {
          return Err(
            decode_error(
              TruncatedInput,
              "mp4.stream",
              "Sample offset exceeds available input",
            ),
          )
        }
        return Ok(Wait)
      }
      let amount = if gap < available.to_int64() {
        gap.to_int()
      } else {
        available
      }
      input.consume(amount)
      self.consumed_through = input.offset()
      return Ok(Progress)
    }
    if sample.size > self.limits.max_input_bytes {
      return Err(
        decode_error(
          LimitExceeded,
          "mp4.stream",
          "Sample payload exceeds max_input_bytes",
        ),
      )
    }
    if available < sample.size {
      if eof {
        return Err(
          decode_error(
            TruncatedInput,
            "mp4.stream",
            "Truncated MP4 sample payload",
          ),
        )
      }
      return Ok(Wait)
    }
    let packet : Av1Packet = {
      data: Array::makei(sample.size, i => input.bytes[input.start + i]),
      presentation_timestamp: Some(sample.pts),
      decode_timestamp: Some(sample.dts),
      duration: Some(sample.duration),
      sync_sample: sample.sync,
    }
    input.consume(sample.size)
    self.consumed_through = input.offset()
    self.next_sample += 1
    if self.fragment_samples {
      // Fragment descriptors are only needed until their payload is emitted.
      // Keep the last one as a scalar ordering guard for the next moof.
      self.last_sample = Some(sample)
      let remaining = self.descriptors.length() - self.next_sample
      self.descriptors = Array::makei(remaining, i => {
        self.descriptors[self.next_sample + i]
      })
      self.next_sample = 0
    }
    Ok(Packet(packet))
  }
}

///|
fn Av1Mp4StreamState::mp4_stream_finish(
  self : Av1Mp4StreamState,
  eof : Bool,
) -> Result[ContainerStreamStep, DecodeError] {
  if self.current_mdat is Some(_) {
    if eof {
      return Err(
        decode_error(TruncatedInput, "mp4.stream", "Truncated mdat payload"),
      )
    }
    return Ok(Wait)
  }
  if !eof {
    return Ok(Wait)
  }
  if self.track is None {
    return Err(decode_error(InvalidData, "mp4.stream", "MP4 has no moov box"))
  }
  if self.replay_available && self.media_before_metadata && !self.replay_mode {
    return Ok(ReplayRequired)
  }
  if self.next_sample < self.descriptors.length() {
    return Err(
      decode_error(
        InvalidData,
        "mp4.stream",
        "Selected MP4 sample range is outside every mdat box",
      ),
    )
  }
  if !self.saw_mdat {
    return Err(decode_error(InvalidData, "mp4.stream", "MP4 has no mdat box"))
  }
  let has_tables = match self.mp4_track {
    Some(value) => value.has_sample_tables
    None => false
  }
  if !has_tables && !self.saw_selected_moof {
    return Err(
      decode_error(
        InvalidData,
        "mp4.stream",
        "Selected fragmented track has no samples",
      ),
    )
  }
  Ok(End)
}

///|
fn Av1Mp4StreamState::step(
  self : Av1Mp4StreamState,
  input : ContainerStreamInput,
  eof : Bool,
) -> Result[ContainerStreamStep, DecodeError] {
  if self.current_skip is Some(_) {
    return self.mp4_stream_step_skip(input, eof)
  }
  if self.current_mdat is Some(_) {
    return self.mp4_stream_step_mdat(input, eof)
  }
  if input.available() == 0 {
    return self.mp4_stream_finish(eof)
  }
  let header = match mp4_stream_header(input, eof) {
    Err(error) => return Err(error)
    Ok(None) => return Ok(Wait)
    Ok(Some(value)) => value
  }
  let box_start = input.offset()
  let size = match header.size {
    Some(value) => value
    None => {
      if header.typ == mp4_moov || header.typ == mp4_moof {
        // A size-zero metadata box extends to EOF. Keep it under the usual
        // input cap, then parse its bounded body when finish supplies EOF.
        return Ok(Wait)
      }
      if header.typ != mp4_mdat {
        input.consume(header.header)
        self.current_skip = Some({ end: None, })
        return Ok(Progress)
      }
      input.consume(header.header)
      self.saw_mdat = true
      self.current_mdat = Some({ end: None, })
      if self.track is None {
        self.media_before_metadata = true
      }
      return Ok(Progress)
    }
  }
  if header.typ == mp4_mdat {
    let end = match mp4_stream_add(box_start, size) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    input.consume(header.header)
    self.saw_mdat = true
    self.current_mdat = Some({ end: Some(end), })
    if self.track is None {
      self.media_before_metadata = true
    }
    return Ok(Progress)
  }
  if header.typ != mp4_moov && header.typ != mp4_moof {
    let end = match mp4_stream_add(box_start, size) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    input.consume(header.header)
    self.current_skip = Some({ end: Some(end), })
    return Ok(Progress)
  }
  if size > self.limits.max_input_bytes.to_int64() {
    return Err(
      decode_error(
        LimitExceeded,
        "mp4.stream",
        "MP4 metadata box exceeds max_input_bytes",
      ),
    )
  }
  let size_i = size.to_int()
  if input.available() < size_i {
    return Ok(Wait)
  }
  if header.typ == mp4_moov {
    if self.replay_mode {
      input.consume(size_i)
      return Ok(Progress)
    }
    if self.parsed_moov {
      return Err(
        decode_error(
          InvalidData,
          "mp4.stream",
          "MP4 contains multiple moov boxes",
        ),
      )
    }
    let data = Array::makei(size_i, i => input.bytes[input.start + i])
    match self.mp4_stream_parse_moov(data) {
      Err(error) => return Err(error)
      Ok(_) => ()
    }
    input.consume(size_i)
    if self.media_before_metadata {
      self.replay_available = true
    }
    return Ok(Progress)
  }
  if header.typ == mp4_moof {
    if self.track is None || (self.media_before_metadata && !self.replay_mode) {
      self.media_before_metadata = true
      input.consume(size_i)
      return Ok(Progress)
    }
    let data = Array::makei(size_i, i => input.bytes[input.start + i])
    match self.mp4_stream_parse_moof(data, box_start) {
      Err(error) => return Err(error)
      Ok(_) => ()
    }
    input.consume(size_i)
    return Ok(Progress)
  }
  input.consume(size_i)
  Ok(Progress)
}

///|
/// The outer stream calls this only after a completed metadata-only tail pass.
/// The parsed track metadata and ordinary table descriptors are retained;
/// fragmented descriptors are rebuilt from moofs as the original file is
/// replayed from offset zero.
fn Av1Mp4StreamState::replay(
  self : Av1Mp4StreamState,
) -> Result[Av1Mp4StreamState, DecodeError] {
  if !self.replay_available {
    return Err(
      decode_error(
        InvalidState,
        "mp4.stream",
        "No completed MP4 replay request",
      ),
    )
  }
  self.replay_available = false
  self.replay_mode = true
  self.current_mdat = None
  self.current_skip = None
  self.next_sample = 0
  self.saw_mdat = false
  self.saw_selected_moof = false
  self.media_before_metadata = false
  self.consumed_through = 0L
  self.fragment_state.next_decode_time = 0L
  if self.fragment_samples {
    self.descriptors = []
    self.sample_count = 0
    self.last_sample = None
    self.fragment_samples = false
    self.table_samples = false
  }
  Ok(self)
}