///|
fn schema_tick(work : Ref[Int], depth : Int) -> Unit raise SchemaError {
  work.val += 1
  if work.val > 100000 || depth > 64 {
    raise InvalidSchema("schema value node/depth limit")
  }
}

///|
fn schema_integer(n : Int64, t : IdlType) -> Value raise SchemaError {
  match t {
    Base("bool") => Bool(n != 0L)
    Base("byte") =>
      if n >= -128L && n <= 127L {
        Byte(n.to_int())
      } else {
        raise InvalidSchema("byte out of range")
      }
    Base("i16") =>
      if n >= -32768L && n <= 32767L {
        I16(n.to_int())
      } else {
        raise InvalidSchema("i16 out of range")
      }
    Base("i32") | Named(_) =>
      if n >= -2147483648L && n <= 2147483647L {
        I32(n.to_int())
      } else {
        raise InvalidSchema("i32/enum out of range")
      }
    Base("i64") => I64(n)
    Base("double") => Double(n.to_double())
    _ => raise InvalidSchema("integer constant has incompatible type")
  }
}

///|
fn schema_hex(data : Bytes) -> String {
  let digits = "0123456789abcdef"
  let out = StringBuilder()
  for byte in data {
    let n = byte.to_int()
    out.write_string(digits[n >> 4:(n >> 4) + 1].to_owned())
    out.write_string(digits[n & 15:(n & 15) + 1].to_owned())
  }
  out.to_string()
}

///|
fn schema_unhex(text : String) -> Bytes raise SchemaError {
  if text.length() > 2097152 {
    raise InvalidSchema("hex byte limit")
  }
  let chars = text.to_array()
  if chars.length() % 2 != 0 {
    raise InvalidSchema("odd hex length")
  }
  let bytes = []
  let mut value = 0
  for i, c in chars {
    if !c.is_ascii_hexdigit() {
      raise InvalidSchema("invalid hex")
    }
    let n = if c <= '9' {
      c.to_int() - 48
    } else {
      c.to_ascii_lowercase().to_int() - 87
    }
    if i % 2 == 0 {
      value = n << 4
    } else {
      bytes.push((value + n).to_byte())
    }
  }
  Bytes::from_array(bytes)
}

///|
fn schema_uuid(text : String) -> Bytes raise SchemaError {
  if text.length() != 36 ||
    text[8:9] != "-" ||
    text[13:14] != "-" ||
    text[18:19] != "-" ||
    text[23:24] != "-" {
    raise InvalidSchema("UUID requires canonical hyphenated spelling")
  }
  let data = schema_unhex(text.replace_all(old="-", new=""))
  if data.length() != 16 {
    raise InvalidSchema("UUID requires 16 bytes")
  }
  data
}

///|
fn Schema::enum_number(
  self : Schema,
  owner : String,
  t : IdlType,
  name : String,
) -> Int? {
  let mut target : IdlDefinition? = None
  let mut label = name
  let parts = name.split(".").map(s => s.to_owned()).collect()
  if parts.length() > 1 {
    label = parts[parts.length() - 1]
    let prefix = parts[:parts.length() - 1].to_owned().join(".")
    let resolved = self.find(owner, prefix) catch { _ => return None }
    target = Some(resolved.1)
  } else if t is Named(target_name) {
    target = self.definitions.get(target_name)
  }
  if target is Some(Enumeration(_, values, _)) {
    for pair in values {
      if pair.0 == label {
        return Some(pair.1)
      }
    }
  }
  None
}

///|
fn Schema::const_value(
  self : Schema,
  owner : String,
  t : IdlType,
  value : IdlConst,
  trail : Array[String],
  depth : Int,
  budget? : Ref[Int] = Ref(0),
) -> Value raise SchemaError {
  let json = self.const_json(owner, t, value, trail, depth, budget~)
  self.from_json_type(owner, t, json, budget, depth)
}

