///|
/// Reader for evolved records, the runtime engine behind derived codecs
/// (mirrors desert-rust's `AdtDeserializer`).
///
/// The generated (or hand-written) codec first reads the stored version
/// byte, then either uses the direct field reads (both versions are 0) or
/// constructs this reader, which parses the evolution header, fences each
/// chunk behind an input region, and applies the compatibility rules:
///
/// - fields introduced after the stored version fall back to their
///   `FieldAdded` defaults (or fail without one)
/// - fields removed in the serialized version fail on read
///   (`FieldRemovedInSerializedVersion`) — optional fields return `None`
/// - a field made optional after the stored version is read bare and wrapped
///   in `Some`
/// - an old reader finding a made-optional field serialized as `None` fails
///   with `NonOptionalFieldSerializedAsNone`
/// - unknown header entries (code 0) from future versions are ignored
pub struct AdtReader {
  priv metadata : AdtMetadata
  priv ctx : DeserializationContext
  /// The version the data was written with.
  priv stored_version : Int32
  /// FieldPosition -> version index, from the wire header.
  priv made_optional_at : @hashmap.HashMap[Int32, Int32]?
  /// Names removed according to the wire header.
  priv removed_fields : @hashmap.HashMap[String, Bool]?
  /// Remaining-region handle per version (updated after each chunk read).
  priv inputs : Array[InputRegion]?
  priv last_index_per_chunk : Array[Int32]?
}

///|
/// Byte range fencing one generation chunk on the input. An empty region
/// rejects all reads.
priv struct InputRegion {
  start : Int32
  size : Int32
}

///|
fn InputRegion::empty() -> InputRegion {
  { start: 0, size: 0, }
}

///|
/// Create a reader for data whose *stored* version is 0: fields follow the
/// version byte directly (the metadata may still describe a newer schema —
/// new readers reading old data).
pub fn AdtReader::new_v0(
  metadata : AdtMetadata,
  ctx : DeserializationContext,
) -> AdtReader {
  // Note: metadata may have any version — this constructor is used whenever
  // the *stored* version is 0 (a new reader reading old data), where chunk
  // machinery is not needed.
  {
    metadata,
    ctx,
    stored_version: 0,
    made_optional_at: None,
    removed_fields: None,
    inputs: None,
    last_index_per_chunk: None,
  }
}

