///|
/// A JSON value together with the path used in error messages.
struct Value {
  json : Json
  path : Path
}

///|
/// An object together with the path used in error messages.
struct Object {
  fields : Map[String, Json]
  path : Path
}

///|
/// Wrap JSON as a `Value` so it can be validated while a typed value is built.
pub fn from_json(json : Json) -> Value {
  { json, path: Root, }
}

///|
/// Return the underlying JSON. Use this as an escape hatch for types that
/// maru does not convert directly.
pub fn Value::raw(self : Value) -> Json {
  self.json
}

///|
/// Require this value to be JSON `null`.
pub fn Value::null(self : Value) -> Unit raise Invalid {
  match self.json {
    Null => ()
    other => err(self.path, "expected null, got \{json_kind(other)}")
  }
}

///|
/// Convert this value to `Bool`.
pub fn Value::bool(self : Value) -> Checked[Bool] raise Invalid {
  match self.json {
    True => { val: true, path: self.path, }
    False => { val: false, path: self.path, }
    other => err(self.path, "expected boolean, got \{json_kind(other)}")
  }
}

///|
/// Convert this value to `Int`.
///
/// JSON numbers must be finite integers in the 32-bit range.
pub fn Value::int(self : Value) -> Checked[Int] raise Invalid {
  let n = expect_integer(self)
  if n > 2147483647.0 || n < -2147483648.0 {
    err(self.path, "integer overflow")
  }
  { val: n.to_int(), path: self.path, }
}

///|
/// Convert this value to `Int64`.
///
/// JSON numbers must be finite integers in the 64-bit range.
pub fn Value::int64(self : Value) -> Checked[Int64] raise Invalid {
  let n = expect_integer(self)
  let i = n.to_int64()
  if i.to_double() != n {
    err(self.path, "integer overflow")
  }
  { val: i, path: self.path, }
}

///|
/// Convert this value to `Double`.
pub fn Value::double(self : Value) -> Checked[Double] raise Invalid {
  match self.json {
    Number(n, ..) => { val: n, path: self.path, }
    other => err(self.path, "expected number, got \{json_kind(other)}")
  }
}

///|
/// Convert this value to `String`.
pub fn Value::string(self : Value) -> Checked[String] raise Invalid {
  match self.json {
    String(s) => { val: s, path: self.path, }
    other => err(self.path, "expected string, got \{json_kind(other)}")
  }
}

///|
/// Convert this value to an array of `Value`s.
pub fn Value::array(self : Value) -> Checked[Array[Value]] raise Invalid {
  match self.json {
    Array(items) => {
      let values = items.mapi(fn(i, item) {
        { json: item, path: Index(self.path, i), }
      })
      { val: values, path: self.path, }
    }
    other => err(self.path, "expected array, got \{json_kind(other)}")
  }
}

///|
/// Convert this value to an object.
pub fn Value::object(self : Value) -> Object raise Invalid {
  match self.json {
    Object(fields) => { fields, path: self.path, }
    other => err(self.path, "expected object, got \{json_kind(other)}")
  }
}

///|
/// Get a required field. Missing keys raise; JSON `null` is still a `Value`.
pub fn Object::field(self : Object, name : String) -> Value raise Invalid {
  match self.fields.get(name) {
    Some(json) => { json, path: Field(self.path, name), }
    None => err(Field(self.path, name), "missing field")
  }
}

///|
/// Get an optional field.
///
/// Returns `None` only when the key is absent. JSON `null` is `Some(Value)`.
pub fn Object::optional_field(self : Object, name : String) -> Value? {
  match self.fields.get(name) {
    Some(json) => Some({ json, path: Field(self.path, name), })
    None => None
  }
}

///|
/// Try each parser in order and return the first success.
///
/// Raises when every parser fails.
pub fn[T] first_of(
  value : Value,
  parsers : Array[(Value) -> T raise Invalid],
) -> T raise Invalid {
  for parser in parsers {
    try {
      return parser(value)
    } catch {
      Invalid(_) => ()
    }
  }
  err(value.path, "no matching parser")
}

///|
fn json_kind(json : Json) -> String {
  match json {
    Null => "null"
    True | False => "boolean"
    Number(_, ..) => "number"
    String(_) => "string"
    Array(_) => "array"
    Object(_) => "object"
  }
}

///|
fn expect_integer(value : Value) -> Double raise Invalid {
  match value.json {
    Number(n, ..) => {
      if n.is_nan() || n.is_inf() || n != n.trunc() {
        err(value.path, "expected integer")
      }
      n
    }
    other => err(value.path, "expected integer, got \{json_kind(other)}")
  }
}