///|
/// Streaming writer for evolved records, the runtime engine behind derived
/// codecs (mirrors desert-rust's `AdtSerializer`).
///
/// For a record with `V` evolution steps (`AdtMetadata::version`):
/// - `new_v0` (V = 0) writes the `0` version byte and fields go straight to
///   the active output — wire-identical to a plain tuple.
/// - `new` (V > 0) writes the version byte, routes every field into the
///   buffer of its generation chunk (`write_field`), and `finish` appends the
///   evolution header followed by the chunk bytes.
///
/// Fields not mentioned in any `FieldAdded` step belong to chunk 0.
pub struct AdtWriter {
  priv metadata : AdtMetadata
  priv ctx : SerializationContext
  /// One buffer per version, `None` until a field is routed there.
  priv buffers : Array[ByteOutput?]
  /// Per-chunk last field position, tracked only when the record has
  /// `FieldMadeOptional` steps (needed to emit their header entries).
  priv last_index_per_chunk : Array[Int32]?
  priv field_indices : @hashmap.HashMap[String, FieldPosition]?
}

///|
/// Create a writer for a version-0 record: writes the version byte `0` and
/// lets fields serialize directly to the active output.
pub fn AdtWriter::new_v0(
  metadata : AdtMetadata,
  ctx : SerializationContext,
) -> AdtWriter raise DesertFailure {
  if metadata.version != 0 {
    abort(
      "desert: AdtWriter::new_v0 requires version-0 metadata (got version \{metadata.version})",
    )
  }
  ctx.write_u8(b'\x00')
  {
    metadata,
    ctx,
    buffers: [],
    last_index_per_chunk: None,
    field_indices: None,
  }
}

///|
/// Create a writer for an evolved record: writes the version byte and
/// prepares the per-generation chunk buffers.
pub fn AdtWriter::new(
  metadata : AdtMetadata,
  ctx : SerializationContext,
) -> AdtWriter raise DesertFailure {
  ctx.write_u8(metadata.version.to_byte())
  let chunk_count = metadata.evolution_steps.length()
  let buffers : Array[ByteOutput?] = []
  for _i in 0.. Unit raise DesertFailure {
  let chunk = self.metadata.generation_of(field_name)
  if self.buffers.length() > 0 {
    let buffer = match self.buffers[chunk] {
      Some(existing) => existing
      None => ByteOutput::new()
    }
    let obj : &BinaryOutput = buffer
    self.ctx.push_output(obj)
    BinarySerializer::serialize(value, self.ctx)
    let _ = self.ctx.pop_output()
    self.buffers[chunk] = Some(buffer)
    self.record_field_index(field_name, chunk)
  } else {
    BinarySerializer::serialize(value, self.ctx)
  }
}

///|
/// Write a coproduct constructor index directly to the active output,
/// followed by the case body produced by `serialize_case`.
pub fn AdtWriter::write_constructor(
  self : AdtWriter,
  constructor_idx : UInt32,
  serialize_case : (SerializationContext) -> Unit raise DesertFailure,
) -> Unit raise DesertFailure {
  self.ctx.write_var_u32(constructor_idx)
  serialize_case(self.ctx)
}

///|
/// Append the evolution header and the chunk data (evolved records only;
/// a no-op for version 0).
pub fn AdtWriter::finish(self : AdtWriter) -> Unit raise DesertFailure {
  if self.buffers.length() == 0 {
    return
  }
  for v, step in self.metadata.evolution_steps {
    let serialized : SerializedEvolutionStep = match step {
      InitialVersion | FieldAdded(_) =>
        FieldAddedToNewChunk(chunk_size(self.buffers[v]))
      FieldMadeOptional(name) =>
        match self.field_index_of(name) {
          Some(position) => FieldMadeOptionalStep(position)
          None =>
            if self.metadata.removed_fields.contains(name) {
              FieldRemovedStep(name)
            } else {
              raise UnknownFieldReferenceInEvolutionStep(name)
            }
        }
      FieldRemoved(name) | FieldMadeTransient(name) => FieldRemovedStep(name)
    }
    serialized.serialize(self.ctx)
  }
  for buffer in self.buffers {
    match buffer {
      Some(b) => {
        let bytes = b.to_bytes()
        if bytes.length() > 0 {
          self.ctx.write_bytes(bytes.exact_view())
        }
      }
      None => ()
    }
  }
}

///|
fn chunk_size(buffer : ByteOutput?) -> Int32 {
  match buffer {
    Some(b) => b.length()
    None => 0
  }
}

///|
fn AdtWriter::field_index_of(
  self : AdtWriter,
  field_name : String,
) -> FieldPosition? {
  match self.field_indices {
    Some(indices) => indices.get(field_name)
    None => None
  }
}

///|
fn AdtWriter::record_field_index(
  self : AdtWriter,
  field_name : String,
  chunk : Int32,
) -> Unit {
  match self.last_index_per_chunk {
    Some(indices) => {
      let new_index = indices[chunk] + 1
      indices[chunk] = new_index
      match self.field_indices {
        Some(map) => map.set(field_name, FieldPosition::new(chunk, new_index))
        None => ()
      }
    }
    None => ()
  }
}