///|
/// Create a reader for data stored with `stored_version`. Parses the
/// `stored_version + 1` header entries and slices the input into per-version
/// chunk regions.
pub fn AdtReader::new(
  metadata : AdtMetadata,
  ctx : DeserializationContext,
  stored_version : Int32,
) -> AdtReader raise DesertFailure {
  let made_optional : @hashmap.HashMap[Int32, Int32] = @hashmap.HashMap([])
  let removed : @hashmap.HashMap[String, Bool] = @hashmap.HashMap([])
  // Pass 1: parse the header entries sequentially (they are contiguous
  // varint payloads; chunk data follows the whole header).
  let steps : Array[SerializedEvolutionStep] = []
  for _i in 0..<(stored_version + 1) {
    steps.push(SerializedEvolutionStep::deserialize(ctx))
  }
  // Pass 2: slice the chunk regions sequentially from the current cursor.
  let inputs : Array[InputRegion] = []
  for idx, step in steps {
    match step {
      FieldAddedToNewChunk(size) => {
        let start = ctx.pos()
        ctx.skip(size)
        inputs.push({ start, size, })
      }
      FieldMadeOptionalStep(position) => {
        made_optional.set(position.to_byte(), idx)
        inputs.push(InputRegion::empty())
      }
      FieldRemovedStep(field_name) => {
        removed.set(field_name, true)
        inputs.push(InputRegion::empty())
      }
      UnknownStep => inputs.push(InputRegion::empty())
    }
  }
  let made_optional_at = if made_optional.is_empty() {
    None
  } else {
    Some(made_optional)
  }
  {
    metadata,
    ctx,
    stored_version,
    made_optional_at,
    removed_fields: if removed.is_empty() {
      None
    } else {
      Some(removed)
    },
    inputs: Some(inputs),
    last_index_per_chunk: match made_optional_at {
      Some(_) => {
        let arr : Array[Int32] = []
        for _i in 0.. None
    },
  }
}

///|
/// Read a required field.
///
/// `default` supplies the value for fields that were added after the stored
/// version; without a default such data fails with
/// `FieldWithoutDefaultValueIsMissing`.
pub fn[T : BinaryDeserializer] AdtReader::read_field(
  self : AdtReader,
  field_name : String,
  default : T?,
) -> T raise DesertFailure {
  match self.removed_fields {
    Some(removed) if removed.contains(field_name) =>
      raise FieldRemovedInSerializedVersion(field_name)
    _ => ()
  }
  let chunk = self.metadata.generation_of(field_name)
  let field_position = self.record_field_index(chunk)
  if self.stored_version < chunk {
    match default {
      Some(value) => value
      None => raise FieldWithoutDefaultValueIsMissing(field_name)
    }
  } else {
    let has_inputs = self.push_chunk_region(chunk)
    let result = if self.is_made_optional_on_wire(field_position) {
      // The writer's codec had this field optional; a bare `None` cannot be
      // represented in the non-optional reader type.
      let is_defined = self.ctx.read_u8() != b'\x00'
      if is_defined {
        T::deserialize(self.ctx)
      } else {
        raise NonOptionalFieldSerializedAsNone(field_name)
      }
    } else {
      T::deserialize(self.ctx)
    }
    if has_inputs {
      self.pop_chunk_region(chunk)
    }
    result
  }
}

///|
/// Read an optional field, applying the `FieldMadeOptional` compatibility
/// rules. `default` supplies the whole `Option` for fields added after the
/// stored version.
pub fn[T : BinaryDeserializer] AdtReader::read_optional_field(
  self : AdtReader,
  field_name : String,
  default : T??,
) -> T? raise DesertFailure {
  match self.removed_fields {
    Some(removed) if removed.contains(field_name) => return None
    _ => ()
  }
  let chunk = self.metadata.generation_of(field_name)
  let opt_since = match self.metadata.made_optional_at.get(field_name) {
    Some(version) => version
    None => 0
  }
  let _ = self.record_field_index(chunk)
  if self.stored_version < chunk {
    match default {
      Some(value) => value
      None =>
        raise DeserializationFailure(
          "Field \{field_name} is not in the stream and does not have a default value",
        )
    }
  } else {
    let has_inputs = self.push_chunk_region(chunk)
    let result = if self.stored_version < opt_since {
      // Older writer stored the bare value.
      Some(T::deserialize(self.ctx))
    } else {
      self.ctx.read_optional_value()
    }
    if has_inputs {
      self.pop_chunk_region(chunk)
    }
    result
  }
}

///|
/// Read the coproduct constructor index (the record itself being an enum).
pub fn AdtReader::read_constructor_idx(
  self : AdtReader,
) -> Int32 raise DesertFailure {
  self.ctx.read_var_u32().reinterpret_as_int()
}

///|
fn AdtReader::is_made_optional_on_wire(
  self : AdtReader,
  field_position : FieldPosition,
) -> Bool {
  match self.made_optional_at {
    Some(map) => map.contains(field_position.to_byte())
    None => false
  }
}

///|
fn AdtReader::push_chunk_region(self : AdtReader, chunk : Int32) -> Bool {
  match self.inputs {
    Some(inputs) => {
      let region = inputs[chunk]
      self.ctx.push_region(region.start, region.size)
      true
    }
    None => false
  }
}

///|
fn AdtReader::pop_chunk_region(self : AdtReader, chunk : Int32) -> Unit {
  match self.inputs {
    Some(inputs) => {
      let (start, size) = self.ctx.pop_region()
      inputs[chunk] = { start, size, }
    }
    None => ()
  }
}

///|
fn AdtReader::record_field_index(
  self : AdtReader,
  chunk : Int32,
) -> FieldPosition {
  match self.last_index_per_chunk {
    Some(indices) => {
      let new_index = indices[chunk] + 1
      indices[chunk] = new_index
      FieldPosition::new(chunk, new_index)
    }
    None => FieldPosition::new(0, 0)
  }
}