///|
/// Errors raised when a simulation checkpoint cannot be trusted.
pub(all) suberror CheckpointError {
  InvalidHeader
  InvalidRecord(String)
  InvalidSize
  InvalidPayload
}

///|
/// Encode a floating-point array as a comma-separated record.
fn doubles_to_record(values : Array[Double]) -> String {
  let builder = StringBuilder(size_hint=values.length() * 12)
  for i, value in values {
    if i > 0 {
      builder.write_char(',')
    }
    builder.write_string("\{value}")
  }
  builder.to_string()
}

///|
/// Encode a solid mask as a compact binary record.
fn solids_to_record(values : Array[Bool]) -> String {
  let builder = StringBuilder(size_hint=values.length())
  for value in values {
    builder.write_char(if value { '1' } else { '0' })
  }
  builder.to_string()
}

///|
/// Parse an integer token and convert parser failures to checkpoint errors.
fn checkpoint_int(token : StringView) -> Int raise CheckpointError {
  @string.parse_int(token) catch {
    _ => raise InvalidRecord(token.to_owned())
  }
}

///|
/// Parse a floating-point token and convert parser failures to checkpoint errors.
fn checkpoint_double(token : StringView) -> Double raise CheckpointError {
  @string.parse_double(token) catch {
    _ => raise InvalidRecord(token.to_owned())
  }
}

///|
/// Require a record with a specific keyword and minimum field count.
fn checkpoint_record(
  line : StringView,
  keyword~ : String,
  minimum~ : Int,
) -> Array[StringView] raise CheckpointError {
  let fields = line.split(" ").to_array()
  if fields.length() < minimum || fields[0].to_owned() != keyword {
    raise InvalidRecord(line.to_owned())
  }
  fields
}

///|
/// Serialize a simulation into a versioned, dependency-free text checkpoint.
pub fn Simulation::to_checkpoint(self : Simulation) -> String {
  let model_name = match self.options.model {
    Bgk(_) => "bgk"
    Regularized(_) => "regularized"
    Mrt(_) => "mrt"
  }
  let boundary_name = match self.options.boundary {
    BounceBack => "bounce"
    Periodic => "periodic"
    Open => "open"
  }
  let builder = StringBuilder(
    size_hint=self.f.length() * 10 + self.solid.length() + 256,
  )
  builder.write_string("MLBM/1\n")
  builder.write_string("size \{self.size.width} \{self.size.height}\n")
  builder.write_string("steps \{self.step_count}\n")
  builder.write_string("model \{model_name} \{self.options.model.omega()}\n")
  builder.write_string("boundary \{boundary_name}\n")
  builder.write_string(
    "force \{self.options.force_x} \{self.options.force_y}\n",
  )
  builder.write_string("solid \{solids_to_record(self.solid)}\n")
  builder.write_string("dist \{doubles_to_record(self.f)}\n")
  builder.to_string()
}

///|
/// Restore a simulation from `Simulation::to_checkpoint` output.
pub fn Simulation::from_checkpoint(
  text : String,
) -> Simulation raise CheckpointError {
  let lines = text.split("\n").to_array()
  if lines.length() == 0 || lines[0].to_owned() != "MLBM/1" {
    raise InvalidHeader
  }
  if lines.length() < 8 {
    raise InvalidRecord("incomplete checkpoint")
  }
  let size_fields = checkpoint_record(lines[1], keyword="size", minimum=3)
  let width = checkpoint_int(size_fields[1])
  let height = checkpoint_int(size_fields[2])
  if width <= 0 || height <= 0 {
    raise InvalidSize
  }
  let steps_fields = checkpoint_record(lines[2], keyword="steps", minimum=2)
  let steps = checkpoint_int(steps_fields[1])
  let model_fields = checkpoint_record(lines[3], keyword="model", minimum=3)
  let omega = checkpoint_double(model_fields[2])
  let model = match model_fields[1].to_owned() {
    "bgk" => Bgk(omega)
    "regularized" => Regularized(omega)
    "mrt" => Mrt(omega)
    _ => raise InvalidRecord(lines[3].to_owned())
  }
  let boundary_fields = checkpoint_record(
    lines[4],
    keyword="boundary",
    minimum=2,
  )
  let boundary = match boundary_fields[1].to_owned() {
    "bounce" => BounceBack
    "periodic" => Periodic
    "open" => Open
    _ => raise InvalidRecord(lines[4].to_owned())
  }
  let force_fields = checkpoint_record(lines[5], keyword="force", minimum=3)
  let force_x = checkpoint_double(force_fields[1])
  let force_y = checkpoint_double(force_fields[2])
  let solid_fields = checkpoint_record(lines[6], keyword="solid", minimum=2)
  let dist_fields = checkpoint_record(lines[7], keyword="dist", minimum=2)
  let cells = width * height
  let solid_record = solid_fields[1].to_owned()
  if solid_record.length() != cells {
    raise InvalidPayload
  }
  let distributions = dist_fields[1].split(",").to_array()
  if distributions.length() != cells * q {
    raise InvalidPayload
  }
  let simulation = Simulation::new(
    size=Size::new(width~, height~),
    options=SimulationOptions::new(model~, boundary~, force_x~, force_y~),
  )
  simulation.step_count = steps
  for i, character in solid_record {
    simulation.solid[i] = character == '1'
  }
  for i, token in distributions {
    simulation.f[i] = checkpoint_double(token)
  }
  simulation
}

///|
/// Validate a checkpoint without retaining the restored simulation.
pub fn validate_checkpoint(text : String) -> Result[Unit, CheckpointError] {
  try {
    ignore(Simulation::from_checkpoint(text))
    Ok(())
  } catch {
    error => Err(error)
  }
}