///|
/// UTF-16/UTF-8 helpers.
///
/// MoonBit strings are sequences of UTF-16 code units, whereas RFC 3986/3987
/// are defined over UTF-8 octets. Percent-encoding therefore has to go through
/// the UTF-8 encoding of a string, and percent-decoding has to reassemble
/// (possibly ill-formed) UTF-8 octets back into a string.

///|
/// Returns the UTF-16 code unit at `i` as an `Int`.
fn code_at(s : String, i : Int) -> Int {
  s[i].to_int()
}

///|
/// Returns the code unit at `i`, or `-1` if `i` is out of bounds.
fn code_or_eof(s : String, i : Int) -> Int {
  if i < 0 || i >= s.length() {
    -1
  } else {
    s[i].to_int()
  }
}

///|
/// Returns the code point starting at `i` together with its length in code
/// units. An unpaired surrogate is returned as is, which no table allows.
fn next_code_point(s : String, i : Int) -> (Int, Int) {
  let c = code_at(s, i)
  if c >= 0xd800 && c <= 0xdbff && i + 1 < s.length() {
    let d = code_at(s, i + 1)
    if d >= 0xdc00 && d <= 0xdfff {
      return (0x10000 + ((c - 0xd800) << 10) + (d - 0xdc00), 2)
    }
  }
  (c, 1)
}

///|
/// Returns the substring between the two code unit offsets.
fn slice(s : String, start : Int, end : Int) -> String {
  if start == 0 && end == s.length() {
    s
  } else {
    s.view(start_offset=start, end_offset=end).to_owned()
  }
}

///|
/// Lowercases the ASCII letters of `s`, leaving other characters untouched.
fn ascii_lowercase(s : String) -> String {
  let mut needed = false
  for i in 0..= 'A'.to_int() && x <= 'Z'.to_int() {
      needed = true
      break
    }
  }
  if !needed {
    return s
  }
  let sb = StringBuilder::new(size_hint=s.length())
  for ch in s {
    if ch >= 'A' && ch <= 'Z' {
      sb.write_char((ch.to_int() + 32).to_char().unwrap())
    } else {
      sb.write_char(ch)
    }
  }
  sb.to_string()
}

///|
/// Appends the UTF-8 encoding of `cp` to `out`.
fn utf8_encode_code_point(out : Array[Byte], cp : Int) -> Unit {
  if cp < 0x80 {
    out.push(cp.to_byte())
  } else if cp < 0x800 {
    out.push((0xc0 | (cp >> 6)).to_byte())
    out.push((0x80 | (cp & 0x3f)).to_byte())
  } else if cp < 0x10000 {
    out.push((0xe0 | (cp >> 12)).to_byte())
    out.push((0x80 | ((cp >> 6) & 0x3f)).to_byte())
    out.push((0x80 | (cp & 0x3f)).to_byte())
  } else {
    out.push((0xf0 | (cp >> 18)).to_byte())
    out.push((0x80 | ((cp >> 12) & 0x3f)).to_byte())
    out.push((0x80 | ((cp >> 6) & 0x3f)).to_byte())
    out.push((0x80 | (cp & 0x3f)).to_byte())
  }
}

///|
/// Returns the expected length of the UTF-8 sequence starting with the
/// non-ASCII octet `x`, or `0` if `x` cannot start one.
fn utf8_char_width(x : Int) -> Int {
  if x < 0xc2 {
    0
  } else if x < 0xe0 {
    2
  } else if x < 0xf0 {
    3
  } else if x < 0xf5 {
    4
  } else {
    0
  }
}

///|
/// Decodes a byte range known to be well-formed UTF-8 into a string.
fn utf8_decode_valid(b : ArrayView[Byte], start : Int, end : Int) -> String {
  let sb = StringBuilder::new(size_hint=end - start)
  let mut i = start
  while i < end {
    let x = b[i].to_int()
    let cp = if x < 0x80 {
      i += 1
      x
    } else if x < 0xe0 {
      let v = ((x & 0x1f) << 6) | (b[i + 1].to_int() & 0x3f)
      i += 2
      v
    } else if x < 0xf0 {
      let v = ((x & 0x0f) << 12) |
        ((b[i + 1].to_int() & 0x3f) << 6) |
        (b[i + 2].to_int() & 0x3f)
      i += 3
      v
    } else {
      let v = ((x & 0x07) << 18) |
        ((b[i + 1].to_int() & 0x3f) << 12) |
        ((b[i + 2].to_int() & 0x3f) << 6) |
        (b[i + 3].to_int() & 0x3f)
      i += 4
      v
    }
    sb.write_char(cp.to_char().unwrap())
  }
  sb.to_string()
}

///|
/// Splits `b` into chunks of a maximal well-formed UTF-8 run followed by a
/// maximal invalid sequence, following the "maximal subpart" rule of the
/// Unicode standard (the same one Rust's `String::from_utf8_lossy` uses).
///
/// `on_chunk` receives the decoded valid run (possibly empty) and the offsets
/// of the invalid sequence that follows it (empty when the run reaches the end
/// of the input). No chunk is emitted for empty input.
fn utf8_chunks(
  b : ArrayView[Byte],
  on_chunk : (String, ArrayView[Byte]) -> Unit,
) -> Unit {
  let len = b.length()
  let mut valid_start = 0
  let mut i = 0

  ///|
  fn is_cont(j : Int) -> Bool {
    let x = b[j].to_int()
    x >= 0x80 && x <= 0xbf
  }

  while i < len {
    let first = b[i].to_int()
    if first < 0x80 {
      i += 1
      continue
    }
    // Number of invalid octets, or 0 if the sequence at `i` is well-formed.
    let mut bad = 0
    let w = utf8_char_width(first)
    if w == 0 {
      bad = 1
    } else if i + 1 >= len {
      bad = len - i
    } else {
      let second = b[i + 1].to_int()
      let second_ok = if w == 2 {
        second >= 0x80 && second <= 0xbf
      } else if w == 3 {
        if first == 0xe0 {
          second >= 0xa0 && second <= 0xbf
        } else if first == 0xed {
          second >= 0x80 && second <= 0x9f
        } else {
          second >= 0x80 && second <= 0xbf
        }
      } else if first == 0xf0 {
        second >= 0x90 && second <= 0xbf
      } else if first == 0xf4 {
        second >= 0x80 && second <= 0x8f
      } else {
        second >= 0x80 && second <= 0xbf
      }
      if !second_ok {
        bad = 1
      } else if w == 2 {
        i += 2
        continue
      } else if i + 2 >= len {
        bad = len - i
      } else if !is_cont(i + 2) {
        bad = 2
      } else if w == 3 {
        i += 3
        continue
      } else if i + 3 >= len {
        bad = len - i
      } else if !is_cont(i + 3) {
        bad = 3
      } else {
        i += 4
        continue
      }
    }
    on_chunk(utf8_decode_valid(b, valid_start, i), b[i:i + bad])
    i += bad
    valid_start = i
  }
  if i > valid_start {
    on_chunk(utf8_decode_valid(b, valid_start, i), b[i:i])
  }
}