///|
/// Self-describing bounded field snapshot, not a general PDE configuration.
pub fn Field::to_json(self : Field) -> Json {
  {
    "nx": self.grid.nx,
    "ny": self.grid.ny,
    "lx": self.grid.lx,
    "ly": self.grid.ly,
    "values": self.values,
  }
}

///|
fn json_number(
  object : Map[String, Json],
  key : String,
) -> Double raise SolverError {
  match object.get(key) {
    Some(Number(v, ..)) => {
      require_finite(v, key)
      v
    }
    _ => raise Invalid("missing or nonnumeric field metadata")
  }
}

///|
/// Strict keys, finite values, bounded text/depth, exact integer intervals.
pub fn field_from_json(text : String) -> Field raise SolverError {
  if text.length() > 32000000 {
    raise Budget("field JSON exceeds 32 million UTF-16 units")
  }
  // Guard nesting before invoking the core parser (whose depth label is deprecated).
  let mut depth = 0
  let mut quoted = false
  let mut escaped = false
  for ch in text.iter() {
    if quoted {
      if escaped {
        escaped = false
      } else if ch == '\\' {
        escaped = true
      } else if ch == '"' {
        quoted = false
      }
    } else if ch == '"' {
      quoted = true
    } else if ch == '{' || ch == '[' {
      depth = depth + 1
      if depth > 4 {
        raise Budget("field JSON nesting exceeds four")
      }
    } else if ch == '}' || ch == ']' {
      depth = depth - 1
    }
  }
  let value = @json.parse(text) catch {
    _ => raise Invalid("invalid field JSON")
  }
  let object = match value {
    Object(o) => o
    _ => raise Invalid("field JSON must be an object")
  }
  if object.length() != 5 {
    raise Invalid("field JSON requires exactly nx,ny,lx,ly,values")
  }
  for key in object.keys() {
    if key != "nx" &&
      key != "ny" &&
      key != "lx" &&
      key != "ly" &&
      key != "values" {
      raise Invalid("unknown field JSON key")
    }
  }
  let nx = json_number(object, "nx")
  let ny = json_number(object, "ny")
  if nx < 2.0 ||
    nx > 1024.0 ||
    ny < 2.0 ||
    ny > 1024.0 ||
    nx != nx.to_int().to_double() ||
    ny != ny.to_int().to_double() {
    raise Invalid("invalid JSON grid intervals")
  }
  let grid = Grid::new(
    nx.to_int(),
    ny.to_int(),
    lx=json_number(object, "lx"),
    ly=json_number(object, "ly"),
  )
  let array = match object.get("values") {
    Some(Array(a)) => a
    _ => raise Invalid("values must be an array")
  }
  if array.length() != grid.size() {
    raise Shape("JSON values length mismatch")
  }
  let values = []
  for entry in array {
    match entry {
      Number(v, ..) => {
        require_finite(v, "JSON sample")
        values.push(v)
      }
      _ => raise Invalid("nonnumeric JSON sample")
    }
  }
  Field::new(grid, values)
}

///|
/// Long-form CSV with x-fast ordering for plotting and spreadsheet ingestion.
pub fn Field::to_csv(self : Field) -> String {
  let out = StringBuilder()
  out.write_string("i,j,x,y,value\n")
  let g = self.grid
  let (hx, hy) = g.spacing()
  for j in 0..<=g.ny {
    for i in 0..<=g.nx {
      out.write_string(
        "\{i},\{j},\{i.to_double()*hx},\{j.to_double()*hy},\{self.values[g.at(i,j)]}\n",
      )
    }
  }
  out.to_string()
}