///|
fn object_size(value : Json, size : Int32) -> Unit raise GasError {
  match value {
    Object(o) =>
      if o.length() != size {
        raise Invalid("unexpected JSON field count")
      }
    _ => raise Invalid("expected JSON object")
  }
}

///|
fn required_json(value : Json, key : String) -> Json raise GasError {
  match json_field(value, key) {
    Some(v) => v
    None => raise Invalid("missing JSON field: " + key)
  }
}

///|
fn json_text(value : Json, key : String) -> String raise GasError {
  match required_json(value, key) {
    String(s) => s
    _ => raise Invalid("expected JSON string: " + key)
  }
}

///|
fn decode_primitive(value : Json) -> Primitive raise GasError {
  object_size(value, 3)
  Primitive::new(
    json_number(value, "rho"),
    json_number(value, "velocity"),
    json_number(value, "pressure"),
  )
}

///|
fn boundary_name(bc : Boundaries) -> String {
  match bc {
    Periodic => "periodic"
    Ends(Outflow, Outflow) => "outflow"
    Ends(Wall, Wall) => "wall"
    Ends(Wall, Outflow) => "wall-outflow"
    Ends(Outflow, Wall) => "outflow-wall"
  }
}

///|
fn decode_solver(value : Json) -> SolverConfig raise GasError {
  object_size(value, 7)
  let boundaries = match json_text(value, "boundaries") {
    "periodic" => Periodic
    "outflow" => Ends(Outflow, Outflow)
    "wall" => Ends(Wall, Wall)
    "wall-outflow" => Ends(Wall, Outflow)
    "outflow-wall" => Ends(Outflow, Wall)
    _ => raise Invalid("unknown boundary policy")
  }
  let reconstruction = match json_text(value, "reconstruction") {
    "constant" => Constant
    "minmod" => Minmod
    "mc" => MonotonizedCentral
    _ => raise Invalid("unknown reconstruction")
  }
  let flux = match json_text(value, "flux") {
    "hll" => Hll
    "rusanov" => Rusanov
    _ => raise Invalid("unknown numerical flux")
  }
  let integrator = match json_text(value, "integrator") {
    "euler" => ForwardEuler
    "ssprk2" => SspRk2
    _ => raise Invalid("unknown time integrator")
  }
  SolverConfig::new(
    boundaries,
    reconstruction,
    flux,
    integrator,
    cfl=json_number(value, "cfl"),
    max_steps=nonnegative_count(json_number(value, "max_steps")),
    max_retries=nonnegative_count(json_number(value, "max_retries")),
  )
}

///|
/// Strict versioned experiment input. Rejects unknown/missing/duplicate fields,
/// fractional counts, unrecognized policies and configurations outside budgets.
/// The document represents inputs only; it never claims a run was successful.
pub fn parse_experiment(text : String) -> Experiment raise GasError {
  if text.length() > 1000000 {
    raise Invalid("experiment JSON exceeds one million code units")
  }
  let value = @json.parse(text, max_nesting_depth=8, reject_duplicate_keys=true) catch {
    _ => raise Invalid("invalid experiment JSON")
  }
  object_size(value, 9)
  if json_number(value, "schema") != 1.0 {
    raise Invalid("unsupported experiment schema")
  }
  let gas = Gas::new(json_number(value, "gamma"))
  let grid = required_json(value, "mesh")
  object_size(grid, 3)
  let mesh = Mesh::new(
    json_number(grid, "left"),
    json_number(grid, "right"),
    nonnegative_count(json_number(grid, "cells")),
  )
  Experiment::new(
    json_text(value, "name"),
    gas,
    mesh,
    decode_primitive(required_json(value, "left_state")),
    decode_primitive(required_json(value, "right_state")),
    json_number(value, "interface_at"),
    json_number(value, "end_time"),
    decode_solver(required_json(value, "solver")),
  )
}

///|
/// Portable JSON describing precisely the validated inputs used by this experiment.
pub fn Experiment::json_config(self : Experiment) -> String {
  let config = self.config
  let reconstruction = match config.reconstruction {
    Constant => "constant"
    Minmod => "minmod"
    MonotonizedCentral => "mc"
  }
  let flux = match config.flux {
    Hll => "hll"
    Rusanov => "rusanov"
  }
  let integrator = match config.integrator {
    ForwardEuler => "euler"
    SspRk2 => "ssprk2"
  }
  Json::object({
    "schema": Json::number(1.0),
    "name": Json::string(self.name),
    "gamma": Json::number(self.gas.gamma),
    "mesh": Json::object({
      "left": Json::number(self.mesh.left),
      "right": Json::number(self.mesh.right),
      "cells": Json::number(self.mesh.cells.to_double()),
    }),
    "left_state": self.left.to_json(),
    "right_state": self.right.to_json(),
    "interface_at": Json::number(self.interface_at),
    "end_time": Json::number(self.end_time),
    "solver": Json::object({
      "boundaries": Json::string(boundary_name(config.boundaries)),
      "reconstruction": Json::string(reconstruction),
      "flux": Json::string(flux),
      "integrator": Json::string(integrator),
      "cfl": Json::number(config.cfl),
      "max_steps": Json::number(config.max_steps.to_double()),
      "max_retries": Json::number(config.max_retries.to_double()),
    }),
  }).stringify()
}