///|
/// Schema evolution steps, mirroring the Scala `Evolution` sealed trait and
/// the Rust `Evolution` enum.
///
/// A record's evolution history is declared as `InitialVersion` followed by
/// one step per schema change; the list length determines the wire version
/// byte (`steps.length() - 1`).
pub(all) enum Evolution {
  InitialVersion
  /// Add a new field to a record. The default value is supplied by the
  /// reading codec, not stored on the wire.
  FieldAdded(String)
  /// An existing non-option field is made optional.
  FieldMadeOptional(String)
  /// Field removed from the record.
  FieldRemoved(String)
  /// Field made transient — an alias for `FieldRemoved` on the wire.
  FieldMadeTransient(String)
}

///|
/// Wire codes of serialized evolution steps.
const EVOLUTION_STEP_UNKNOWN : Int32 = 0

///|
const EVOLUTION_STEP_FIELD_MADE_OPTIONAL : Int32 = -1

///|
const EVOLUTION_STEP_FIELD_REMOVED : Int32 = -2

///|
/// Position of a field inside the evolved record layout, as referenced by
/// `FieldMadeOptional` header entries.
///
/// Wire encoding is a single byte: for chunk 0 the negated position
/// (so the reader can distinguish it from a chunk index), otherwise the
/// chunk number (position 0 implied).
priv struct FieldPosition {
  chunk : Int32
  position : Int32
}

///|
fn FieldPosition::new(chunk : Int32, position : Int32) -> FieldPosition {
  { chunk, position, }
}

///|
fn FieldPosition::to_byte(self : FieldPosition) -> Int32 {
  if self.chunk == 0 {
    -self.position
  } else {
    self.chunk
  }
}

///|
fn FieldPosition::from_byte(byte : Int32) -> FieldPosition {
  if byte < 0 {
    FieldPosition::new(0, -byte)
  } else {
    FieldPosition::new(byte, 0)
  }
}

///|
/// Static description of a record's evolution history, shared by the writer
/// and the reader. Build once per record type (e.g. as a toplevel `let`) via
/// `AdtMetadata::new`.
pub struct AdtMetadata {
  /// Wire version: `evolution_steps.length() - 1`.
  priv version : Int32
  /// Field name -> index of the `FieldAdded` step that introduced it
  /// (0 if present from the start).
  priv field_generations : @hashmap.HashMap[String, Int32]
  /// Field name -> index of its `FieldMadeOptional` step.
  priv made_optional_at : @hashmap.HashMap[String, Int32]
  /// Names removed by `FieldRemoved`/`FieldMadeTransient` steps.
  priv removed_fields : @hashmap.HashMap[String, Bool]
  /// The declared steps, including the leading `InitialVersion`.
  priv evolution_steps : Array[Evolution]
  /// True when any step is `FieldMadeOptional` (enables field-position
  /// tracking, which is only needed to disambiguate those entries).
  priv contains_field_made_optional : Bool
}

///|
/// Build the metadata for a record declared with `steps`
/// (`InitialVersion` first, then one entry per schema change).
///
/// Panics with more than 255 steps (the version byte is a single `u8`).
pub fn AdtMetadata::new(steps : Array[Evolution]) -> AdtMetadata {
  if steps.length() == 0 {
    abort("desert: evolution steps must start with InitialVersion")
  }
  if steps.length() > 255 {
    abort("desert: too many evolution steps (max 255)")
  }
  let field_generations : @hashmap.HashMap[String, Int32] = @hashmap.HashMap([])
  let made_optional_at : @hashmap.HashMap[String, Int32] = @hashmap.HashMap([])
  let removed_fields : @hashmap.HashMap[String, Bool] = @hashmap.HashMap([])
  for i, step in steps {
    match step {
      FieldAdded(name) => field_generations.set(name, i)
      FieldMadeOptional(name) => made_optional_at.set(name, i)
      FieldRemoved(name) | FieldMadeTransient(name) =>
        removed_fields.set(name, true)
      InitialVersion => ()
    }
  }
  {
    version: steps.length() - 1,
    field_generations,
    made_optional_at,
    removed_fields,
    evolution_steps: steps,
    contains_field_made_optional: !made_optional_at.is_empty(),
  }
}

///|
fn AdtMetadata::generation_of(self : AdtMetadata, field_name : String) -> Int32 {
  match self.field_generations.get(field_name) {
    Some(generation) => generation
    None => 0
  }
}

///|
/// A serialized evolution header entry (wire form).
priv enum SerializedEvolutionStep {
  /// Positive (or zero) size: a data chunk with this many bytes follows.
  FieldAddedToNewChunk(Int32)
  /// -1 + position byte.
  FieldMadeOptionalStep(FieldPosition)
  /// -2 + deduplicated field name.
  FieldRemovedStep(String)
  /// 0 — written by unknown future steps; ignored by older readers.
  UnknownStep
}

///|
fn SerializedEvolutionStep::serialize(
  self : SerializedEvolutionStep,
  ctx : SerializationContext,
) -> Unit raise DesertFailure {
  match self {
    FieldAddedToNewChunk(size) => ctx.write_var_i32(size)
    FieldMadeOptionalStep(position) => {
      ctx.write_var_i32(EVOLUTION_STEP_FIELD_MADE_OPTIONAL)
      ctx.write_u8(position.to_byte().to_byte())
    }
    FieldRemovedStep(field_name) => {
      ctx.write_var_i32(EVOLUTION_STEP_FIELD_REMOVED)
      DeduplicatedString::new(field_name).serialize(ctx)
    }
    UnknownStep => ctx.write_var_i32(EVOLUTION_STEP_UNKNOWN)
  }
}

///|
fn SerializedEvolutionStep::deserialize(
  ctx : DeserializationContext,
) -> SerializedEvolutionStep raise DesertFailure {
  let code_or_size = ctx.read_var_i32()
  if code_or_size == EVOLUTION_STEP_UNKNOWN {
    return UnknownStep
  }
  if code_or_size == EVOLUTION_STEP_FIELD_MADE_OPTIONAL {
    return FieldMadeOptionalStep(FieldPosition::from_byte(ctx.read_i8()))
  }
  if code_or_size == EVOLUTION_STEP_FIELD_REMOVED {
    return FieldRemovedStep(DeduplicatedString::deserialize(ctx).get())
  }
  if code_or_size < 0 {
    // Negative codes other than -1/-2 are unknown encodings.
    raise UnknownSerializedEvolutionStep(code_or_size)
  }
  FieldAddedToNewChunk(code_or_size)
}