///|
/// Versioned data schema: [schema, direction, loss, quantile, [[x,y,weight],...]].
pub fn encode_model(model : Model) -> Result[String, String] {
  match validate_model(model) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  let direction = match model.direction {
    Increasing => "increasing"
    Decreasing => "decreasing"
  }
  let (loss, tau) = match model.loss {
    Squared => ("squared", 0.0)
    Absolute => ("absolute", 0.0)
    Quantile(tau) => ("quantile", tau)
  }
  let knots = model.knots.map(fn(k) {
    Json::array([Json::number(k.x), Json::number(k.y), Json::number(k.weight)])
  })
  Ok(
    Json::array([
      Json::string("moonisotonic/1"),
      Json::string(direction),
      Json::string(loss),
      Json::number(tau),
      Json::array(knots),
    ]).stringify(),
  )
}

///|
fn json_number(value : Json) -> Result[Double, String] {
  match value {
    Number(number, ..) =>
      if finite(number) {
        Ok(number)
      } else {
        Err("nonfinite JSON number")
      }
    _ => Err("expected JSON number")
  }
}

///|
/// Parse bounded, untrusted model data. No executable code or callbacks in the format.
pub fn decode_model(
  text : String,
  max_knots? : Int = 100000,
) -> Result[Model, String] {
  if max_knots <= 0 {
    return Err("nonpositive knot limit")
  }
  if text.length() > 16000000 {
    return Err("model text exceeds 16 million UTF-16 units")
  }
  let value = @json.parse(text) catch { _ => return Err("invalid model JSON") }
  let fields = match value {
    Array(fields) => fields
    _ => return Err("expected model array")
  }
  if fields.length() != 5 {
    return Err("model field count mismatch")
  }
  match fields[0] {
    String("moonisotonic/1") => ()
    _ => return Err("unsupported schema version")
  }
  let direction = match fields[1] {
    String("increasing") => Increasing
    String("decreasing") => Decreasing
    _ => return Err("invalid model direction")
  }
  let tau = match json_number(fields[3]) {
    Err(e) => return Err(e)
    Ok(v) => v
  }
  let loss = match fields[2] {
    String("squared") =>
      if tau == 0.0 {
        Squared
      } else {
        return Err("unexpected squared quantile")
      }
    String("absolute") =>
      if tau == 0.0 {
        Absolute
      } else {
        return Err("unexpected absolute quantile")
      }
    String("quantile") => Quantile(tau)
    _ => return Err("invalid loss name")
  }
  let rows = match fields[4] {
    Array(rows) => rows
    _ => return Err("expected knot array")
  }
  if rows.length() > max_knots {
    return Err("knot budget exceeded")
  }
  let knots : Array[Knot] = []
  for row in rows {
    let values = match row {
      Array(values) => values
      _ => return Err("expected knot row")
    }
    if values.length() != 3 {
      return Err("knot field count mismatch")
    }
    let x = match json_number(values[0]) {
      Err(e) => return Err(e)
      Ok(v) => v
    }
    let y = match json_number(values[1]) {
      Err(e) => return Err(e)
      Ok(v) => v
    }
    let weight = match json_number(values[2]) {
      Err(e) => return Err(e)
      Ok(v) => v
    }
    knots.push({ x, y, weight, })
  }
  let model = { knots, direction, loss, }
  match validate_model(model) {
    Err(e) => Err(e)
    Ok(_) => Ok(model)
  }
}