///|
fn json_integer(input : Json) -> Int64 raise SchemaError {
  match input {
    String(s) => {
      let cs = s.to_array()
      if cs.is_empty() {
        raise InvalidSchema("empty integer")
      }
      for i, c in cs {
        if !idl_digit(c) && !(i == 0 && (c == '-' || c == '+')) {
          raise InvalidSchema("invalid integer spelling")
        }
      }
      @strconv.parse_int64(s) catch {
        _ => raise InvalidSchema("integer outside signed 64-bit range")
      }
    }
    Number(n, ..) =>
      if !n.is_nan() &&
        !n.is_inf() &&
        n == n.trunc() &&
        n.abs() <= 9007199254740991.0 {
        n.to_int64()
      } else {
        raise InvalidSchema(
          "JSON integer must be safe and integral; use a decimal string for i64",
        )
      }
    _ => raise InvalidSchema("expected integer")
  }
}

///|
fn Schema::fields_from_json(
  self : Schema,
  owner : String,
  fields : Array[IdlField],
  input : Json,
  union : Bool,
  work : Ref[Int],
  depth : Int,
) -> Value raise SchemaError {
  let object = match input {
    Object(fields) => fields
    _ => raise InvalidSchema("expected JSON object for struct")
  }
  for key, _ in object {
    if !fields.any(f => f.name == key) {
      raise InvalidSchema("unknown field " + key)
    }
  }
  let result = []
  for field in fields {
    let t = self.resolve(owner, field.field_type, 0)
    match object.get(field.name) {
      Some(Null) =>
        if field.requiredness == "required" {
          raise InvalidSchema("required field " + field.name + " is null")
        }
      Some(v) =>
        result.push(
          (field.id, self.from_json_type(owner, t, v, work, depth + 1)),
        )
      None =>
        match field.default_value {
          Some(value) =>
            result.push(
              (
                field.id,
                self.const_value(owner, t, value, [], depth + 1, budget=work),
              ),
            )
          None =>
            if field.requiredness == "required" {
              raise InvalidSchema("missing required field " + field.name)
            }
        }
    }
  }
  if union && result.length() > 1 {
    raise InvalidSchema("union permits at most one field")
  }
  Struct(result)
}

///|
fn Schema::from_json_type(
  self : Schema,
  owner : String,
  t : IdlType,
  input : Json,
  work : Ref[Int],
  depth : Int,
) -> Value raise SchemaError {
  schema_tick(work, depth)
  match (t, input) {
    (Base("bool"), True) => Bool(true)
    (Base("bool"), False) => Bool(false)
    (Base("byte" | "i16" | "i32" | "i64"), _) =>
      schema_integer(json_integer(input), t)
    (Base("double"), Number(n, ..)) => Double(n)
    (Base("double"), String(s)) => {
      let n = if s == "NaN" {
        0.0 / 0.0
      } else if s == "Infinity" {
        1.0 / 0.0
      } else if s == "-Infinity" {
        -1.0 / 0.0
      } else {
        @strconv.parse_double(s) catch {
          _ => raise InvalidSchema("invalid double")
        }
      }
      Double(n)
    }
    (Base("string"), String(s)) => Binary(@utf8.encode(s))
    (Base("binary"), { "$binary": String(s), .. }) => Binary(schema_unhex(s))
    (Base("uuid"), String(s)) => Uuid(schema_uuid(s))
    (ListOf(elem) | SetOf(elem), Array(values)) => {
      let items = values.map(v => {
        self.from_json_type(owner, elem, v, work, depth + 1)
      })
      let kind = self.wire_kind(elem)
      if t is ListOf(_) {
        List(kind, items)
      } else {
        SetValue(kind, items)
      }
    }
    (MapOf(key, tvalue), Array(values)) => {
      let items = []
      for pair in values {
        if pair is Array([k, v]) {
          items.push(
            (
              self.from_json_type(owner, key, k, work, depth + 1),
              self.from_json_type(owner, tvalue, v, work, depth + 1),
            ),
          )
        } else {
          raise InvalidSchema("map uses an array of [key,value] entries")
        }
      }
      MapValue(Some(self.wire_kind(key)), Some(self.wire_kind(tvalue)), items)
    }
    (Named(name), _) =>
      match self.definitions.get(name) {
        Some(Enumeration(_, _, _)) => {
          if input is String(label) {
            if self.enum_number(definition_owner(name), t, label) is Some(n) {
              return I32(n)
            }
          }
          schema_integer(json_integer(input), t)
        }
        Some(Record(_, flavor, fields, _)) =>
          self.fields_from_json(
            definition_owner(name),
            fields,
            input,
            flavor == "union",
            work,
            depth,
          )
        _ => raise InvalidSchema("unresolved JSON value type")
      }
    _ => raise InvalidSchema("JSON value does not match schema type")
  }
}

