///|
/// A protobuf JSON document that breaks a rule: the field path and the rule.
priv suberror Shape {
  Shape(String, String)
}

///|
/// The fields of a protobuf JSON object, checked against the names that the
/// message has, as sigstore-go reads them: the lowerCamelCase name or the proto
/// field name, no unknown field, no field twice, and `null` as absent.
priv struct Fields {
  path : String
  values : Map[String, Json]
}

///|
/// The proto field name of a lowerCamelCase JSON name.
fn snake_case(name : String) -> String {
  let out = StringBuilder()
  for char in name {
    if char >= 'A' && char <= 'Z' {
      out.write_char('_')
      out.write_char((char.to_int() + 32).unsafe_to_char())
    } else {
      out.write_char(char)
    }
  }
  out.to_string()
}

///|
fn fields(json : Json, path : String, names : Array[String]) -> Fields raise Shape {
  guard json is Object(map) else { raise Shape(path, "expected an object") }
  let values : Map[String, Json] = Map([])
  for key, value in map {
    let name = if names.contains(key) {
      key
    } else {
      match names.iter().find_first(name => snake_case(name) == key) {
        Some(name) => name
        None => raise Shape(join(path, key), "unknown field")
      }
    }
    guard !values.contains(name) else { raise Shape(join(path, name), "field is repeated") }
    if value is Null {
      continue
    }
    values[name] = value
  }
  { path, values }
}

///|
fn join(path : String, name : String) -> String {
  if path.is_empty() {
    name
  } else {
    "\{path}.\{name}"
  }
}

///|
fn Fields::at(self : Fields, name : String) -> String {
  join(self.path, name)
}

///|
fn Fields::has(self : Fields, name : String) -> Bool {
  self.values.contains(name)
}

///|
fn Fields::get(self : Fields, name : String) -> Json? {
  self.values.get(name)
}

///|
fn Fields::object(
  self : Fields,
  name : String,
  names : Array[String],
) -> Fields? raise Shape {
  match self.get(name) {
    Some(json) => Some(fields(json, self.at(name), names))
    None => None
  }
}

///|
fn Fields::require_object(
  self : Fields,
  name : String,
  names : Array[String],
) -> Fields raise Shape {
  guard self.object(name, names) is Some(found) else {
    raise Shape(self.at(name), "field is required")
  }
  found
}

///|
fn Fields::string(self : Fields, name : String) -> String? raise Shape {
  match self.get(name) {
    Some(String(text)) => Some(text)
    Some(_) => raise Shape(self.at(name), "expected a string")
    None => None
  }
}

///|
fn Fields::require_string(self : Fields, name : String) -> String raise Shape {
  guard self.string(name) is Some(text) && !text.is_empty() else {
    raise Shape(self.at(name), "field is required")
  }
  text
}

///|
/// A `bytes` field: base64 in either alphabet. An absent field is empty.
fn Fields::bytes(self : Fields, name : String) -> Bytes raise Shape {
  match self.string(name) {
    Some(text) =>
      @encoding.decode_base64(text) catch {
        _ => raise Shape(self.at(name), "not base64")
      }
    None => b""
  }
}

///|
fn Fields::require_bytes(self : Fields, name : String) -> Bytes raise Shape {
  let bytes = self.bytes(name)
  guard !bytes.is_empty() else { raise Shape(self.at(name), "field is required") }
  bytes
}

///|
/// An `int64` field: a JSON string or an integral number. An absent field is 0.
fn Fields::int64(self : Fields, name : String) -> Int64 raise Shape {
  let text = match self.get(name) {
    Some(String(text)) => text
    Some(Number(value, repr~)) =>
      match repr {
        Some(text) => text
        None => {
          guard value == value.floor() && value.abs() < 9007199254740992.0 else {
            raise Shape(self.at(name), "not an integer")
          }
          return value.to_int64()
        }
      }
    Some(_) => raise Shape(self.at(name), "expected an integer")
    None => return 0L
  }
  let mut digits = 0
  for index, char in text {
    if char >= '0' && char <= '9' {
      digits += 1
    } else if !(index == 0 && char == '-') {
      raise Shape(self.at(name), "not an integer")
    }
  }
  guard digits > 0 else { raise Shape(self.at(name), "not an integer") }
  @string.parse_int64(text) catch {
    _ => raise Shape(self.at(name), "not a 64-bit integer")
  }
}

///|
fn Fields::array(self : Fields, name : String) -> Array[Json] raise Shape {
  match self.get(name) {
    Some(Array(items)) => items
    Some(_) => raise Shape(self.at(name), "expected an array")
    None => []
  }
}

///|
/// An enum field: its name, or the name of its number in `names`.
fn Fields::enum_name(
  self : Fields,
  name : String,
  names : Array[String],
) -> String? raise Shape {
  match self.get(name) {
    Some(String(text)) => Some(text)
    Some(Number(value, ..)) => {
      let index = value.to_int()
      guard index.to_double() == value && index >= 0 && index < names.length() else {
        raise Shape(self.at(name), "unknown enum number")
      }
      Some(names[index])
    }
    Some(_) => raise Shape(self.at(name), "expected an enum")
    None => None
  }
}

///|
/// Parse a JSON document as protobuf JSON expects: duplicate keys fail and
/// numbers keep their text.
fn parse_json(text : StringView) -> Json raise Shape {
  @json.parse(text, reject_duplicate_keys=true, preserve_number_repr=true) catch {
    err => raise Shape("", "not JSON: \{err}")
  }
}