// Decoding a `serde_json::Value` into the schema model, reproducing
// `serde_json::from_str::`:
//
// - every struct is `#[serde(default)]`: absent keys take default values;
// - `Option` fields read `null` as `None`, except `const` and `default`
//   (`allow_null`), which keep `Some(Null)`;
// - validation groups and metadata are `#[serde(flatten)]`: each consumes the
//   keys it recognises and all remaining keys become `extensions`;
// - a group equal to its default is `None` (`skip_if_default`);
// - `Schema` and `SingleOrVec` are untagged enums.

///|
/// A schema decoding error with serde's message and a JSON pointer to the
/// offending value.
pub(all) suberror SchemaError {
  SchemaError(message~ : String, path~ : String)
} derive(Debug, Eq)

///|
pub impl Show for SchemaError with fn output(self, logger) {
  let SchemaError(message~, path~) = self
  if path == "" {
    logger.write_string(message)
  } else {
    logger.write_string("\{message} at \{path}")
  }
}

///|
fn json_pointer_escape(key : String) -> String {
  key.replace_all(old="~", new="~0").replace_all(old="/", new="~1")
}

///|
/// serde's `Unexpected` rendering of a value.
fn unexpected(v : @serde_json.Value) -> String {
  match v {
    Null => "null"
    Bool(b) => "boolean `\{b}`"
    Number(PosInt(n)) => "integer `\{n}`"
    Number(NegInt(n)) => "integer `\{n}`"
    Number(Float(_) as n) => "floating point `\{n}`"
    String(s) => {
      let buf = StringBuilder()
      @serde_json.write_escaped_str(buf, s)
      "string \{buf}"
    }
    Array(_) => "sequence"
    Object(_) => "map"
  }
}

///|
fn invalid_type(
  v : @serde_json.Value,
  expected : String,
  path : String,
) -> SchemaError {
  SchemaError(
    message="invalid type: \{unexpected(v)}, expected \{expected}",
    path~,
  )
}

///|
fn child(path : String, key : String) -> String {
  "\{path}/\{json_pointer_escape(key)}"
}

///|
fn decode_string(
  v : @serde_json.Value,
  path : String,
) -> String raise SchemaError {
  match v {
    String(s) => s
    _ => raise invalid_type(v, "a string", path)
  }
}

///|
fn decode_bool(v : @serde_json.Value, path : String) -> Bool raise SchemaError {
  match v {
    Bool(b) => b
    _ => raise invalid_type(v, "a boolean", path)
  }
}

///|
fn decode_f64(v : @serde_json.Value, path : String) -> Double raise SchemaError {
  match v {
    Number(n) => n.as_f64()
    _ => raise invalid_type(v, "f64", path)
  }
}

///|
fn decode_u32(v : @serde_json.Value, path : String) -> UInt raise SchemaError {
  match v {
    Number(PosInt(n)) =>
      if n <= 0xFFFF_FFFFUL {
        n.to_uint()
      } else {
        raise SchemaError(
          message="invalid value: integer `\{n}`, expected u32",
          path~,
        )
      }
    Number(NegInt(n)) =>
      raise SchemaError(
        message="invalid value: integer `\{n}`, expected u32",
        path~,
      )
    _ => raise invalid_type(v, "u32", path)
  }
}

///|
fn decode_array(
  v : @serde_json.Value,
  path : String,
) -> Array[@serde_json.Value] raise SchemaError {
  match v {
    Array(xs) => xs
    _ => raise invalid_type(v, "a sequence", path)
  }
}

///|
fn decode_object(
  v : @serde_json.Value,
  path : String,
) -> @collections.StrMap[@serde_json.Value] raise SchemaError {
  match v {
    Object(o) => o
    _ => raise invalid_type(v, "a map", path)
  }
}

///|
/// Decode `Option`: `null` is `None`.
fn[T] opt(
  v : @serde_json.Value?,
  path : String,
  f : (@serde_json.Value, String) -> T raise SchemaError,
) -> T? raise SchemaError {
  match v {
    None | Some(Null) => None
    Some(x) => Some(f(x, path))
  }
}

///|
/// Decode an untagged `Schema`: a boolean or a schema object.
pub fn decode_schema(
  v : @serde_json.Value,
  path? : String = "",
) -> Schema raise SchemaError {
  match v {
    Bool(b) => Bool(b)
    Object(o) => Object(decode_schema_object(o, path~))
    _ =>
      raise SchemaError(
        message="data did not match any variant of untagged enum Schema",
        path~,
      )
  }
}

///|
fn decode_schema_array(
  v : @serde_json.Value,
  path : String,
) -> Array[Schema] raise SchemaError {
  let xs = decode_array(v, path)
  let out = []
  for i, x in xs {
    out.push(decode_schema(x, path=child(path, i.to_string())))
  }
  out
}