///|
/// Convert named-field JSON to a wire tree, applying explicit IDL defaults.
/// i64 uses decimal strings, binary uses {"$binary":"hex"}, and maps use pair arrays.
pub fn Schema::from_json(
  self : Schema,
  name : String,
  input : Json,
) -> Value raise SchemaError {
  let t = self.resolve(self.root, Named(name), 0)
  self.from_json_type(self.root, t, input, Ref(0), 0)
}

///|
fn Schema::read_fields(
  self : Schema,
  owner : String,
  fields : Array[IdlField],
  wire : Array[(Int, Value)],
  union : Bool,
  work : Ref[Int],
  depth : Int,
) -> Value raise SchemaError {
  let known = Map([])
  for pair in wire {
    for field in fields {
      if field.id == pair.0 {
        let t = self.resolve(owner, field.field_type, 0)
        if self.read_type(owner, t, pair.1, work, depth + 1) is Some(value) {
          known[field.id] = value
        }
        break
      }
    }
  }
  if union && known.length() > 1 {
    raise InvalidSchema("wire union has multiple fields")
  }
  let result = []
  for field in fields {
    if known.get(field.id) is Some(value) {
      result.push((field.id, value))
    } else if field.requiredness == "required" {
      raise InvalidSchema("missing required wire field " + field.name)
    } else if field.default_value is Some(value) {
      result.push(
        (
          field.id,
          self.const_value(
            owner,
            self.resolve(owner, field.field_type, 0),
            value,
            [],
            depth + 1,
            budget=work,
          ),
        ),
      )
    }
  }
  if union && result.length() > 1 {
    raise InvalidSchema("union defaults select multiple fields")
  }
  Struct(result)
}

///|
fn Schema::read_type(
  self : Schema,
  owner : String,
  t : IdlType,
  wire : Value,
  work : Ref[Int],
  depth : Int,
) -> Value? raise SchemaError {
  schema_tick(work, depth)
  if wire.kind() != self.wire_kind(t) {
    return None
  }
  match (t, wire) {
    (Named(name), Struct(fields)) =>
      match self.definitions.get(name) {
        Some(Record(_, flavor, expected, _)) =>
          Some(
            self.read_fields(
              definition_owner(name),
              expected,
              fields,
              flavor == "union",
              work,
              depth,
            ),
          )
        _ => None
      }
    (ListOf(elem), List(kind, values))
    | (SetOf(elem), SetValue(kind, values)) => {
      if self.wire_kind(elem) != kind {
        return None
      }
      let items = []
      for value in values {
        match self.read_type(owner, elem, value, work, depth + 1) {
          Some(v) => items.push(v)
          None => return None
        }
      }
      Some(
        if t is ListOf(_) {
          List(kind, items)
        } else {
          SetValue(kind, items)
        },
      )
    }
    (MapOf(key, value), MapValue(k, v, values)) => {
      let kk = self.wire_kind(key)
      let vk = self.wire_kind(value)
      if !values.is_empty() && (k != Some(kk) || v != Some(vk)) {
        return None
      }
      let items = []
      for pair in values {
        let k = match self.read_type(owner, key, pair.0, work, depth + 1) {
          Some(v) => v
          None => return None
        }
        let v = match self.read_type(owner, value, pair.1, work, depth + 1) {
          Some(v) => v
          None => return None
        }
        items.push((k, v))
      }
      Some(MapValue(Some(kk), Some(vk), items))
    }
    _ => Some(wire)
  }
}

