///|
/// Decode predefined (`&` `<` `>` `"` `'`) and numeric
/// (`&#NNN;`, `&#xHHH;`) character references. Unknown or malformed
/// references are left verbatim (lenient, mirroring quick-xml defaults).
pub fn decode_entities(input : String) -> String {
  let len = input.length()
  let mut has_amp = false
  for i in 0.. {
        sb.write_string(text)
        i = next
      }
      None => {
        sb.write_char(cu('&').unsafe_to_char())
        i += 1
      }
    }
  }
  sb.to_string()
}

///|
/// Try to decode one entity whose `&` sits at offset `i`.
/// Returns the replacement text and the offset just past the terminating `;`.
fn decode_one_entity(input : String, amp : Int) -> (String, Int)? {
  let len = input.length()
  let name_start = amp + 1
  let mut j = name_start
  while j < len &&
        input[j].to_int() != cu(';') &&
        input[j].to_int() != cu('&') &&
        j - name_start < 32 {
    j += 1
  }
  if j >= len || input[j].to_int() != cu(';') {
    return None
  }
  let name = substring(input, name_start, j)
  let semi = j + 1
  if name.length() == 0 {
    return None
  }
  // numeric references
  if name[0].to_int() == cu('#') {
    let cp : Int? = if name.length() > 1 &&
      (name[1].to_int() == cu('x') || name[1].to_int() == cu('X')) {
      parse_hex_cp(substring(name, 2, name.length()))
    } else {
      parse_dec_cp(substring(name, 1, name.length()))
    }
    match cp {
      Some(cp) =>
        match code_point_to_string(cp) {
          Some(text) => Some((text, semi))
          None => None
        }
      None => None
    }
  } else {
    // named references
    let repl : String? = if name is "amp" {
      Some("&")
    } else if name is "lt" {
      Some("<")
    } else if name is "gt" {
      Some(">")
    } else if name is "quot" {
      Some("\"")
    } else if name is "apos" {
      Some("'")
    } else {
      None
    }
    match repl {
      Some(text) => Some((text, semi))
      None => None
    }
  }
}

///|
fn parse_dec_cp(s : String) -> Int? {
  if s.length() == 0 {
    return None
  }
  let mut v = 0
  for i in 0..= cu('0') && c <= cu('9') else { return None }
    v = v * 10 + (c - cu('0'))
    guard v <= 0x110000 else { return None }
  }
  Some(v)
}

///|
fn parse_hex_cp(s : String) -> Int? {
  if s.length() == 0 {
    return None
  }
  let mut v = 0
  for i in 0..= cu('0') && c <= cu('9') {
      c - cu('0')
    } else if c >= cu('a') && c <= cu('f') {
      c - cu('a') + 10
    } else if c >= cu('A') && c <= cu('F') {
      c - cu('A') + 10
    } else {
      return None
    }
    v = v * 16 + digit
    guard v <= 0x110000 else { return None }
  }
  Some(v)
}

///|
/// Convert a Unicode code point to its UTF-16 string form.
/// Rejects surrogates and out-of-range values.
fn code_point_to_string(cp : Int) -> String? {
  if cp >= 0xD800 && cp <= 0xDFFF {
    return None
  }
  if cp < 0 || cp > 0x10FFFF {
    return None
  }
  let sb = StringBuilder()
  if cp < 0x10000 {
    sb.write_char(cp.unsafe_to_char())
  } else {
    let v = cp - 0x10000
    let hi = 0xD800 + (v >> 10)
    let lo = 0xDC00 + (v & 0x3FF)
    sb.write_char(hi.unsafe_to_char())
    sb.write_char(lo.unsafe_to_char())
  }
  Some(sb.to_string())
}