///|
fn decode_schema_map(
  v : @serde_json.Value,
  path : String,
) -> @collections.StrMap[Schema] raise SchemaError {
  let o = decode_object(v, path)
  let out = @collections.StrMap::new()
  for k, x in o {
    out.set(k, decode_schema(x, path=child(path, k)))
  }
  out
}

///|
fn decode_instance_type(
  v : @serde_json.Value,
  path : String,
) -> InstanceType raise SchemaError {
  match v {
    String("null") => Null
    String("boolean") => Boolean
    String("object") => Object
    String("array") => Array
    String("number") => Number
    String("string") => String
    String("integer") => Integer
    _ => raise SchemaError(message="unknown instance type", path~)
  }
}

///|
fn decode_type(
  v : @serde_json.Value,
  path : String,
) -> SingleOrVec[InstanceType] raise SchemaError {
  let fail = SchemaError(
    message="data did not match any variant of untagged enum SingleOrVec",
    path~,
  )
  match v {
    String(_) => Single(decode_instance_type(v, path)) catch { _ => raise fail }
    Array(xs) => {
      let out = []
      for x in xs {
        out.push(decode_instance_type(x, path)) catch {
          _ => raise fail
        }
      }
      Vec(out)
    }
    _ => raise fail
  }
}

///|
fn decode_items(
  v : @serde_json.Value,
  path : String,
) -> SingleOrVec[Schema] raise SchemaError {
  match v {
    Array(_) => Vec(decode_schema_array(v, path))
    _ =>
      Single(decode_schema(v, path~)) catch {
        _ =>
          raise SchemaError(
            message="data did not match any variant of untagged enum SingleOrVec",
            path~,
          )
      }
  }
}

///|
/// Key-consuming view of an object's entries (serde's flatten buffer).
priv struct Entries {
  map : @collections.StrMap[@serde_json.Value]
  path : String
}

///|
fn Entries::take(self : Entries, key : String) -> @serde_json.Value? {
  self.map.remove(key)
}

///|
fn Entries::path_of(self : Entries, key : String) -> String {
  child(self.path, key)
}

///|
fn Entries::metadata(self : Entries) -> Metadata? raise SchemaError {
  let m : Metadata = {
    id: opt(self.take("$id"), self.path_of("$id"), decode_string),
    title: opt(self.take("title"), self.path_of("title"), decode_string),
    description: opt(
      self.take("description"),
      self.path_of("description"),
      decode_string,
    ),
    // allow_null
    default: self.take("default"),
    deprecated: match self.take("deprecated") {
      None => false
      Some(v) => decode_bool(v, self.path_of("deprecated"))
    },
    read_only: match self.take("readOnly") {
      None => false
      Some(v) => decode_bool(v, self.path_of("readOnly"))
    },
    write_only: match self.take("writeOnly") {
      None => false
      Some(v) => decode_bool(v, self.path_of("writeOnly"))
    },
    examples: match self.take("examples") {
      None => []
      Some(v) => decode_array(v, self.path_of("examples"))
    },
  }
  if m == Metadata::default() {
    None
  } else {
    Some(m)
  }
}

///|
fn Entries::subschemas(
  self : Entries,
) -> SubschemaValidation? raise SchemaError {
  let s : SubschemaValidation = {
    all_of: opt(self.take("allOf"), self.path_of("allOf"), decode_schema_array),
    any_of: opt(self.take("anyOf"), self.path_of("anyOf"), decode_schema_array),
    one_of: opt(self.take("oneOf"), self.path_of("oneOf"), decode_schema_array),
    not: opt(self.take("not"), self.path_of("not"), (v, p) => {
      decode_schema(v, path=p)
    }),
    if_schema: opt(self.take("if"), self.path_of("if"), (v, p) => {
      decode_schema(v, path=p)
    }),
    then_schema: opt(self.take("then"), self.path_of("then"), (v, p) => {
      decode_schema(v, path=p)
    }),
    else_schema: opt(self.take("else"), self.path_of("else"), (v, p) => {
      decode_schema(v, path=p)
    }),
  }
  if s == SubschemaValidation::default() {
    None
  } else {
    Some(s)
  }
}

///|
fn Entries::number(self : Entries) -> NumberValidation? raise SchemaError {
  let n : NumberValidation = {
    multiple_of: opt(
      self.take("multipleOf"),
      self.path_of("multipleOf"),
      decode_f64,
    ),
    maximum: opt(self.take("maximum"), self.path_of("maximum"), decode_f64),
    exclusive_maximum: opt(
      self.take("exclusiveMaximum"),
      self.path_of("exclusiveMaximum"),
      decode_f64,
    ),
    minimum: opt(self.take("minimum"), self.path_of("minimum"), decode_f64),
    exclusive_minimum: opt(
      self.take("exclusiveMinimum"),
      self.path_of("exclusiveMinimum"),
      decode_f64,
    ),
  }
  if n == NumberValidation::default() {
    None
  } else {
    Some(n)
  }
}