///|
fn Schema::json_type(
  self : Schema,
  owner : String,
  t : IdlType,
  value : Value,
  depth : Int,
) -> Json raise SchemaError {
  if depth > 64 {
    raise InvalidSchema("schema JSON depth limit")
  }
  match (t, value) {
    (_, Bool(n)) => n.to_json()
    (_, Byte(n) | I16(n) | I32(n)) => n.to_json()
    (_, I64(n)) => n.to_string().to_json()
    (_, Double(n)) =>
      if n.is_nan() {
        "NaN".to_json()
      } else if n.is_inf() {
        (if n < 0.0 { "-Infinity" } else { "Infinity" }).to_json()
      } else {
        n.to_json()
      }
    (Base("string"), Binary(data)) => {
      let text = @utf8.decode(data) catch {
        _ => raise InvalidSchema("invalid UTF-8 string")
      }
      text.to_json()
    }
    (_, Binary(data)) => Json::object({ "$binary": schema_hex(data).to_json() })
    (_, Uuid(data)) => {
      let hex = schema_hex(data)
      if hex.length() != 32 {
        raise InvalidSchema("invalid UUID size")
      }
      (hex[:8].to_owned() +
      "-" +
      hex[8:12].to_owned() +
      "-" +
      hex[12:16].to_owned() +
      "-" +
      hex[16:20].to_owned() +
      "-" +
      hex[20:].to_owned()).to_json()
    }
    (ListOf(elem), List(_, values)) | (SetOf(elem), SetValue(_, values)) =>
      Json::array(values.map(v => self.json_type(owner, elem, v, depth + 1)))
    (MapOf(key, tvalue), MapValue(_, _, values)) =>
      Json::array(
        values.map(pair => {
          Json::array([
            self.json_type(owner, key, pair.0, depth + 1),
            self.json_type(owner, tvalue, pair.1, depth + 1),
          ])
        }),
      )
    (Named(name), Struct(values)) =>
      match self.definitions.get(name) {
        Some(Record(_, _, fields, _)) =>
          self.fields_json(definition_owner(name), fields, values, depth)
        _ => raise InvalidSchema("unknown struct")
      }
    _ => raise InvalidSchema("wire value does not match schema")
  }
}

///|
fn Schema::fields_json(
  self : Schema,
  owner : String,
  fields : Array[IdlField],
  values : Array[(Int, Value)],
  depth : Int,
) -> Json raise SchemaError {
  let object = Map([])
  for pair in values {
    for field in fields {
      if field.id == pair.0 {
        object[field.name] = self.json_type(
          owner,
          self.resolve(owner, field.field_type, 0),
          pair.1,
          depth + 1,
        )
        break
      }
    }
  }
  Json::object(object)
}

///|
/// Read a wire tree with unknown/type-mismatched fields skipped and required fields checked.
pub fn Schema::to_json(
  self : Schema,
  name : String,
  value : Value,
) -> Json raise SchemaError {
  let t = self.resolve(self.root, Named(name), 0)
  let value = match self.read_type(self.root, t, value, Ref(0), 0) {
    Some(v) => v
    None => raise InvalidSchema("root wire type mismatch")
  }
  self.json_type(self.root, t, value, 0)
}

///|
pub fn Schema::encode_json(
  self : Schema,
  name : String,
  input : Json,
  protocol : Protocol,
) -> Bytes raise {
  encode(self.from_json(name, input), protocol)
}

///|
pub fn Schema::decode_json(
  self : Schema,
  name : String,
  data : Bytes,
  protocol : Protocol,
) -> Json raise {
  let t = self.resolve(self.root, Named(name), 0)
  self.to_json(name, decode(data, self.wire_kind(t), protocol))
}