///|
pub(all) suberror ModelJsonError {
  InvalidJson
  MissingField(String)
  WrongType(String)
  InvalidGraph(ModelError)
} derive(Debug)

///|
pub extend ModelJsonError with Debug::{to_repr}

///|
fn field(value : Json, key : String) -> Json raise ModelJsonError {
  match value {
    Object(map) =>
      match map.get(key) {
        Some(item) => item
        None => raise MissingField(key)
      }
    _ => raise WrongType("object")
  }
}

///|
fn string_field(value : Json, key : String) -> String raise ModelJsonError {
  match field(value, key) {
    String(s) => s
    _ => raise WrongType(key)
  }
}

///|
fn int_field(value : Json, key : String) -> Int raise ModelJsonError {
  match field(value, key) {
    Number(n, ..) => {
      let int = n.to_int()
      if n != int.to_double() {
        raise WrongType(key)
      }
      int
    }
    _ => raise WrongType(key)
  }
}

///|
fn array_field(value : Json, key : String) -> Array[Json] raise ModelJsonError {
  match field(value, key) {
    Array(items) => items
    _ => raise WrongType(key)
  }
}

///|
fn string_array_field(
  value : Json,
  key : String,
) -> Array[String] raise ModelJsonError {
  let result : Array[String] = []
  for item in array_field(value, key) {
    match item {
      String(s) => result.push(s)
      _ => raise WrongType(key)
    }
  }
  result
}

///|
/// Parse an explicit finite graph from JSON. The input is validated before
/// any temporal formula is checked.
pub fn load_model_json(source : String) -> Model raise ModelJsonError {
  let value = @json.parse(source) catch { _ => raise InvalidJson }
  let states : Array[State] = []
  let edges : Array[Edge] = []
  for raw in array_field(value, "states") {
    states.push({
      id: string_field(raw, "id"),
      labels: string_array_field(raw, "labels"),
    })
  }
  for raw in array_field(value, "edges") {
    edges.push({
      from: int_field(raw, "from"),
      to: int_field(raw, "to"),
      action: string_field(raw, "action"),
    })
  }
  let initial = int_field(value, "initial")
  match Model::new(states, edges, initial~) {
    Ok(model) => model
    Err(error) => raise InvalidGraph(error)
  }
}