///|
/// Number of UTF-8 bytes needed to encode `s`.
///
/// MoonBit strings are UTF-16 internally; lone surrogates are counted as one
/// byte because they are encoded as the ASCII `?` substitution character
/// (matching Scala's `String.getBytes(UTF_8)` REPLACE behavior).
pub fn utf8_byte_length(s : String) -> Int32 {
  let mut total = 0
  let len = s.length()
  let mut i = 0
  while i < len {
    let unit = s[i].to_int()
    if unit < 0x80 {
      total = total + 1
      i = i + 1
    } else if unit < 0x800 {
      total = total + 2
      i = i + 1
    } else if unit < 0xD800 || unit > 0xDFFF {
      total = total + 3
      i = i + 1
    } else if unit <= 0xDBFF &&
      i + 1 < len &&
      s[i + 1].to_int() >= 0xDC00 &&
      s[i + 1].to_int() <= 0xDFFF {
      // Surrogate pair, encodes as 4 bytes.
      total = total + 4
      i = i + 2
    } else {
      // Lone surrogate, substituted with '?'.
      total = total + 1
      i = i + 1
    }
  }
  total
}

///|
/// Encode `s` as UTF-8 into `out`, which must have space for at least
/// `utf8_byte_length(s)` bytes. Returns the number of bytes written.
///
/// Lone surrogates are substituted with the ASCII `?` character (0x3F),
/// matching Scala's `String.getBytes(UTF_8)` replacement behavior.
pub fn write_utf8(s : String, out : FixedArray[Byte], offset : Int32) -> Int32 {
  let mut i = offset
  let len = s.length()
  let mut j = 0
  while j < len {
    let unit = s[j].to_int()
    if unit < 0x80 {
      out[i] = unit.to_byte()
      i = i + 1
      j = j + 1
    } else if unit < 0x800 {
      out[i] = (0xC0 | (unit >> 6)).to_byte()
      out[i + 1] = (0x80 | (unit & 0x3F)).to_byte()
      i = i + 2
      j = j + 1
    } else if unit < 0xD800 || unit > 0xDFFF {
      out[i] = (0xE0 | (unit >> 12)).to_byte()
      out[i + 1] = (0x80 | ((unit >> 6) & 0x3F)).to_byte()
      out[i + 2] = (0x80 | (unit & 0x3F)).to_byte()
      i = i + 3
      j = j + 1
    } else if unit <= 0xDBFF &&
      j + 1 < len &&
      s[j + 1].to_int() >= 0xDC00 &&
      s[j + 1].to_int() <= 0xDFFF {
      let hi = unit - 0xD800
      let lo = s[j + 1].to_int() - 0xDC00
      let cp = 0x10000 + (hi << 10) + lo
      out[i] = (0xF0 | (cp >> 18)).to_byte()
      out[i + 1] = (0x80 | ((cp >> 12) & 0x3F)).to_byte()
      out[i + 2] = (0x80 | ((cp >> 6) & 0x3F)).to_byte()
      out[i + 3] = (0x80 | (cp & 0x3F)).to_byte()
      i = i + 4
      j = j + 2
    } else {
      out[i] = b'?'
      i = i + 1
      j = j + 1
    }
  }
  i - offset
}

///|
/// Decode UTF-8 bytes into a MoonBit (UTF-16) string.
///
/// Malformed sequences fail with `DeserializationFailure`, matching the Rust
/// library (`String::from_utf8`). All standard UTF-8 forms are accepted,
/// including 4-byte sequences for astral code points.
pub fn read_utf8(bytes : BytesView) -> String raise DesertFailure {
  let len = bytes.length()
  let sb = StringBuilder()
  let mut i = 0
  while i < len {
    let b0 = bytes[i].to_int()
    if b0 < 0x80 {
      sb.write_char(b0.unsafe_to_char())
      i = i + 1
    } else if b0 < 0xC0 {
      // Continuation byte as a leading byte.
      raise DeserializationFailure(
        "Failed to decode string: invalid UTF-8 leading byte",
      )
    } else if b0 < 0xE0 {
      i = decode_2_bytes(bytes, i, b0, sb)
    } else if b0 < 0xF0 {
      i = decode_3_bytes(bytes, i, b0, sb)
    } else if b0 < 0xF8 {
      i = decode_4_bytes(bytes, i, b0, sb)
    } else {
      raise DeserializationFailure(
        "Failed to decode string: invalid UTF-8 leading byte",
      )
    }
  }
  sb.to_string()
}

///|
fn continuation(bytes : BytesView, i : Int32) -> Int32 raise DesertFailure {
  if i >= bytes.length() {
    raise DeserializationFailure(
      "Failed to decode string: truncated UTF-8 sequence",
    )
  }
  let b = bytes[i].to_int()
  if (b & 0xC0) != 0x80 {
    raise DeserializationFailure(
      "Failed to decode string: invalid UTF-8 continuation byte",
    )
  }
  b & 0x3F
}

///|
fn decode_2_bytes(
  bytes : BytesView,
  i : Int32,
  b0 : Int32,
  sb : StringBuilder,
) -> Int32 raise DesertFailure {
  let cp = ((b0 & 0x1F) << 6) | continuation(bytes, i + 1)
  if cp < 0x80 {
    raise DeserializationFailure(
      "Failed to decode string: overlong UTF-8 sequence",
    )
  }
  sb.write_char(cp.unsafe_to_char())
  i + 2
}

///|
fn decode_3_bytes(
  bytes : BytesView,
  i : Int32,
  b0 : Int32,
  sb : StringBuilder,
) -> Int32 raise DesertFailure {
  let cp = ((b0 & 0x0F) << 12) |
    (continuation(bytes, i + 1) << 6) |
    continuation(bytes, i + 2)
  if cp < 0x800 {
    raise DeserializationFailure(
      "Failed to decode string: overlong UTF-8 sequence",
    )
  }
  if cp >= 0xD800 && cp <= 0xDFFF {
    raise DeserializationFailure(
      "Failed to decode string: UTF-8 encoded surrogate",
    )
  }
  sb.write_char(cp.unsafe_to_char())
  i + 3
}

///|
fn decode_4_bytes(
  bytes : BytesView,
  i : Int32,
  b0 : Int32,
  sb : StringBuilder,
) -> Int32 raise DesertFailure {
  let cp = ((b0 & 0x07) << 18) |
    (continuation(bytes, i + 1) << 12) |
    (continuation(bytes, i + 2) << 6) |
    continuation(bytes, i + 3)
  if cp < 0x10000 || cp > 0x10FFFF {
    raise DeserializationFailure(
      "Failed to decode string: invalid UTF-8 code point",
    )
  }
  sb.write_char(cp.unsafe_to_char())
  i + 4
}