///|
pub(all) enum Case {
  Pascal
  Snake
}

///|
/// Whether `ident` may be used as a Rust identifier as-is (upstream
/// `accept_as_ident`): false for keywords, which get a trailing `_`.
pub fn accept_as_ident(ident : String) -> Bool {
  !(ident
  is ("_"
  | "abstract"
  | "as"
  | "async"
  | "await"
  | "become"
  | "box"
  | "break"
  | "const"
  | "continue"
  | "crate"
  | "do"
  | "dyn"
  | "else"
  | "enum"
  | "extern"
  | "false"
  | "final"
  | "fn"
  | "for"
  | "gen"
  | "if"
  | "impl"
  | "in"
  | "let"
  | "loop"
  | "macro"
  | "match"
  | "mod"
  | "move"
  | "mut"
  | "override"
  | "priv"
  | "pub"
  | "ref"
  | "return"
  | "Self"
  | "self"
  | "static"
  | "struct"
  | "super"
  | "trait"
  | "true"
  | "try"
  | "type"
  | "typeof"
  | "unsafe"
  | "unsized"
  | "use"
  | "virtual"
  | "where"
  | "while"
  | "yield"))
}

///|
fn to_case(s : String, case : Case) -> String {
  match case {
    Pascal => @heck.to_pascal_case(s)
    Snake => @heck.to_snake_case(s)
  }
}

///|
/// Turn arbitrary text into a Rust identifier in the given case (upstream
/// `sanitize`).
pub fn sanitize(input : String, case : Case) -> String {
  let out = match input {
    "+1" => "plus1"
    "-1" => "minus1"
    _ => {
      let buf = StringBuilder()
      for c in input {
        if c == '\'' {
          continue
        }
        buf.write_char(if @unicode.is_xid_continue(c) { c } else { '-' })
      }
      to_case(buf.to_string(), case)
    }
  }
  let prefix = to_case("x", case)
  let out = match out.get_char(0) {
    None => prefix
    Some(c) if @unicode.is_xid_start(c) => out
    Some(_) => prefix + out
  }
  if accept_as_ident(out) {
    out
  } else {
    out + "_"
  }
}

///|
/// Sanitize and report the original name if it changed.
pub fn recase(input : String, case : Case) -> (String, String?) {
  let new = sanitize(input, case)
  (new, if new == input { None } else { Some(input) })
}

///|
fn metadata_description(metadata : @schema.Metadata?) -> String? {
  metadata.bind(m => m.description)
}

///|
fn metadata_title(metadata : @schema.Metadata?) -> String? {
  metadata.bind(m => m.title)
}

///|
fn metadata_default(metadata : @schema.Metadata?) -> @serde_json.Value? {
  metadata.bind(m => m.default)
}

///|
fn metadata_title_and_description(metadata : @schema.Metadata?) -> String? {
  match metadata {
    None => None
    Some(m) =>
      match (m.title, m.description) {
        (Some(t), Some(d)) => Some("\{t}\n\n\{d}")
        (Some(t), None) => Some(t)
        (None, Some(d)) => Some(d)
        (None, None) => None
      }
  }
}

///|
/// The type name to use: a required name wins, then the title, then a
/// suggestion.
fn get_type_name(type_name : Name, metadata : @schema.Metadata?) -> String? {
  let name = match (type_name, metadata_title(metadata)) {
    (Required(name), _) => name
    (Suggested(name), None) => name
    (_, Some(name)) => name
    (Unknown, None) => return None
  }
  Some(sanitize(name, Pascal))
}

///|
priv struct TypePatch {
  name : String
  derives : @collections.StrSet
  attrs : @collections.StrSet
}

///|
fn TypePatch::new(
  settings : TypeSpaceSettings,
  type_name : String,
) -> TypePatch {
  match settings.patch.get(type_name) {
    None =>
      {
        name: type_name,
        derives: @collections.StrSet::new(),
        attrs: @collections.StrSet::new(),
      }
    Some(patch) =>
      {
        name: patch.rename.unwrap_or(type_name),
        derives: @collections.StrSet::from_array(patch.derives),
        attrs: @collections.StrSet::from_array(patch.attrs),
      }
  }
}

///|
/// Whether all items are distinct.
fn[T : Hash + Eq] unique(items : Array[T]) -> Bool {
  let seen : Map[T, Unit] = Map([])
  for item in items {
    if seen.contains(item) {
      return false
    }
    seen[item] = ()
  }
  true
}

///|
/// UTF-8 byte length (Rust `str::len`).
fn utf8_len(s : String) -> Int {
  let mut n = 0
  for c in s {
    let cp = c.to_int()
    n += if cp < 0x80 {
      1
    } else if cp < 0x800 {
      2
    } else if cp < 0x10000 {
      3
    } else {
      4
    }
  }
  n
}

///|
/// Rust `&s[byte_offset..]`; aborts (like Rust panics) on a non-boundary.
fn slice_from_byte(s : String, byte_offset : Int) -> String raise TypifyError {
  let mut n = 0
  let mut i = 0
  for c in s {
    if n == byte_offset {
      return s.unsafe_substring(start=i, end=s.length())
    }
    if n > byte_offset {
      break
    }
    let cp = c.to_int()
    n += if cp < 0x80 {
      1
    } else if cp < 0x800 {
      2
    } else if cp < 0x10000 {
      3
    } else {
      4
    }
    i += c.utf16_len()
  }
  if n == byte_offset {
    return ""
  }
  raise panic_with("byte index \{byte_offset} is not a char boundary of \{s}")
}