///|
fn Entries::string(self : Entries) -> StringValidation? raise SchemaError {
  let s : StringValidation = {
    max_length: opt(
      self.take("maxLength"),
      self.path_of("maxLength"),
      decode_u32,
    ),
    min_length: opt(
      self.take("minLength"),
      self.path_of("minLength"),
      decode_u32,
    ),
    pattern: opt(self.take("pattern"), self.path_of("pattern"), decode_string),
  }
  if s == StringValidation::default() {
    None
  } else {
    Some(s)
  }
}

///|
fn Entries::array(self : Entries) -> ArrayValidation? raise SchemaError {
  let a : ArrayValidation = {
    items: opt(self.take("items"), self.path_of("items"), decode_items),
    additional_items: opt(
      self.take("additionalItems"),
      self.path_of("additionalItems"),
      (v, p) => decode_schema(v, path=p),
    ),
    max_items: opt(self.take("maxItems"), self.path_of("maxItems"), decode_u32),
    min_items: opt(self.take("minItems"), self.path_of("minItems"), decode_u32),
    unique_items: opt(
      self.take("uniqueItems"),
      self.path_of("uniqueItems"),
      decode_bool,
    ),
    contains: opt(self.take("contains"), self.path_of("contains"), (v, p) => {
      decode_schema(v, path=p)
    }),
  }
  if a == ArrayValidation::default() {
    None
  } else {
    Some(a)
  }
}

///|
fn Entries::object(self : Entries) -> ObjectValidation? raise SchemaError {
  let required = match self.take("required") {
    None => @collections.StrSet::new()
    Some(v) => {
      let p = self.path_of("required")
      let set = @collections.StrSet::new()
      for i, x in decode_array(v, p) {
        set.add(decode_string(x, child(p, i.to_string())))
      }
      set
    }
  }
  let o : ObjectValidation = {
    max_properties: opt(
      self.take("maxProperties"),
      self.path_of("maxProperties"),
      decode_u32,
    ),
    min_properties: opt(
      self.take("minProperties"),
      self.path_of("minProperties"),
      decode_u32,
    ),
    required,
    properties: match self.take("properties") {
      None => @collections.StrMap::new()
      Some(v) => decode_schema_map(v, self.path_of("properties"))
    },
    pattern_properties: match self.take("patternProperties") {
      None => @collections.StrMap::new()
      Some(v) => decode_schema_map(v, self.path_of("patternProperties"))
    },
    additional_properties: opt(
      self.take("additionalProperties"),
      self.path_of("additionalProperties"),
      (v, p) => decode_schema(v, path=p),
    ),
    property_names: opt(
      self.take("propertyNames"),
      self.path_of("propertyNames"),
      (v, p) => decode_schema(v, path=p),
    ),
  }
  if o == ObjectValidation::default() {
    None
  } else {
    Some(o)
  }
}

///|
fn decode_schema_object_entries(e : Entries) -> SchemaObject raise SchemaError {
  let instance_type = opt(e.take("type"), e.path_of("type"), decode_type)
  let format = opt(e.take("format"), e.path_of("format"), decode_string)
  let enum_values = opt(e.take("enum"), e.path_of("enum"), decode_array)
  // allow_null
  let const_value = e.take("const")
  let reference = opt(e.take("$ref"), e.path_of("$ref"), decode_string)
  let metadata = e.metadata()
  let subschemas = e.subschemas()
  let number = e.number()
  let string = e.string()
  let array = e.array()
  let object = e.object()
  {
    metadata,
    instance_type,
    format,
    enum_values,
    const_value,
    subschemas,
    number,
    string,
    array,
    object,
    reference,
    extensions: e.map,
  }
}

///|
/// Decode a schema object from the entries of a JSON object.
pub fn decode_schema_object(
  o : @collections.StrMap[@serde_json.Value],
  path? : String = "",
) -> SchemaObject raise SchemaError {
  decode_schema_object_entries({ map: o.copy(), path, })
}

///|
/// Decode a root schema document from a JSON value.
pub fn RootSchema::from_value(
  v : @serde_json.Value,
) -> RootSchema raise SchemaError {
  guard v is Object(o) else { raise invalid_type(v, "a map", "") }
  let e : Entries = { map: o.copy(), path: "", }
  let meta_schema = opt(e.take("$schema"), "/$schema", decode_string)
  let definitions = match (e.take("definitions"), e.take("$defs")) {
    (Some(_), Some(_)) =>
      raise SchemaError(message="duplicate field `definitions`", path="")
    (Some(v), None) => decode_schema_map(v, "/definitions")
    (None, Some(v)) => decode_schema_map(v, "/$defs")
    (None, None) => @collections.StrMap::new()
  }
  let schema = decode_schema_object_entries(e)
  { meta_schema, schema, definitions, }
}

///|
/// Parse and decode a root schema document from JSON text.
pub fn RootSchema::from_json_str(text : StringView) -> RootSchema raise {
  RootSchema::from_value(@serde_json.parse(text))
}