// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Returns the offset (charcode index) of the first occurrence of the given
/// substring. If the substring is not found, it returns None.
pub fn StringView::find(self : StringView, str : StringView) -> Int? {
  let pattern_len = str.length()
  match pattern_len {
    0 => Some(0)
    1 => {
      let found = find_code_unit_from_view(
        self,
        0,
        self.length(),
        str.unsafe_get(0),
      )
      if found < 0 {
        None
      } else {
        Some(found)
      }
    }
    _ =>
      if pattern_len > self.length() {
        None
      } else {
        find_by_two_anchors(self, str)
      }
  }
  // Worst-case linearity: after dense false-anchor candidates the search
  // cuts over to a KMP fallback (see the two-anchor search internals), so
  // the total cost stays O(self + str) even on adversarial inputs.
}

///|
/// Returns the offset of the first occurrence of the given substring. If the
/// substring is not found, it returns None.
pub fn String::find(self : String, str : StringView) -> Int? {
  self[:].find(str)
}

///|
test "find" {
  assert_true("hello".find("o") == Some(4))
  assert_true("hello".find("l") == Some(2))
  assert_true("hello".find("hello") == Some(0))
  assert_true("hello".find("h") == Some(0))
  assert_true("hello".find("") == Some(0))
  assert_true("hello".find("world") == None)
  assert_true("".find("") == Some(0))
  assert_true("".find("a") == None)
  assert_true("hello hello".find("hello") == Some(0))
  assert_true("aaa".find("aa") == Some(0))
  assert_true("πŸ˜€πŸ˜€".find("πŸ˜€") == Some(0))
  assert_true(
    ("πŸ˜€πŸ˜€aa".repeat(20) + "πŸ˜€πŸ˜€πŸ˜€πŸ˜€").find("πŸ˜€πŸ˜€πŸ˜€πŸ˜€") ==
    Some(120),
  )
  assert_true(
    ("πŸ˜€πŸ˜€πŸ˜€πŸ˜€" + "πŸ˜€πŸ˜€aa".repeat(20)).find("πŸ˜€πŸ˜€πŸ˜€πŸ˜€") ==
    Some(0),
  )
}

///|
/// Returns the UTF-16 code-unit offset, relative to the beginning of this view,
/// of the first character that satisfies the given predicate. If no such
/// character is found, it returns None.
#locals(pred)
pub fn StringView::find_by(self : StringView, pred : (Char) -> Bool) -> Int? {
  for c in self; offset = 0 {
    if pred(c) {
      break Some(offset)
    }
    continue offset + c.utf16_len()
  } nobreak {
    None
  }
}

///|
/// Returns the UTF-16 code-unit offset, relative to the beginning of this
/// string, of the first character that satisfies the given predicate. If no
/// such character is found, it returns None.
pub fn String::find_by(self : String, pred : (Char) -> Bool) -> Int? {
  self[:].find_by(pred)
}

///|
test "find_by" {
  assert_true("hello".find_by(c => c == 'o') == Some(4))
  assert_true("hello".find_by(c => c == 'l') == Some(2))
  assert_true("hello".find_by(c => c == 'z') == None)
  assert_true("".find_by(c => c == 'a') == None)
  assert_true("hello".find_by(c => c is ('0'..='9')) == None)
  assert_true("hello123".find_by(c => c is ('0'..='9')) == Some(5))
  assert_true("hello".find_by(c => c is ('A'..='Z')) == None)
  assert_true("Hello".find_by(c => c is ('A'..='Z')) == Some(0))
  assert_true("Ξ±Ξ²Ξ³".find_by(c => c == 'Ξ²') == Some(1))
}

///|
test "find_by returns UTF-16 code-unit offsets" {
  assert_true("πŸ˜€X".find_by(c => c == 'X') == Some(2))
  assert_true("πŸ˜€πŸ˜πŸ˜‚".find_by(c => c == 'πŸ˜‚') == Some(4))
  let view = "aπŸ˜€Xz"[1:4]
  assert_true(view.find_by(c => c == 'X') == Some(2))
}

///|
/// Returns the offset of the last occurrence of the given substring. If the
/// substring is not found, it returns None.
pub fn StringView::rev_find(self : StringView, str : StringView) -> Int? {
  let pattern_len = str.length()
  match pattern_len {
    0 => Some(self.length())
    1 => {
      let found = rev_find_code_unit_from_view(
        self,
        0,
        self.length(),
        str.unsafe_get(0),
      )
      if found < 0 {
        None
      } else {
        Some(found)
      }
    }
    _ =>
      if pattern_len > self.length() {
        None
      } else {
        rev_find_by_two_anchors(self, str)
      }
  }
  // Worst-case linearity: after dense false-anchor candidates the search
  // cuts over to a KMP fallback (see the two-anchor search internals), so
  // the total cost stays O(self + str) even on adversarial inputs.
}

///|
/// Returns the offset (charcode index) of the last occurrence of the given
/// substring. If the substring is not found, it returns None.
pub fn String::rev_find(self : String, str : StringView) -> Int? {
  self[:].rev_find(str)
}

///|
test "rev_find" {
  assert_true("hello".rev_find("o") == Some(4))
  assert_true("hello".rev_find("l") == Some(3))
  assert_true("hello".rev_find("hello") == Some(0))
  assert_true("hello".rev_find("h") == Some(0))
  assert_true("hello".rev_find("") == Some(5))
  assert_true("hello".rev_find("world") == None)
  assert_true("".rev_find("") == Some(0))
  assert_true("".rev_find("a") == None)
  assert_true("hello hello".rev_find("hello") == Some(6))
  assert_true("aaa".rev_find("aa") == Some(1))
  assert_true("πŸ˜€πŸ˜€".rev_find("πŸ˜€") == Some(2))
  assert_true(
    ("πŸ˜€πŸ˜€aa".repeat(20) + "πŸ˜€πŸ˜€πŸ˜€πŸ˜€").rev_find("πŸ˜€πŸ˜€πŸ˜€πŸ˜€") ==
    Some(120),
  )
  assert_true(
    ("πŸ˜€πŸ˜€πŸ˜€πŸ˜€" + "πŸ˜€πŸ˜€aa".repeat(20)).rev_find("πŸ˜€πŸ˜€πŸ˜€πŸ˜€") ==
    Some(4),
  )
}

///|
/// Returns true if the given substring is suffix of this string.
#alias(ends_with, deprecated)
pub fn StringView::has_suffix(self : StringView, str : StringView) -> Bool {
  let self_len = self.length()
  let str_len = str.length()
  guard str_len <= self_len else { return false }
  let start = self_len - str_len
  // Compare the anchor code unit inline: a mismatch there is the common case
  // and is cheaper to reject than to pay the range-compare call overhead.
  guard str_len == 0 || self.unsafe_get(start) == str.unsafe_get(0) else {
    return false
  }
  self
  .str()
  .unsafe_range_equal(
    str.str(),
    self_off=self.start() + start,
    other_off=str.start(),
    len=str_len,
  )
}

///|
/// Returns true if the given substring is suffix of this string.
#alias(ends_with, deprecated)
pub fn String::has_suffix(self : String, str : StringView) -> Bool {
  self[:].has_suffix(str)
}

///|
test "has_suffix" {
  inspect("hello".has_suffix("lo"), content="true")
  inspect("hello".has_suffix("hello"), content="true")
  inspect("hello".has_suffix(""), content="true")
  inspect("hello".has_suffix("world"), content="false")
  inspect("hello".has_suffix("hel"), content="false")
  inspect("".has_suffix(""), content="true")
  inspect("".has_suffix("a"), content="false")
  inspect("hello world".has_suffix("world"), content="true")
  inspect("πŸ˜€πŸ˜€".has_suffix("πŸ˜€"), content="true")
  inspect("πŸ˜€πŸ˜€".has_suffix("πŸ˜€πŸ˜€"), content="true")
}

///|
/// Returns true if this string starts with the given substring.
#alias(starts_with, deprecated)
pub fn StringView::has_prefix(self : StringView, str : StringView) -> Bool {
  let str_len = str.length()
  guard str_len <= self.length() else { return false }
  // Compare the anchor code unit inline: a mismatch there is the common case
  // and is cheaper to reject than to pay the range-compare call overhead.
  guard str_len == 0 || self.unsafe_get(0) == str.unsafe_get(0) else {
    return false
  }
  self
  .str()
  .unsafe_range_equal(
    str.str(),
    self_off=self.start(),
    other_off=str.start(),
    len=str_len,
  )
}

///|
/// Returns true if this string starts with the given substring.
#alias(starts_with, deprecated)
pub fn String::has_prefix(self : String, str : StringView) -> Bool {
  self[:].has_prefix(str)
}

///|
test "has_prefix" {
  inspect("hello".has_prefix("h"), content="true")
  inspect("hello".has_prefix("he"), content="true")
  inspect("hello".has_prefix(""), content="true")
  inspect("hello".has_prefix("world"), content="false")
  inspect("hello".has_prefix("lo"), content="false")
  inspect("".has_prefix(""), content="true")
  inspect("".has_prefix("a"), content="false")
  inspect("πŸ˜€hello".has_prefix("πŸ˜€"), content="true")
  inspect("πŸ˜€πŸ˜ƒhello".has_prefix("πŸ˜€πŸ˜ƒ"), content="true")
  inspect("πŸ˜€hello".has_prefix("πŸ˜ƒ"), content="false")
  inspect("helloπŸ˜€".has_prefix("πŸ˜€"), content="false")
}

///|
test "StringView has_prefix and has_suffix" {
  let view = "xπŸ˜€hello worldy"[1:14]
  assert_true(view.has_prefix("πŸ˜€hello"))
  assert_false(view.has_prefix("world"))
  assert_false(view.has_prefix("πŸ˜€hello world!"))
  assert_true(view.has_suffix("world"))
  assert_false(view.has_suffix("πŸ˜€hello"))
  assert_false(view.has_suffix("πŸ˜€hello world!"))
}

///|
/// Removes the given suffix from the string if it exists.
///
/// Returns `Some(prefix)` if the string ends with the given suffix,
/// where `prefix` is the string without the suffix.
/// Returns `None` if the string does not end with the suffix.
///
/// # Example
///
/// ```mbt check
/// test {
///   assert_true("hello world".strip_suffix(" world") == Some("hello"))
///   assert_true("hello world".strip_suffix(" moon") == None)
///   assert_true("hello".strip_suffix("hello") == Some(""))
/// }
/// ```
#alias(chop_suffix)
pub fn String::strip_suffix(self : String, suffix : StringView) -> StringView? {
  self[:].strip_suffix(suffix)
}

///|
test "strip_prefix" {
  assert_true("hello world".strip_prefix("hello ") == Some("world"))
  assert_true("hello world".strip_prefix("hi ") == None)
  assert_true("hello".strip_prefix("hello") == Some(""))
  assert_true("".strip_prefix("") == Some(""))
  assert_true("".strip_prefix("a") == None)
  assert_true("abc".strip_prefix("") == Some("abc"))
  assert_true("πŸ˜€hello".strip_prefix("πŸ˜€") == Some("hello"))
  assert_true("πŸ˜€πŸ˜ƒhello".strip_prefix("πŸ˜€πŸ˜ƒ") == Some("hello"))
}

///|
test "strip_suffix" {
  assert_true("hello world".strip_suffix(" world") == Some("hello"))
  assert_true("hello world".strip_suffix(" moon") == None)
  assert_true("hello".strip_suffix("hello") == Some(""))
  assert_true("".strip_suffix("") == Some(""))
  assert_true("".strip_suffix("a") == None)
  assert_true("abc".strip_suffix("") == Some("abc"))
  assert_true("helloπŸ˜€".strip_suffix("πŸ˜€") == Some("hello"))
  assert_true("helloπŸ˜€πŸ˜ƒ".strip_suffix("πŸ˜€πŸ˜ƒ") == Some("hello"))
}

///|
/// Removes the given prefix from the string if it exists.
///
/// Returns `Some(suffix)` if the string starts with the given prefix,
/// where `suffix` is the string without the prefix.
/// Returns `None` if the string does not start with the prefix.
///
/// # Example
///
/// ```mbt check
/// test {
///   assert_true("hello world".strip_prefix("hello ") == Some("world"))
///   assert_true("hello world".strip_prefix("hi ") == None)
///   assert_true("hello".strip_prefix("hello") == Some(""))
/// }
/// ```
#alias(chop_prefix)
pub fn String::strip_prefix(self : String, prefix : StringView) -> StringView? {
  self[:].strip_prefix(prefix)
}

///|
/// Removes the given prefix from the view if it exists.
///
/// Returns `Some(suffix)` if the view starts with the given prefix,
/// where `suffix` is the view without the prefix.
/// Returns `None` if the view does not start with the prefix.
///
/// # Example
///
/// ```mbt check
/// test {
///   let view = "hello world"[:]
///   assert_true(view.strip_prefix("hello ") == Some("world"))
///   assert_true(view.strip_prefix("hi ") == None)
///   assert_true(view.strip_prefix("hello world") == Some(""))
/// }
/// ```
#alias(chop_prefix)
pub fn StringView::strip_prefix(
  self : StringView,
  prefix : StringView,
) -> StringView? {
  let prefix_len = prefix.length()
  if self.length() >= prefix_len && self.view(end_offset=prefix_len) == prefix {
    Some(self.view(start_offset=prefix_len))
  } else {
    None
  }
}

///|
/// Removes the given suffix from the view if it exists.
///
/// Returns `Some(prefix)` if the view ends with the given suffix,
/// where `prefix` is the view without the suffix.
/// Returns `None` if the view does not end with the suffix.
///
/// # Example
///
/// ```mbt check
/// test {
///   let view = "hello world"[:]
///   assert_true(view.strip_suffix(" world") == Some("hello"))
///   assert_true(view.strip_suffix(" moon") == None)
///   assert_true(view.strip_suffix("hello world") == Some(""))
/// }
/// ```
#alias(chop_suffix)
pub fn StringView::strip_suffix(
  self : StringView,
  suffix : StringView,
) -> StringView? {
  let self_len = self.length()
  let suffix_len = suffix.length()
  if self_len >= suffix_len &&
    self.view(start_offset=self_len - suffix_len) == suffix {
    Some(self.view(end_offset=self_len - suffix_len))
  } else {
    None
  }
}

///|
/// Converts the View into an array of Chars.
///
/// # Example
///
/// ```mbt check
/// test {
///   let view = "Hello🀣xa"[1:8]
///   let chars = view.to_array()
///   @debug.debug_inspect(chars, content="['e', 'l', 'l', 'o', '🀣', 'x']")
/// }
/// ```
pub fn StringView::to_array(self : StringView) -> Array[Char] {
  self
  .iter()
  .fold(init=Array::new(capacity=self.length()), (rv, c) => {
    rv.push(c)
    rv
  })
}

///|
/// Convert to `bytes`.
#deprecated("Check `@encoding/utf8.encode`")
#coverage.skip
pub fn StringView::to_bytes(self : StringView) -> Bytes {
  let array = FixedArray::make(self.length() * 2, b'\x00')
  array.blit_from_string(0, self.data(), self.start_offset(), self.length())
  array |> unsafe_to_bytes
}

///|
test "View::strip_prefix" {
  let view = "hello world"[:]
  assert_true(view.strip_prefix("hello ") == Some("world"))
  assert_true(view.strip_prefix("hi ") == None)
  assert_true(view.strip_prefix("hello world") == Some(""))
  assert_true(view.strip_prefix("") == Some("hello world"))
  let empty_view = ""[:]
  assert_true(empty_view.strip_prefix("") == Some(""))
  assert_true(empty_view.strip_prefix("a") == None)
  let unicode_view = "πŸ˜€helloπŸ˜ƒ"[:]
  assert_true(unicode_view.strip_prefix("πŸ˜€") == Some("helloπŸ˜ƒ"))
  assert_true(unicode_view.strip_prefix("πŸ˜ƒ") == None)
}

///|
test "View::strip_suffix" {
  let view = "hello world"[:]
  assert_true(view.strip_suffix(" world") == Some("hello"))
  assert_true(view.strip_suffix(" moon") == None)
  assert_true(view.strip_suffix("hello world") == Some(""))
  assert_true(view.strip_suffix("") == Some("hello world"))
  let empty_view = ""[:]
  assert_true(empty_view.strip_suffix("") == Some(""))
  assert_true(empty_view.strip_suffix("a") == None)
  let unicode_view = "πŸ˜€helloπŸ˜ƒ"[:]
  assert_true(unicode_view.strip_suffix("πŸ˜ƒ") == Some("πŸ˜€hello"))
  assert_true(unicode_view.strip_suffix("πŸ˜€") == None)
}

///|
test "View::to_array" {
  let view = "Hello🀣"[:]
  let chars = view.to_array()
  assert_true(chars == ['H', 'e', 'l', 'l', 'o', '🀣'])
  let empty_view = ""[:]
  let empty_chars = empty_view.to_array()
  assert_true(empty_chars == [])
  let sub_view = "Hello World"[6:11] // "World"
  let sub_chars = sub_view.to_array()
  assert_true(sub_chars == ['W', 'o', 'r', 'l', 'd'])
}

///|
/// Returns true if this string view contains the given UTF-16 code unit.
///
/// This searches raw UTF-16 code units and does not combine surrogate pairs.
/// Use `contains_char` when searching for a Unicode character.
pub fn StringView::contains_code_unit(self : StringView, code : UInt16) -> Bool {
  string_contains_code_unit(self.str(), self.start(), self.end(), code)
}

///|
// The caller must ensure `0 <= start <= end <= str.length()`.
#inline
fn string_contains_code_unit_scalar(
  str : String,
  start : Int,
  end : Int,
  code : UInt16,
) -> Bool {
  for i in start.. Bool {
  string_contains_code_unit_scalar(str, start, end, code)
}

///|
// Scan eight UTF-16 code units at a time on linear-memory backends, then scan
// the remaining tail one code unit at a time.
#cfg(any(target="native", target="wasm"))
fn string_contains_code_unit(
  str : String,
  start : Int,
  end : Int,
  code : UInt16,
) -> Bool {
  guard start + 8 <= end else {
    return string_contains_code_unit_scalar(str, start, end, code)
  }
  let needle = i16x8_splat(code)
  let tail_start = for pos = start; pos + 8 <= end; {
    if v128_any_true(i16x8_eq(v128_load_i16x8(str, pos), needle)) {
      return true
    }
    continue pos + 8
  } nobreak {
    pos
  }
  string_contains_code_unit_scalar(str, tail_start, end, code)
}

///|
/// Returns true if this string contains the given substring.
pub fn StringView::contains(self : StringView, str : StringView) -> Bool {
  match str.length() {
    0 => true
    1 => self.contains_code_unit(str.unsafe_get(0))
    _ => self.find(str) is Some(_)
  }
}

///|
/// Returns true if this string contains the given substring.
pub fn String::contains(self : String, str : StringView) -> Bool {
  self[:].contains(str)
}

///|
/// Returns true if this string contains the given UTF-16 code unit.
///
/// This searches raw UTF-16 code units and does not combine surrogate pairs.
/// Use `contains_char` when searching for a Unicode character.
pub fn String::contains_code_unit(self : String, code : UInt16) -> Bool {
  string_contains_code_unit(self, 0, self.length(), code)
}

///|
const ASCII_CHAR_SET_LIMIT : UInt = 128U

///|
const ASCII_CHAR_SET_WORD_MASK : UInt = 31U

///|
const ASCII_CHAR_SET_WORD_SHIFT = 5

///|
// At most this many set members are broadcast to vectors by the SIMD scan;
// larger sets use the scalar bitmap scan.
#cfg(any(target="native", target="wasm"))
const ASCII_CHAR_SET_SIMD_MAX_CHARS = 8

///|
#inline
fn build_ascii_char_set(chars : StringView) -> (UInt, UInt, UInt, UInt)? {
  let mut bits0 = 0U
  let mut bits1 = 0U
  let mut bits2 = 0U
  let mut bits3 = 0U
  for c in chars {
    let code = c.to_uint()
    guard code < ASCII_CHAR_SET_LIMIT else { return None }
    let bit = 1U << (code & ASCII_CHAR_SET_WORD_MASK).reinterpret_as_int()
    match code >> ASCII_CHAR_SET_WORD_SHIFT {
      0 => bits0 = bits0 | bit
      1 => bits1 = bits1 | bit
      2 => bits2 = bits2 | bit
      _ => bits3 = bits3 | bit
    }
  }
  Some((bits0, bits1, bits2, bits3))
}

///|
/// Tests membership in a 128-bit ASCII character set represented by four
/// scalar words, so callers do not need a temporary heap allocation. Code
/// units outside the ASCII range are never members.
///
/// An ASCII code unit is never half of a surrogate pair, so for ASCII-only
/// sets scanning raw code units is equivalent to scanning characters.
#inline
fn ascii_char_set_contains(
  bits0 : UInt,
  bits1 : UInt,
  bits2 : UInt,
  bits3 : UInt,
  code : UInt,
) -> Bool {
  guard code < ASCII_CHAR_SET_LIMIT else { return false }
  let bit = 1U << (code & ASCII_CHAR_SET_WORD_MASK).reinterpret_as_int()
  match code >> ASCII_CHAR_SET_WORD_SHIFT {
    0 => (bits0 & bit) != 0U
    1 => (bits1 & bit) != 0U
    2 => (bits2 & bit) != 0U
    _ => (bits3 & bit) != 0U
  }
}

///|
// The caller must ensure `0 <= start <= end <= str.length()`.
#cfg(not(target="js"))
#inline
fn string_contains_any_ascii_scalar(
  str : String,
  start : Int,
  end : Int,
  bits0 : UInt,
  bits1 : UInt,
  bits2 : UInt,
  bits3 : UInt,
) -> Bool {
  for i in start.. Int {
  for pos = start {
    if pos < end &&
      ascii_char_set_contains(
        bits0,
        bits1,
        bits2,
        bits3,
        str.unsafe_get(pos).to_uint(),
      ) {
      continue pos + 1
    } else {
      break pos
    }
  }
}

///|
// Returns the position just past the last code unit in `start.. Int {
  for pos = end {
    if pos > start &&
      ascii_char_set_contains(
        bits0,
        bits1,
        bits2,
        bits3,
        str.unsafe_get(pos - 1).to_uint(),
      ) {
      continue pos - 1
    } else {
      break pos
    }
  }
}

///|
// Broadcasts the set member at `index` (repeating the first member for unused
// slots, so the compare tree stays branchless) for the SIMD scan.
#cfg(any(target="native", target="wasm"))
#inline
fn ascii_char_set_splat(chars : StringView, count : Int, index : Int) -> V128 {
  i16x8_splat(chars.unsafe_get(if index < count { index } else { 0 }))
}

///|
// Per-lane mask of which of the eight code units in `block` are members of
// the set broadcast across `s0..s7`.
#cfg(any(target="native", target="wasm"))
#inline
fn ascii_char_set_block_mask(
  block : V128,
  s0 : V128,
  s1 : V128,
  s2 : V128,
  s3 : V128,
  s4 : V128,
  s5 : V128,
  s6 : V128,
  s7 : V128,
) -> V128 {
  v128_or(
    v128_or(
      v128_or(i16x8_eq(block, s0), i16x8_eq(block, s1)),
      v128_or(i16x8_eq(block, s2), i16x8_eq(block, s3)),
    ),
    v128_or(
      v128_or(i16x8_eq(block, s4), i16x8_eq(block, s5)),
      v128_or(i16x8_eq(block, s6), i16x8_eq(block, s7)),
    ),
  )
}

///|
// On the JavaScript backend the character iterator compiles to a faster loop
// than indexed code-unit reads, and for an ASCII set the two scans agree.
#cfg(target="js")
fn string_contains_any_ascii(
  str : String,
  start : Int,
  end : Int,
  _chars : StringView,
  bits0 : UInt,
  bits1 : UInt,
  bits2 : UInt,
  bits3 : UInt,
) -> Bool {
  StringView::make_view(str, start, end).contains_any_ascii_chars(
    bits0, bits1, bits2, bits3,
  )
}

///|
#cfg(target="js")
fn StringView::contains_any_ascii_chars(
  self : StringView,
  bits0 : UInt,
  bits1 : UInt,
  bits2 : UInt,
  bits3 : UInt,
) -> Bool {
  for c in self {
    if ascii_char_set_contains(bits0, bits1, bits2, bits3, c.to_uint()) {
      return true
    }
  }
  false
}

///|
#cfg(not(any(target="native", target="wasm", target="js")))
fn string_contains_any_ascii(
  str : String,
  start : Int,
  end : Int,
  _chars : StringView,
  bits0 : UInt,
  bits1 : UInt,
  bits2 : UInt,
  bits3 : UInt,
) -> Bool {
  string_contains_any_ascii_scalar(str, start, end, bits0, bits1, bits2, bits3)
}

///|
// Scan eight UTF-16 code units at a time on linear-memory backends, comparing
// each block against every set member at once, then scan the remaining tail
// one code unit at a time.
#cfg(any(target="native", target="wasm"))
fn string_contains_any_ascii(
  str : String,
  start : Int,
  end : Int,
  chars : StringView,
  bits0 : UInt,
  bits1 : UInt,
  bits2 : UInt,
  bits3 : UInt,
) -> Bool {
  let count = chars.length()
  guard count >= 1 && count <= ASCII_CHAR_SET_SIMD_MAX_CHARS && start + 8 <= end else {
    return string_contains_any_ascii_scalar(
      str, start, end, bits0, bits1, bits2, bits3,
    )
  }
  let s0 = ascii_char_set_splat(chars, count, 0)
  let s1 = ascii_char_set_splat(chars, count, 1)
  let s2 = ascii_char_set_splat(chars, count, 2)
  let s3 = ascii_char_set_splat(chars, count, 3)
  let s4 = ascii_char_set_splat(chars, count, 4)
  let s5 = ascii_char_set_splat(chars, count, 5)
  let s6 = ascii_char_set_splat(chars, count, 6)
  let s7 = ascii_char_set_splat(chars, count, 7)
  let tail_start = for pos = start; pos + 8 <= end; {
    let block = v128_load_i16x8(str, pos)
    if v128_any_true(
        ascii_char_set_block_mask(block, s0, s1, s2, s3, s4, s5, s6, s7),
      ) {
      return true
    }
    continue pos + 8
  } nobreak {
    pos
  }
  string_contains_any_ascii_scalar(
    str, tail_start, end, bits0, bits1, bits2, bits3,
  )
}

///|
#cfg(not(any(target="native", target="wasm")))
fn string_trim_start_ascii(
  str : String,
  start : Int,
  end : Int,
  _chars : StringView,
  bits0 : UInt,
  bits1 : UInt,
  bits2 : UInt,
  bits3 : UInt,
) -> Int {
  string_trim_start_ascii_scalar(str, start, end, bits0, bits1, bits2, bits3)
}

///|
// Skip eight fully-trimmable code units at a time on linear-memory backends;
// the first block containing a non-member (and the sub-8 tail) is finished by
// the scalar scan.
#cfg(any(target="native", target="wasm"))
fn string_trim_start_ascii(
  str : String,
  start : Int,
  end : Int,
  chars : StringView,
  bits0 : UInt,
  bits1 : UInt,
  bits2 : UInt,
  bits3 : UInt,
) -> Int {
  let count = chars.length()
  guard count >= 1 && count <= ASCII_CHAR_SET_SIMD_MAX_CHARS && start + 8 <= end else {
    return string_trim_start_ascii_scalar(
      str, start, end, bits0, bits1, bits2, bits3,
    )
  }
  let s0 = ascii_char_set_splat(chars, count, 0)
  let s1 = ascii_char_set_splat(chars, count, 1)
  let s2 = ascii_char_set_splat(chars, count, 2)
  let s3 = ascii_char_set_splat(chars, count, 3)
  let s4 = ascii_char_set_splat(chars, count, 4)
  let s5 = ascii_char_set_splat(chars, count, 5)
  let s6 = ascii_char_set_splat(chars, count, 6)
  let s7 = ascii_char_set_splat(chars, count, 7)
  let boundary = for pos = start; pos + 8 <= end; {
    let block = v128_load_i16x8(str, pos)
    let mask = ascii_char_set_block_mask(block, s0, s1, s2, s3, s4, s5, s6, s7)
    if i16x8_bitmask(mask) != 0xFF {
      break pos
    }
    continue pos + 8
  } nobreak {
    pos
  }
  string_trim_start_ascii_scalar(str, boundary, end, bits0, bits1, bits2, bits3)
}

///|
#cfg(not(any(target="native", target="wasm")))
fn string_trim_end_ascii(
  str : String,
  start : Int,
  end : Int,
  _chars : StringView,
  bits0 : UInt,
  bits1 : UInt,
  bits2 : UInt,
  bits3 : UInt,
) -> Int {
  string_trim_end_ascii_scalar(str, start, end, bits0, bits1, bits2, bits3)
}

///|
// Mirror of `string_trim_start_ascii`, scanning blocks backward from the end.
#cfg(any(target="native", target="wasm"))
fn string_trim_end_ascii(
  str : String,
  start : Int,
  end : Int,
  chars : StringView,
  bits0 : UInt,
  bits1 : UInt,
  bits2 : UInt,
  bits3 : UInt,
) -> Int {
  let count = chars.length()
  guard count >= 1 && count <= ASCII_CHAR_SET_SIMD_MAX_CHARS && start + 8 <= end else {
    return string_trim_end_ascii_scalar(
      str, start, end, bits0, bits1, bits2, bits3,
    )
  }
  let s0 = ascii_char_set_splat(chars, count, 0)
  let s1 = ascii_char_set_splat(chars, count, 1)
  let s2 = ascii_char_set_splat(chars, count, 2)
  let s3 = ascii_char_set_splat(chars, count, 3)
  let s4 = ascii_char_set_splat(chars, count, 4)
  let s5 = ascii_char_set_splat(chars, count, 5)
  let s6 = ascii_char_set_splat(chars, count, 6)
  let s7 = ascii_char_set_splat(chars, count, 7)
  let boundary = for pos = end; pos - 8 >= start; {
    let block = v128_load_i16x8(str, pos - 8)
    let mask = ascii_char_set_block_mask(block, s0, s1, s2, s3, s4, s5, s6, s7)
    if i16x8_bitmask(mask) != 0xFF {
      break pos
    }
    continue pos - 8
  } nobreak {
    pos
  }
  string_trim_end_ascii_scalar(str, start, boundary, bits0, bits1, bits2, bits3)
}

///|
fn StringView::trim_start_with_chars(
  self : StringView,
  chars : StringView,
) -> StringView {
  for x = self {
    match x {
      [] as v => break v
      [c, .. rest] as v =>
        if chars.contains_char(c) {
          continue rest
        } else {
          break v
        }
    }
  }
}

///|
fn StringView::trim_end_with_chars(
  self : StringView,
  chars : StringView,
) -> StringView {
  for x = self {
    match x {
      [] as v => break v
      [.. rest, c] as v =>
        if chars.contains_char(c) {
          continue rest
        } else {
          break v
        }
    }
  }
}

///|
/// Returns true if this string contains any character from the given set.
pub fn StringView::contains_any(self : StringView, chars~ : StringView) -> Bool {
  match chars {
    [] => false
    [c] => self.contains_char(c) // specialize for single character
    _ =>
      match build_ascii_char_set(chars) {
        Some((bits0, bits1, bits2, bits3)) =>
          string_contains_any_ascii(
            self.str(),
            self.start(),
            self.end(),
            chars,
            bits0,
            bits1,
            bits2,
            bits3,
          )
        None =>
          for c in self {
            if chars.contains_char(c) {
              break true
            }
          } nobreak {
            false
          }
      }
  }
}

///|
/// Returns true if this string contains any character from the given set.
pub fn String::contains_any(self : String, chars~ : StringView) -> Bool {
  self[:].contains_any(chars~)
}

///|
test "contains" {
  inspect("hello".contains("o"), content="true")
  inspect("hello".contains("l"), content="true")
  inspect("hello".contains("hello"), content="true")
  inspect("hello".contains("h"), content="true")
  inspect("hello".contains(""), content="true")
  inspect("hello".contains("world"), content="false")
  inspect("".contains(""), content="true")
  inspect("".contains("a"), content="false")
  inspect("hello hello".contains("hello"), content="true")
  inspect("aaa".contains("aa"), content="true")
  inspect("πŸ˜€πŸ˜€".contains("πŸ˜€"), content="true")
  let leading_surrogate = String::from_array([(0xD800).unsafe_to_char()])
  inspect(leading_surrogate.contains(leading_surrogate), content="true")
}

///|
test "contains_code_unit" {
  inspect("hello".contains_code_unit(('h' : UInt16)), content="true")
  inspect("hello".contains_code_unit(('x' : UInt16)), content="false")
  inspect("xhello"[1:].contains_code_unit(('h' : UInt16)), content="true")
  inspect("xhello"[1:].contains_code_unit(('x' : UInt16)), content="false")
  inspect("πŸ˜€".contains_code_unit(0xD83D), content="true")
  inspect("πŸ˜€".contains_code_unit(0xDE00), content="true")
  let leading_surrogate = String::from_array([(0xD800).unsafe_to_char()])
  inspect(leading_surrogate.contains_code_unit(0xD800), content="true")
}

///|
test "contains_code_unit SIMD blocks and view bounds" {
  let str = "01234567abcdefghZ"
  inspect(str.contains_code_unit(('0' : UInt16)), content="true")
  inspect(str.contains_code_unit(('7' : UInt16)), content="true")
  inspect(str.contains_code_unit(('a' : UInt16)), content="true")
  inspect(str.contains_code_unit(('h' : UInt16)), content="true")
  inspect(str.contains_code_unit(('Z' : UInt16)), content="true")
  inspect(str.contains_code_unit(('x' : UInt16)), content="false")
  let view = ("x" + str + "y")[1:18]
  inspect(view.contains_code_unit(('x' : UInt16)), content="false")
  inspect(view.contains_code_unit(('a' : UInt16)), content="true")
  inspect(view.contains_code_unit(('Z' : UInt16)), content="true")
  inspect(view.contains_code_unit(('y' : UInt16)), content="false")
  let wide = "01234567abcdπŸ˜€efgh"
  inspect(wide.contains_code_unit(0xD83D), content="true")
  inspect(wide.contains_code_unit(0xDE00), content="true")
}

///|
test "contains_any" {
  inspect("hello".contains_any(chars="h"), content="true")
  inspect("hello".contains_any(chars="xyz"), content="false")
  inspect("hello".contains_any(chars=""), content="false")
  inspect("".contains_any(chars="abc"), content="false")
  inspect("πŸ˜€πŸ˜ƒ".contains_any(chars="πŸ˜„πŸ˜€"), content="true")
  inspect("hello"[:].contains_any(chars="eo"), content="true")
}

///|
test "contains_any and trim ASCII character sets" {
  assert_true("πŸ˜€a".contains_any(chars="az"))
  assert_false("πŸ˜€".contains_any(chars="az"))
  assert_true("πŸ˜€".contains_any(chars="aπŸ˜€"))
  let view = "x  hello \ty"[1:10]
  assert_true(view.trim(chars=" \t") == "hello")
}

///|
test "build ASCII character set" {
  match build_ascii_char_set("a z") {
    Some((bits0, bits1, bits2, bits3)) => {
      assert_true(
        ascii_char_set_contains(bits0, bits1, bits2, bits3, 'a'.to_uint()),
      )
      assert_true(
        ascii_char_set_contains(bits0, bits1, bits2, bits3, 'z'.to_uint()),
      )
      assert_true(
        ascii_char_set_contains(bits0, bits1, bits2, bits3, ' '.to_uint()),
      )
      assert_false(
        ascii_char_set_contains(bits0, bits1, bits2, bits3, 'b'.to_uint()),
      )
    }
    None => assert_false(true)
  }
  assert_true(build_ascii_char_set("aπŸ˜€") is None)
}

///|
/// Returns true if this string contains the given character.
pub fn StringView::contains_char(self : StringView, c : Char) -> Bool {
  let len = self.length()
  // Check empty
  guard len > 0 else { return false }
  let c = c.to_int()
  if c >= 0 && c <= 0xFFFF {
    // Search BMP
    return self.contains_code_unit(c.to_uint16())
  } else if c < 0 {
    return false
  } else {
    // Check insufficient
    guard len >= 2 else { return false }
    // Calc surrogate pair
    let adj = c - 0x10000
    let high = 0xD800 + (adj >> 10)
    guard high <= 0xFFFF else { return false }
    let high = high.to_uint16()
    let low = (0xDC00 + (adj & 0x3FF)).to_uint16()
    // Search surrogate pair
    for i = 0; i < len - 1; {
      if self.unsafe_get(i) == high {
        if self.unsafe_get(i + 1) == low {
          return true
        }
        continue i + 2
      }
      continue i + 1
    }
  }
  false
}

///|
/// Returns true if this string contains the given character.
pub fn String::contains_char(self : String, c : Char) -> Bool {
  self[:].contains_char(c)
}

///|
test "contains_char" {
  inspect("hello".contains_char('h'), content="true")
  inspect("hello".contains_char('e'), content="true")
  inspect("hello".contains_char('l'), content="true")
  inspect("hello".contains_char('o'), content="true")
  inspect("hello".contains_char('x'), content="false")
  inspect("".contains_char('a'), content="false")
  inspect("hello world".contains_char(' '), content="true")
  inspect("hello world".contains_char('w'), content="true")
  inspect("πŸ˜€πŸ˜€".contains_char('πŸ˜€'), content="true")
  inspect("πŸ˜€πŸ˜€".contains_char('πŸ˜ƒ'), content="false")
  inspect("hello".contains_char('h'), content="true")
  let leading_surrogate = String::from_array([(0xD800).unsafe_to_char()])
  inspect(
    leading_surrogate.contains_char((0xD800).unsafe_to_char()),
    content="true",
  )
  let max_code_unit = String::from_array([(0xFFFF).unsafe_to_char()])
  inspect(max_code_unit.contains_char((-1).unsafe_to_char()), content="false")
}

///|
/// Returns the view of the string without the leading characters that are in
/// the given string.
#label_migration(chars, alias=char_set)
pub fn StringView::trim_start(
  self : StringView,
  chars? : StringView = "\t\n\r ",
) -> StringView {
  match build_ascii_char_set(chars) {
    Some((bits0, bits1, bits2, bits3)) => {
      let start = string_trim_start_ascii(
        self.str(),
        self.start(),
        self.end(),
        chars,
        bits0,
        bits1,
        bits2,
        bits3,
      )
      StringView::make_view(self.str(), start, self.end())
    }
    None => self.trim_start_with_chars(chars)
  }
}

///|
/// Returns the view of the string without the leading characters that are in
/// the given string.
#label_migration(chars, alias=char_set)
pub fn String::trim_start(
  self : String,
  chars? : StringView = "\t\n\r ",
) -> StringView {
  self[:].trim_start(chars~)
}

///|
test "trim_start" {
  inspect("hello".trim_start(chars="h"), content="ello")
  inspect("hello".trim_start(chars="he"), content="llo")
  inspect("hello".trim_start(chars="eh"), content="llo")
  inspect("hello".trim_start(chars="x"), content="hello")
  inspect("hello".trim_start(chars=""), content="hello")
  inspect("".trim_start(chars="a"), content="")
  inspect("   hello".trim_start(chars=" "), content="hello")
  inspect("hello world".trim_start(chars="helo"), content=" world")
  inspect("πŸ˜€πŸ˜€hello".trim_start(chars="πŸ˜€"), content="hello")
  inspect("πŸ˜€πŸ˜ƒhello".trim_start(chars="πŸ˜€πŸ˜ƒ"), content="hello")
  inspect("aaaabc".trim_start(chars="a"), content="bc")
  inspect("aaaa".trim_start(chars="a"), content="")
}

///|
/// Returns the view of the string without the trailing characters that are in
/// the given string.
#label_migration(chars, alias=char_set)
pub fn StringView::trim_end(
  self : StringView,
  chars? : StringView = "\t\n\r ",
) -> StringView {
  match build_ascii_char_set(chars) {
    Some((bits0, bits1, bits2, bits3)) => {
      let end = string_trim_end_ascii(
        self.str(),
        self.start(),
        self.end(),
        chars,
        bits0,
        bits1,
        bits2,
        bits3,
      )
      StringView::make_view(self.str(), self.start(), end)
    }
    None => self.trim_end_with_chars(chars)
  }
}

///|
/// Returns the view of the string without the trailing characters that are in
/// the given string.
// TODO(upstream): label_migration warning does not apply to the current package
// TODO: make chars optional with default value of whitespace characters
#label_migration(chars, alias=char_set)
pub fn String::trim_end(
  self : String,
  chars? : StringView = "\t\n\r ",
) -> StringView {
  self[:].trim_end(chars~)
}

///|
test "trim_end" {
  inspect("hello".trim_end(chars="o"), content="hell")
  inspect("hello".trim_end(chars="lo"), content="he")
  inspect("hello".trim_end(chars="x"), content="hello")
  inspect("hello".trim_end(chars=""), content="hello")
  inspect("".trim_end(chars="a"), content="")
  inspect("hello   ".trim_end(chars=" "), content="hello")
  inspect("hello world".trim_end(chars="dlrow "), content="he")
  inspect("helloπŸ˜€πŸ˜€".trim_end(chars="πŸ˜€"), content="hello")
  inspect("helloπŸ˜€πŸ˜ƒ".trim_end(chars="πŸ˜€πŸ˜ƒ"), content="hello")
  inspect("abcccc".trim_end(chars="c"), content="ab")
  inspect("cccc".trim_end(chars="c"), content="")
}

///|
/// Returns the view of the string without the leading and trailing characters
/// that are in the given string.
#label_migration(chars, alias=char_set)
pub fn StringView::trim(
  self : StringView,
  chars? : StringView = "\t\n\r ",
) -> StringView {
  match build_ascii_char_set(chars) {
    Some((bits0, bits1, bits2, bits3)) => {
      let start = string_trim_start_ascii(
        self.str(),
        self.start(),
        self.end(),
        chars,
        bits0,
        bits1,
        bits2,
        bits3,
      )
      let end = string_trim_end_ascii(
        self.str(),
        start,
        self.end(),
        chars,
        bits0,
        bits1,
        bits2,
        bits3,
      )
      StringView::make_view(self.str(), start, end)
    }
    None => self.trim_start_with_chars(chars).trim_end_with_chars(chars)
  }
}

///|
/// Returns the view of the string without the leading and trailing characters
/// that are in the given string.
#label_migration(chars, alias=char_set)
pub fn String::trim(
  self : String,
  chars? : StringView = "\t\n\r ",
) -> StringView {
  self[:].trim(chars~)
}

///|
test "trim" {
  inspect("hello".trim(chars="h"), content="ello")
  inspect("hello".trim(chars="o"), content="hell")
  inspect("hello".trim(chars="ho"), content="ell")
  inspect("hello".trim(chars="oh"), content="ell")
  inspect("hello".trim(chars="x"), content="hello")
  inspect("hello".trim(chars=""), content="hello")
  inspect("".trim(chars="a"), content="")
  inspect("   hello   ".trim(chars=" "), content="hello")
  inspect("hello world".trim(chars="hd"), content="ello worl")
  inspect("πŸ˜€helloπŸ˜€".trim(chars="πŸ˜€"), content="hello")
  inspect("πŸ˜€πŸ˜ƒhelloπŸ˜€πŸ˜ƒ".trim(chars="πŸ˜€πŸ˜ƒ"), content="hello")
  inspect("aaaabcaaa".trim(chars="a"), content="bc")
  inspect("aaaa".trim(chars="a"), content="")
  inspect("  hello world  ".trim(chars=" "), content="hello world")
  inspect("abcabc".trim(chars="abc"), content="")
}

///|
/// Returns the view of the string without the leading and trailing spaces.
#deprecated("Use `trim` with default whitespace characters instead")
pub fn StringView::trim_space(self : StringView) -> StringView {
  self.trim()
}

///|
/// Returns the view of the string without the leading and trailing spaces.
#deprecated("Use `trim` with default whitespace characters instead")
pub fn String::trim_space(self : String) -> StringView {
  self.trim()
}

///|
test "trim whitespace for string" {
  inspect("hello".trim(), content="hello")
  inspect("  hello  ".trim(), content="hello")
  inspect("hello  ".trim(), content="hello")
  inspect("  hello".trim(), content="hello")
  inspect("\t\nhello\r\n".trim(), content="hello")
  inspect("  hello world  ".trim(), content="hello world")
  inspect("  ".trim(), content="")
  inspect("\n\r\t".trim(), content="")
  inspect("".trim(), content="")
  inspect("  hello\nworld\t".trim(), content="hello\nworld")
}

///|
/// Returns true if this string is empty.
pub fn StringView::is_empty(self : StringView) -> Bool {
  self.length() == 0
}

///|
/// Returns true if this string is empty.
pub fn String::is_empty(self : String) -> Bool {
  self == ""
}

///|
test "is_empty" {
  inspect("".is_empty(), content="true")
  inspect("hello".is_empty(), content="false")
  inspect(" ".is_empty(), content="false")
  inspect("\n".is_empty(), content="false")
  inspect("\t".is_empty(), content="false")
  inspect("   ".is_empty(), content="false")

  // Test with string views
  let s = "hello"
  let empty_view = s[0:0]
  let non_empty_view = s[0:3]
  inspect(empty_view.is_empty(), content="true")
  inspect(non_empty_view.is_empty(), content="false")
}

///|
/// Returns true if this string is blank.
pub fn StringView::is_blank(self : StringView) -> Bool {
  self.trim().is_empty()
}

///|
/// Returns true if this string is blank.
pub fn String::is_blank(self : String) -> Bool {
  self[:].is_blank()
}

///|
test "is_blank" {
  inspect("".is_blank(), content="true")
  inspect("hello".is_blank(), content="false")
  inspect(" ".is_blank(), content="true")
  inspect("\n".is_blank(), content="true")
  inspect("\t".is_blank(), content="true")
  inspect("   ".is_blank(), content="true")
  inspect(" \n\t\r ".is_blank(), content="true")
  inspect("hello world".is_blank(), content="false")
  inspect("  hello  ".is_blank(), content="false")

  // Test with string views
  let s = "   hello  "
  let blank_view = s[0:3] // "   "
  let non_blank_view = s[3:8] // "hello"
  inspect(blank_view.is_blank(), content="true")
  inspect(non_blank_view.is_blank(), content="false")
}

///|
/// Returns a new string with `padding_char`s prefixed to `self` if
/// `self.length() < total_width`. The threshold and the pad count are
/// measured in UTF-16 code units: `total_width - self.length()` copies of
/// `padding_char` are prefixed. Characters outside the BMP count as two
/// code units, so with such characters in `self` the result has fewer than
/// `total_width` characters, and with a non-BMP `padding_char` the result's
/// UTF-16 length exceeds `total_width`.
pub fn StringView::pad_start(
  self : StringView,
  total_width : Int,
  padding_char : Char,
) -> String {
  let len = self.length()
  guard len < total_width else { return self.to_owned() }
  let padding = String::make(total_width - len, padding_char)
  [..padding, ..self]
}

///|
/// Returns a new string with `padding_char`s prefixed to `self` if
/// `self.length() < total_width`. The threshold and the pad count are
/// measured in UTF-16 code units: `total_width - self.length()` copies of
/// `padding_char` are prefixed. Characters outside the BMP count as two
/// code units, so with such characters in `self` the result has fewer than
/// `total_width` characters, and with a non-BMP `padding_char` the result's
/// UTF-16 length exceeds `total_width`.
pub fn String::pad_start(
  self : String,
  total_width : Int,
  padding_char : Char,
) -> String {
  let len = self.length()
  guard len < total_width else { return self }
  let padding = String::make(total_width - len, padding_char)
  [..padding, ..self]
}

///|
test "pad_start" {
  // Test with regular strings
  inspect("2".pad_start(3, '0'), content="002")
  inspect("abc".pad_start(5, 'x'), content="xxabc")
  inspect("hello".pad_start(4, ' '), content="hello") // No padding needed
  inspect("".pad_start(3, '-'), content="---")

  // Test with different padding characters
  inspect("test".pad_start(8, '*'), content="****test")
  inspect("123".pad_start(6, '0'), content="000123")

  // Test with string views
  let s = "hello"
  let view = s[2:5] // "llo"
  inspect(view.pad_start(5, 'x'), content="xxllo")

  // Test with Unicode characters
  inspect("🌟".pad_start(3, '✨'), content="✨🌟")

  // Edge cases
  inspect("abc".pad_start(0, 'x'), content="abc") // width less than string length
  inspect("abc".pad_start(3, 'x'), content="abc") // width equal to string length
}

///|
/// Returns a new string with `padding_char`s appended to `self` if
/// `self.length() < total_width`. The threshold and the pad count are
/// measured in UTF-16 code units: `total_width - self.length()` copies of
/// `padding_char` are appended. Characters outside the BMP count as two
/// code units, so with such characters in `self` the result has fewer than
/// `total_width` characters, and with a non-BMP `padding_char` the result's
/// UTF-16 length exceeds `total_width`.
pub fn StringView::pad_end(
  self : StringView,
  total_width : Int,
  padding_char : Char,
) -> String {
  let len = self.length()
  guard len < total_width else { return self.to_owned() }
  let padding = String::make(total_width - len, padding_char)
  [..self, ..padding]
}

///|
/// Returns a new string with `padding_char`s appended to `self` if
/// `self.length() < total_width`. The threshold and the pad count are
/// measured in UTF-16 code units: `total_width - self.length()` copies of
/// `padding_char` are appended. Characters outside the BMP count as two
/// code units, so with such characters in `self` the result has fewer than
/// `total_width` characters, and with a non-BMP `padding_char` the result's
/// UTF-16 length exceeds `total_width`.
pub fn String::pad_end(
  self : String,
  total_width : Int,
  padding_char : Char,
) -> String {
  let len = self.length()
  guard len < total_width else { return self }
  let padding = String::make(total_width - len, padding_char)
  [..self, ..padding]
}

///|
test "pad_end" {
  // Test with regular strings
  inspect("2".pad_end(3, '0'), content="200")
  inspect("abc".pad_end(5, 'x'), content="abcxx")
  inspect("hello".pad_end(4, ' '), content="hello") // No padding needed
  inspect("".pad_end(3, '-'), content="---")

  // Test with different padding characters
  inspect("test".pad_end(8, '*'), content="test****")
  inspect("123".pad_end(6, '0'), content="123000")

  // Test with string views
  let s = "hello"
  let view = s[2:5] // "llo"
  inspect(view.pad_end(5, 'x'), content="lloxx")

  // Test with Unicode characters
  inspect("🌟".pad_end(3, '✨'), content="🌟✨")

  // Edge cases
  inspect("abc".pad_end(0, 'x'), content="abc") // width less than string length
  inspect("abc".pad_end(3, 'x'), content="abc") // width equal to string length
}

///|
/// Returns a new string with `self` repeated `n` times.
///
/// Aborts if `n` is negative. When `n` is `0`, returns the empty string.
pub fn StringView::repeat(self : StringView, n : Int) -> StringView {
  match n {
    _..<0 => abort("negative repeat count")
    0 => ""
    1 => self
    _ => {
      let len = self.length()
      let total = len * n
      guard len == 0 || total / n == len else {
        abort("repeat result too large")
      }
      let buf = StringBuilder(size_hint=total)
      let str = self.to_owned()
      for _ in 0.. String {
  match n {
    _..<0 => abort("negative repeat count")
    0 => ""
    1 => self
    _ => {
      let len = self.length()
      let total = len * n
      guard len == 0 || total / n == len else {
        abort("repeat result too large")
      }
      let buf = StringBuilder(size_hint=total)
      let str = self.to_string()
      for _ in 0.. String {
  let buf = StringBuilder(size_hint=self.length())
  for c in self.rev_iter() {
    buf.write_char(c)
  }
  buf.to_string()
}

///|
/// Returns a new string with the characters in reverse order. It respects
/// Unicode characters and surrogate pairs but not grapheme clusters.
pub fn String::rev(self : String) -> String {
  self[:].rev()
}

///|
test "rev" {
  inspect("hello".rev(), content="olleh")
  inspect("".rev(), content="")
  inspect("abc".rev(), content="cba")
  inspect("πŸ˜€πŸ˜ƒ".rev(), content="πŸ˜ƒπŸ˜€")
}

///|
/// Splits the string into all substrings separated by the given separator.
/// 
/// If the string does not contain the separator and the separator is not empty,
/// the returned iterator will contain only one element, which is the original
/// string.
/// 
/// If the separator is empty, the returned iterator will contain all the
/// characters in the string as single elements.
pub fn StringView::split(
  self : StringView,
  sep : StringView,
) -> Iter[StringView] {
  let sep_len = sep.length()
  if sep_len == 0 {
    return self.iter().map(c => c.to_string().view())
  }
  let mut remaining = Some(self)
  Iter::new(() => {
    guard remaining is Some(view) else { None }
    guard view.find(sep) is Some(end) else {
      remaining = None
      Some(view)
    }
    remaining = Some(view.view(start_offset=end + sep_len))
    Some(view.view(end_offset=end))
  })
}

///|
/// Splits the string into all substrings separated by the given separator.
/// 
/// If the string does not contain the separator and the separator is not empty,
/// the returned iterator will contain only one element, which is the original
/// string.
/// 
/// If the separator is empty, the returned iterator will contain all the
/// characters in the string as single elements.
pub fn String::split(self : String, sep : StringView) -> Iter[StringView] {
  self[:].split(sep)
}

///|
/// Splits the string into a pair at the first occurrence of the separator.
/// 
/// Returns None if the separator is not found.
/// 
/// If the separator is empty, it splits at the start of the string, returning
/// an empty prefix and the full string as the suffix.
pub fn StringView::split_once(
  self : StringView,
  needle : StringView,
) -> (StringView, StringView)? {
  match self.find(needle) {
    Some(index) =>
      Some(
        (
          self.view(end_offset=index),
          self.view(start_offset=index + needle.length()),
        ),
      )
    None => None
  }
}

///|
/// Splits the string into a pair at the first occurrence of the separator.
/// 
/// Returns None if the separator is not found.
/// 
/// If the separator is empty, it splits at the start of the string, returning
/// an empty prefix and the full string as the suffix.
pub fn String::split_once(
  self : String,
  needle : StringView,
) -> (StringView, StringView)? {
  self[:].split_once(needle)
}

///|
/// Returns the substring before the first occurrence of the separator.
/// 
/// Returns None if the separator is not found.
/// 
/// If the separator is empty, it returns an empty string.
pub fn StringView::before(
  self : StringView,
  needle : StringView,
) -> StringView? {
  match self.find(needle) {
    Some(index) => Some(self.view(end_offset=index))
    None => None
  }
}

///|
/// Returns the substring before the first occurrence of the separator.
/// 
/// Returns None if the separator is not found.
/// 
/// If the separator is empty, it returns an empty string.
pub fn String::before(self : String, needle : StringView) -> StringView? {
  self[:].before(needle)
}

///|
/// Returns the substring after the first occurrence of the separator.
/// 
/// Returns None if the separator is not found.
/// 
/// If the separator is empty, it returns the full string.
pub fn StringView::after(self : StringView, needle : StringView) -> StringView? {
  match self.find(needle) {
    Some(index) => Some(self.view(start_offset=index + needle.length()))
    None => None
  }
}

///|
/// Returns the substring after the first occurrence of the separator.
/// 
/// Returns None if the separator is not found.
/// 
/// If the separator is empty, it returns the full string.
pub fn String::after(self : String, needle : StringView) -> StringView? {
  self[:].after(needle)
}

///|
/// Splits the string into a pair at the last occurrence of the separator.
///
/// Returns None if the separator is not found.
///
/// Example:
///
/// ```mbt check
/// test {
///   assert_true("a::b::c".rev_split_once("::") == Some(("a::b", "c")))
///   assert_true("nope".rev_split_once("::") == None)
/// }
/// ```
pub fn StringView::rev_split_once(
  self : StringView,
  needle : StringView,
) -> (StringView, StringView)? {
  match self.rev_find(needle) {
    Some(index) =>
      Some(
        (
          self.view(end_offset=index),
          self.view(start_offset=index + needle.length()),
        ),
      )
    None => None
  }
}

///|
/// Splits the string into a pair at the last occurrence of the separator.
///
/// Returns None if the separator is not found.
///
/// Example:
///
/// ```mbt check
/// test {
///   assert_true("a::b::c".rev_split_once("::") == Some(("a::b", "c")))
/// }
/// ```
pub fn String::rev_split_once(
  self : String,
  needle : StringView,
) -> (StringView, StringView)? {
  self[:].rev_split_once(needle)
}

///|
/// Returns the substring before the last occurrence of the separator.
///
/// Returns None if the separator is not found.
///
/// Example:
///
/// ```mbt check
/// test {
///   assert_true("a/b/c.txt".rev_before("/") == Some("a/b"))
/// }
/// ```
pub fn StringView::rev_before(
  self : StringView,
  needle : StringView,
) -> StringView? {
  match self.rev_find(needle) {
    Some(index) => Some(self.view(end_offset=index))
    None => None
  }
}

///|
/// Returns the substring before the last occurrence of the separator.
///
/// Returns None if the separator is not found.
///
/// Example:
///
/// ```mbt check
/// test {
///   assert_true("a/b/c.txt".rev_before("/") == Some("a/b"))
/// }
/// ```
pub fn String::rev_before(self : String, needle : StringView) -> StringView? {
  self[:].rev_before(needle)
}

///|
/// Returns the substring after the last occurrence of the separator.
///
/// Returns None if the separator is not found.
///
/// Example:
///
/// ```mbt check
/// test {
///   assert_true("a/b/c.txt".rev_after("/") == Some("c.txt"))
/// }
/// ```
pub fn StringView::rev_after(
  self : StringView,
  needle : StringView,
) -> StringView? {
  match self.rev_find(needle) {
    Some(index) => Some(self.view(start_offset=index + needle.length()))
    None => None
  }
}

///|
/// Returns the substring after the last occurrence of the separator.
///
/// Returns None if the separator is not found.
///
/// Example:
///
/// ```mbt check
/// test {
///   assert_true("a/b/c.txt".rev_after("/") == Some("c.txt"))
/// }
/// ```
pub fn String::rev_after(self : String, needle : StringView) -> StringView? {
  self[:].rev_after(needle)
}

///|
test "split" {
  assert_true(
    "a,b,c".split(",").map(x => x.to_owned()).collect() == ["a", "b", "c"],
  )
  assert_true(
    "a,b,c".split("").map(x => x.to_owned()).collect() ==
    ["a", ",", "b", ",", "c"],
  )
  assert_true(
    "apple::orange::banana".split("::").map(x => x.to_owned()).collect() ==
    ["apple", "orange", "banana"],
  )
  assert_true(
    "abc".split("").map(x => x.to_owned()).collect() == ["a", "b", "c"],
  )
  assert_true("hello".split(",").map(x => x.to_owned()).collect() == ["hello"])
  assert_true(
    ",a,b,c".split(",").map(x => x.to_owned()).collect() == ["", "a", "b", "c"],
  )
  assert_true(
    "a,b,c,".split(",").map(x => x.to_owned()).collect() == ["a", "b", "c", ""],
  )
  assert_true(
    "a,b,c".split("").map(x => x.to_owned()).collect() ==
    ["a", ",", "b", ",", "c"],
  )
  assert_true("".split("").map(x => x.to_owned()).collect() == [])
  assert_true("".split(",").map(x => x.to_owned()).collect() == [""])
  assert_true(
    "πŸ˜€,πŸ˜ƒ,πŸ˜„".split(",").map(x => x.to_owned()).collect() ==
    ["πŸ˜€", "πŸ˜ƒ", "πŸ˜„"],
  )
  assert_true(
    "aπŸ˜€bπŸ˜€c".split("πŸ˜€").map(x => x.to_owned()).collect() ==
    ["a", "b", "c"],
  )
}

///|
test "split_once before after" {
  assert_true("a=b=c".split_once("=") == Some(("a", "b=c")))
  assert_true("a=b=c".before("=") == Some("a"))
  assert_true("a=b=c".after("=") == Some("b=c"))
  assert_true("nope".split_once("=") == None)
  assert_true("nope".before("=") == None)
  assert_true("nope".after("=") == None)
  assert_true("".split_once("") == Some(("", "")))
  assert_true("".before("") == Some(""))
  assert_true("".after("") == Some(""))
  assert_true("hello".split_once("") == Some(("", "hello")))
  assert_true("hello".before("") == Some(""))
  assert_true("hello".after("") == Some("hello"))
  assert_true("😊::πŸš€".split_once("::") == Some(("😊", "πŸš€")))
}

///|
test "rev_split_once rev_before rev_after" {
  assert_true("a=b=c".rev_split_once("=") == Some(("a=b", "c")))
  assert_true("a=b=c".rev_before("=") == Some("a=b"))
  assert_true("a=b=c".rev_after("=") == Some("c"))
  assert_true("nope".rev_split_once("=") == None)
  assert_true("nope".rev_before("=") == None)
  assert_true("nope".rev_after("=") == None)
  assert_true("a/b/c.txt".rev_before("/") == Some("a/b"))
  assert_true("a/b/c.txt".rev_after("/") == Some("c.txt"))
  assert_true("one=two".rev_split_once("=") == Some(("one", "two")))
  assert_true(
    "😊::πŸš€::⭐".rev_split_once("::") == Some(("😊::πŸš€", "⭐")),
  )
}

///|
/// Replaces the first occurrence of `old` with `new` in `self`.
/// 
/// If `old` is empty, it matches the beginning of the string, and `new` is
/// prepended to the string.
pub fn StringView::replace(
  self : StringView,
  old~ : StringView,
  new~ : StringView,
) -> StringView {
  match self.find(old) {
    Some(end) =>
      [
        ..self.view(end_offset=end),
        ..new,
        ..self.view(start_offset=end + old.length()),
      ]
    None => self
  }
}

///|
/// Replaces the first occurrence of `old` with `new` in `self`.
/// 
/// If `old` is empty, it matches the beginning of the string, and `new` is
/// prepended to the string.
pub fn String::replace(
  self : String,
  old~ : StringView,
  new~ : StringView,
) -> String {
  match self.find(old) {
    Some(end) =>
      [
        ..self.view(end_offset=end),
        ..new,
        ..self.view(start_offset=end + old.length()),
      ]
    None => self
  }
}

///|
test "replace" {
  inspect("hello".replace(old="o", new="a"), content="hella")
  inspect("hello".replace(old="l", new="a"), content="healo")
  inspect("hello".replace(old="hello", new="a"), content="a")
  inspect("hello".replace(old="h", new="a"), content="aello")
  inspect("hello".replace(old="", new="a"), content="ahello")
  inspect("hello".replace(old="world", new="a"), content="hello")
  inspect("".replace(old="", new="a"), content="a")
}

///|
/// Replaces all non-overlapping occurrences of `old` with `new` in `self`.
/// 
/// If `old` is empty, it matches at the beginning of the string and after each
/// character in the string, so `new` is inserted at the beginning of the string
/// and after each character.
pub fn StringView::replace_all(
  self : StringView,
  old~ : StringView,
  new~ : StringView,
) -> StringView {
  let len = self.length()
  let buf = StringBuilder(size_hint=len)
  let old_len = old.length()
  let new = new.to_owned()
  // use write_substring to avoid intermediate allocations
  if old_len == 0 {
    buf.write_string(new)
    for c in self {
      buf.write_char(c)
      buf.write_string(new)
    }
    buf.to_string()
  } else {
    let first_end = self.find(old)
    if first_end is Some(end) {
      for view = self, end = end {
        let seg = view.view(end_offset=end)
        buf.write_substring(seg.data(), seg.start_offset(), seg.length())
        buf.write_string(new)
        // check if there is no more characters after the last occurrence of `old`
        guard end + old_len <= len else { break }
        let next_view = view.view(start_offset=end + old_len)
        guard next_view.find(old) is Some(next_end) else {
          buf.write_substring(
            next_view.data(),
            next_view.start_offset(),
            next_view.length(),
          )
          break
        }
        continue next_view, next_end
      }
      buf.to_string()
    } else {
      self
    }
  }
}

///|
/// Replaces all non-overlapping occurrences of `old` with `new` in `self`.
/// 
/// If `old` is empty, it matches at the beginning of the string and after each
/// character in the string, so `new` is inserted at the beginning of the string
/// and after each character.
pub fn String::replace_all(
  self : String,
  old~ : StringView,
  new~ : StringView,
) -> String {
  let len = self.length()
  let buf = StringBuilder(size_hint=len)
  let old_len = old.length()
  let new = new.to_owned()
  // use write_substring to avoid intermediate allocations
  if old_len == 0 {
    buf.write_string(new)
    for c in self {
      buf.write_char(c)
      buf.write_string(new)
    }
    buf.to_string()
  } else {
    let first_end = self.find(old)
    if first_end is Some(end) {
      for view = self[:], end = end {
        let seg = view.view(end_offset=end)
        buf.write_substring(seg.data(), seg.start_offset(), seg.length())
        buf.write_string(new)
        // check if there is no more characters after the last occurrence of `old`
        guard end + old_len <= len else { break }
        let next_view = view.view(start_offset=end + old_len)
        guard next_view.find(old) is Some(next_end) else {
          buf.write_substring(
            next_view.data(),
            next_view.start_offset(),
            next_view.length(),
          )
          break
        }
        continue next_view, next_end
      }
      buf.to_string()
    } else {
      self
    }
  }
}

///|
test "replace_all" {
  assert_true("hello".replace_all(old="o", new="a") == "hella")
  assert_true("hello".replace_all(old="l", new="a") == "heaao")
  assert_true("hello".replace_all(old="ll", new="rr") == "herro")
  assert_true("hello".replace_all(old="hello", new="world") == "world")
  assert_true(
    "hello hello hello".replace_all(old="hello", new="hi") == "hi hi hi",
  )
  assert_true(
    "hello hello helloi".replace_all(old="hello", new="hi") == "hi hi hii",
  )
  assert_true(
    "hi hi hii".replace_all(old="hi", new="hello") == "hello hello helloi",
  )
  assert_true("hello".replace_all(old="", new="a") == "ahaealalaoa")
  assert_true("hello".replace_all(old="world", new="a") == "hello")
  assert_true("".replace_all(old="", new="a") == "a")
  assert_true("aaa".replace_all(old="a", new="b") == "bbb")
  assert_true("aaa".replace_all(old="a", new="bb") == "bbbbbb")
  assert_true("aaa".replace_all(old="aa", new="b") == "ba")
  assert_true(
    "🀣🀣🀣".replace_all(old="🀣", new="😊") == "😊😊😊",
  )
  assert_true("abc123abc".replace_all(old="abc", new="xyz") == "xyz123xyz")
  assert_true("abcabcabc".replace_all(old="abc", new="") == "")
  assert_true("abc".replace_all(old="abc", new="") == "")
  assert_true("abc".replace_all(old="", new="x") == "xaxbxcx")
}

///|
test "String::replace_all boundary cases" {
  // These tests should trigger the uncovered line 1187: guard end + old_len <= len else { break }
  // This happens when the pattern is found at the very end of the string

  // Pattern at the end of string - should trigger the guard condition
  assert_true("helloworld".replace_all(old="world", new="X") == "helloX")
  assert_true("abcdef".replace_all(old="def", new="XYZ") == "abcXYZ")

  // Multiple patterns where the last one is at the end
  assert_true("abcabc".replace_all(old="abc", new="X") == "XX")

  // Pattern that exactly matches the string length
  assert_true("test".replace_all(old="test", new="done") == "done")

  // Empty replacement at the end
  assert_true("remove_me".replace_all(old="_me", new="") == "remove")
}

///|
test "View::replace_all" {
  assert_true("hello"[:].replace_all(old="o", new="a") == "hella")
  assert_true("hello"[:].replace_all(old="l", new="a") == "heaao")
  assert_true("hello"[:].replace_all(old="ll", new="rr") == "herro")
  assert_true("hello"[:].replace_all(old="hello", new="world") == "world")
  assert_true(
    "hello hello hello"[:].replace_all(old="hello", new="hi") == "hi hi hi",
  )
  assert_true(
    "hello hello helloi"[:].replace_all(old="hello", new="hi") == "hi hi hii",
  )
  assert_true(
    "hi hi hii"[:].replace_all(old="hi", new="hello") == "hello hello helloi",
  )
  assert_true("hello"[:].replace_all(old="", new="a") == "ahaealalaoa")
  assert_true("hello"[:].replace_all(old="world", new="a") == "hello")
  assert_true(""[:].replace_all(old="", new="a") == "a")
  assert_true("aaa"[:].replace_all(old="a", new="b") == "bbb")
  assert_true("aaa"[:].replace_all(old="a", new="bb") == "bbbbbb")
  assert_true("aaa"[:].replace_all(old="aa", new="b") == "ba")
  assert_true(
    "🀣🀣🀣"[:].replace_all(old="🀣", new="😊") == "😊😊😊",
  )
  assert_true("abc123abc"[:].replace_all(old="abc", new="xyz") == "xyz123xyz")
  assert_true("abcabcabc"[:].replace_all(old="abc", new="") == "")
  assert_true("abc"[:].replace_all(old="abc", new="") == "")
  assert_true("abc"[:].replace_all(old="", new="x") == "xaxbxcx")
}

///|
test "View::replace_all boundary cases" {
  // These tests should trigger the uncovered line 1141: guard end + old_len <= len else { break }
  // This condition triggers when end + old_len > len, meaning we're at the boundary

  // Let me trace through the algorithm more carefully...
  // Actually, let me try a different approach - create a scenario where the view length changes

  // Try with overlapping patterns or edge cases
  assert_true("abcabc"[:].replace_all(old="abc", new="X") == "XX")
  assert_true("aaaa"[:].replace_all(old="aa", new="b") == "bb")

  // Pattern at exact end
  assert_true("hello"[:].replace_all(old="lo", new="X") == "helX")

  // Test with empty string edge case
  assert_true("a"[:].replace_all(old="a", new="") == "")

  // Let me try to understand when end + old_len > len could happen...
  // Maybe when we have a complex replacement scenario
  inspect("Testing boundary condition", content="Testing boundary condition")
}

///|
#cfg(not(any(target="js", target="native", target="wasm")))
fn find_ascii_uppercase_code_unit(
  data : String,
  start : Int,
  len : Int,
) -> Int? {
  let end = start + len
  for i in start.. Int? {
  let end = start + len
  if data.unsafe_get(start) is ('A'..='Z') {
    return Some(0)
  }
  let ascii_a = i16x8_splat(0x41)
  let ascii_range = i16x8_splat(0x5A - 0x41)
  let tail_start = for pos = start; pos + 8 <= end; {
    let code_units = v128_load_i16x8(data, pos)
    let uppercase_mask = i16x8_le_u(i16x8_sub(code_units, ascii_a), ascii_range)
    if v128_any_true(uppercase_mask) {
      for i in pos..<(pos + 8) {
        if data.unsafe_get(i) is ('A'..='Z') {
          return Some(i - start)
        }
      }
    }
    continue pos + 8
  } nobreak {
    pos
  }
  for i in tail_start.. Int? {
  if len < 24 {
    let end = start + len
    for i in start.. String {
  let len = view.length()
  let output : FixedArray[UInt16] = FixedArray::make(len, 0)
  output.unsafe_blit_from_string(0, view.data(), view.start_offset(), len)
  let ascii_a = i16x8_splat(0x41)
  let ascii_range = i16x8_splat(0x5A - 0x41)
  let lowercase_delta = i16x8_splat(0x20)
  let tail_start = for pos = first_uppercase; pos + 8 <= len; {
    let code_units = v128_load_fixedarray_i16x8(output, pos)
    // Subtraction wraps lanes below `A`, so this unsigned comparison selects
    // exactly the inclusive `A..Z` range with one vector comparison.
    let uppercase_mask = i16x8_le_u(i16x8_sub(code_units, ascii_a), ascii_range)
    let lowered = i16x8_add(
      code_units,
      v128_and(uppercase_mask, lowercase_delta),
    )
    v128_store_i16x8(output, pos, lowered)
    continue pos + 8
  } nobreak {
    pos
  }
  for i in tail_start.. String {
  let len = view.length()
  let output : FixedArray[UInt16] = FixedArray::make(len, 0)
  output.unsafe_blit_from_string(0, view.data(), view.start_offset(), len)
  let tail = view.view(start_offset=first_uppercase)
  // Keep `tail.code_units()` directly in the loop so compiler backends can
  // recognize and lower the zero-copy traversal at the use site.
  for i, u in tail.code_units() {
    if u is ('A'..='Z') {
      output.unsafe_set(first_uppercase + i, (u.to_int() + 32).to_uint16())
    }
  }
  unsafe_fixedarray_uint16_to_string(output)
}

///|
/// Converts this string to lowercase.
#cfg(not(target="js"))
pub fn StringView::to_lower(self : StringView) -> StringView {
  // TODO: deal with non-ascii characters
  let data = self.data()
  let start = self.start_offset()
  let len = self.length()
  guard find_ascii_uppercase_code_unit(data, start, len) is Some(idx) else {
    return self
  }
  ascii_lowercase_copy(self, idx)
}

///|
/// Converts this string to lowercase.
#cfg(not(target="js"))
pub fn String::to_lower(self : String) -> String {
  // TODO: deal with non-ascii characters
  let len = self.length()
  guard find_ascii_uppercase_code_unit(self, 0, len) is Some(idx) else {
    return self
  }
  ascii_lowercase_copy(self, idx)
}

///|
/// JavaScript strings cannot be patched in place, so retain the character-loop
/// fallback instead of allocating an intermediate substring per unchanged run.
#cfg(target="js")
pub fn StringView::to_lower(self : StringView) -> StringView {
  // TODO: deal with non-ascii characters
  guard self.find_by(c => c.is_ascii_uppercase()) is Some(idx) else {
    return self
  }
  let buf = StringBuilder(size_hint=self.length())
  let head = self.view(end_offset=idx)
  buf.write_substring(head.data(), head.start_offset(), head.length())
  for c in self.view(start_offset=idx) {
    if c.is_ascii_uppercase() {
      buf.write_char((c.to_int() + 32).unsafe_to_char())
    } else {
      buf.write_char(c)
    }
  }
  buf.to_string()
}

///|
/// Converts this string to lowercase.
#cfg(target="js")
pub fn String::to_lower(self : String) -> String {
  // TODO: deal with non-ascii characters
  guard self.find_by(c => c.is_ascii_uppercase()) is Some(idx) else {
    return self
  }
  let buf = StringBuilder(size_hint=self.length())
  let head = self.view(end_offset=idx)
  buf.write_substring(head.data(), head.start_offset(), head.length())
  for c in self.view(start_offset=idx) {
    if c.is_ascii_uppercase() {
      buf.write_char((c.to_int() + 32).unsafe_to_char())
    } else {
      buf.write_char(c)
    }
  }
  buf.to_string()
}

///|
test "to_lower" {
  assert_true("Hello".to_lower() == "hello")
  assert_true("HELLO".to_lower() == "hello")
  assert_true("Hello, World!".to_lower() == "hello, world!")
  // Starts at an unaligned code-unit offset, crosses both ASCII range
  // boundaries, fills one vector, and leaves one scalar code unit.
  assert_true("xxA@Z[az09!".to_lower() == "xxa@z[az09!")
  assert_true("🀣A".to_lower() == "🀣a")
  assert_true("A🀣B".to_lower() == "a🀣b")
  let unpaired = String::from_array([(0xD800).unsafe_to_char(), 'A'])
  let lowered_unpaired = String::from_array([(0xD800).unsafe_to_char(), 'a'])
  assert_true(unpaired.to_lower() == lowered_unpaired)
}

///|
test "to_lower after a non-BMP character" {
  inspect("πŸ˜€X".to_lower(), content="πŸ˜€x")
  inspect("aπŸ˜€Xz"[1:4].to_lower(), content="πŸ˜€x")
}

///|
test "to_lower SIMD search boundaries" {
  for uppercase_at in 0..<40 {
    let prefix = "x".repeat(uppercase_at)
    let suffix = "x".repeat(40 - uppercase_at)
    assert_true((prefix + "A" + suffix).to_lower() == prefix + "a" + suffix)
  }
  let source = "A" + "x".repeat(40) + "Z"
  assert_true(source[1:41].to_lower() == "x".repeat(40))
}

///|
test "View::to_lower" {
  assert_true("Hello"[:].to_lower() == "hello")
  assert_true("HELLO"[:].to_lower() == "hello")
  assert_true("Hello, World!"[:].to_lower() == "hello, world!")
  assert_true("x🀣ABy"[1:5].to_lower() == "🀣ab")
  let unpaired = String::from_array([(0xD800).unsafe_to_char(), 'A'])
  let lowered_unpaired = String::from_array([(0xD800).unsafe_to_char(), 'a'])
  assert_true(unpaired[:].to_lower() == lowered_unpaired)
}

///|
/// Converts this string to uppercase.
pub fn StringView::to_upper(self : StringView) -> StringView {
  // TODO: deal with non-ascii characters
  guard self.find_by(c => c.is_ascii_lowercase()) is Some(idx) else {
    return self
  }
  let buf = StringBuilder(size_hint=self.length())
  let head = self.view(end_offset=idx)
  buf.write_substring(head.data(), head.start_offset(), head.length())
  for c in self.view(start_offset=idx) {
    if c.is_ascii_lowercase() {
      buf.write_char((c.to_int() - 32).unsafe_to_char())
    } else {
      buf.write_char(c)
    }
  }
  buf.to_string()
}

///|
/// Converts this string to uppercase.
pub fn String::to_upper(self : String) -> String {
  // TODO: deal with non-ascii characters
  guard self.find_by(c => c.is_ascii_lowercase()) is Some(idx) else {
    return self
  }
  let buf = StringBuilder(size_hint=self.length())
  let head = self.view(end_offset=idx)
  buf.write_substring(head.data(), head.start_offset(), head.length())
  for c in self.view(start_offset=idx) {
    if c.is_ascii_lowercase() {
      buf.write_char((c.to_int() - 32).unsafe_to_char())
    } else {
      buf.write_char(c)
    }
  }
  buf.to_string()
}

///|
test "to_upper" {
  assert_true("hello".to_upper() == "HELLO")
  assert_true("HELLO".to_upper() == "HELLO")
  assert_true("Hello, World!".to_upper() == "HELLO, WORLD!")
}

///|
test "to_upper after a non-BMP character" {
  inspect("πŸ˜€x".to_upper(), content="πŸ˜€X")
  inspect("aπŸ˜€xz"[1:4].to_upper(), content="πŸ˜€X")
}

///|
test "View::to_upper" {
  assert_true("hello"[:].to_upper() == "HELLO")
  assert_true("HELLO"[:].to_upper() == "HELLO")
  assert_true("Hello, World!"[:].to_upper() == "HELLO, WORLD!")
}

///|
/// Folds the characters of the string into a single value.
#locals(f)
pub fn[A] StringView::fold(
  self : StringView,
  init~ : A,
  f : (A, Char) -> A raise?,
) -> A raise? {
  for c in self; rv = (init : A) {
    continue f(rv, c)
  } nobreak {
    rv
  }
}

///|
/// Folds the characters of the string into a single value.
pub fn[A] String::fold(
  self : String,
  init~ : A,
  f : (A, Char) -> A raise?,
) -> A raise? {
  self[:].fold(init~, f)
}

///|
test "fold" {
  assert_true(
    "hello".fold(init=[], (acc, c) => {
      acc.push(c)
      acc
    }) ==
    ['h', 'e', 'l', 'l', 'o'],
  )
  assert_true(
    "hello".fold(init=0, (acc, c) => acc + c.to_int()) ==
    104 + 101 + 108 + 108 + 111,
  )
}

///|
test "fold with raise" {
  try
    ignore(
      "hello".fold(init=0, (acc, c) => {
        if c == 'l' {
          raise Failure("found l")
        }
        acc + 1
      }),
    )
  catch {
    e => assert_true(e is Failure("found l"))
  } noraise {
    _ => fail("expected fold to raise")
  }
}

///|
/// Function `rev_fold`.
#locals(f)
pub fn[A] StringView::rev_fold(
  self : StringView,
  init~ : A,
  f : (A, Char) -> A raise?,
) -> A raise? {
  for c in self.rev_iter(); rv = (init : A) {
    continue f(rv, c)
  } nobreak {
    rv
  }
}

///|
/// Function `rev_fold`.
pub fn[A] String::rev_fold(
  self : String,
  init~ : A,
  f : (A, Char) -> A raise?,
) -> A raise? {
  self[:].rev_fold(init~, f)
}

///|
test "rev_fold" {
  assert_true(
    "hello".rev_fold(init=[], (acc, c) => {
      acc.push(c)
      acc
    }) ==
    ['o', 'l', 'l', 'e', 'h'],
  )
  assert_true(
    "hello".rev_fold(init=0, (acc, c) => acc + c.to_int()) ==
    111 + 108 + 108 + 101 + 104,
  )
}

///|
test "rev_fold with raise" {
  try
    ignore(
      "hello".rev_fold(init=0, (acc, c) => {
        if c == 'l' {
          raise Failure("found l")
        }
        acc + 1
      }),
    )
  catch {
    e => assert_true(e is Failure("found l"))
  } noraise {
    _ => fail("expected rev_fold to raise")
  }
}

///|
/// Returns the UTF-16 code unit at the given index. Returns `None` if the index
/// is out of bounds.
#intrinsic("%string.get_opt")
pub fn String::get(self : String, idx : Int) -> UInt16? {
  guard idx >= 0 && idx < self.length() else { return None }
  Some(self.unsafe_get(idx))
}

///|
/// Returns the UTF-16 code unit at the given index. Returns `None` if the index
/// is out of bounds.
#intrinsic("%stringview.get_opt")
pub fn StringView::get(self : StringView, idx : Int) -> UInt16? {
  guard idx >= 0 && idx < self.length() else { return None }
  Some(self.unsafe_get(idx))
}

///|
test "String::get supports emoji (surrogate pair)" {
  let s = "hello"
  assert_true(s.get(0) is Some(104))
  assert_true(s.get(4) is Some(111))
  assert_true(s.get(5) is None)
  assert_true(s.get(-1) is None)
  let s = "a🀣b"
  assert_true(s.get(0) is Some(97))
  assert_true(s.get(1) is Some(55358))
  assert_true(s.get(2) is Some(56611))
  assert_true(s.get(3) is Some(98))
  assert_true(s.get(4) is None)
}

///|
test "View::get basic cases" {
  let v = "hello"[1:4]
  assert_true(v.get(0) is Some(101))
  assert_true(v.get(2) is Some(108))
  assert_true(v.get(3) is None)
  assert_true(v.get(-1) is None)
  let v = "ab🀣cd"[1:5]
  assert_true(v.get(0) is Some(98))
  assert_true(v.get(1) is Some(55358))
  assert_true(v.get(2) is Some(56611))
}

///|
/// Returns the character at the given index. Returns `None` if the index is out
/// of bounds or the index splits a surrogate pair.
pub fn String::get_char(self : String, idx : Int) -> Char? {
  guard idx >= 0 && idx < self.length() else { return None }
  let c = self.unsafe_get(idx)
  if c.is_leading_surrogate() {
    guard idx + 1 < self.length() else { return None }
    let next = self.unsafe_get(idx + 1)
    if next.is_trailing_surrogate() {
      Some(code_point_of_surrogate_pair(c.to_int(), next.to_int()))
    } else {
      None
    }
  } else if c.is_trailing_surrogate() {
    None
  } else {
    Some(c.unsafe_to_char())
  }
}

///|
/// Returns the character at the given index. Returns `None` if the index is out
/// of bounds or the index splits a surrogate pair.
pub fn StringView::get_char(self : StringView, idx : Int) -> Char? {
  guard idx >= 0 && idx < self.length() else { return None }
  let c = self.unsafe_get(idx)
  if c.is_leading_surrogate() {
    guard idx + 1 < self.length() else { return None }
    let next = self.unsafe_get(idx + 1)
    if next.is_trailing_surrogate() {
      Some(code_point_of_surrogate_pair(c.to_int(), next.to_int()))
    } else {
      None
    }
  } else if c.is_trailing_surrogate() {
    None
  } else {
    Some(c.unsafe_to_char())
  }
}

///|
test "String::get_char basic cases" {
  // Basic ASCII characters
  let s = "hello"
  assert_true(s.get_char(0) == Some('h'))
  assert_true(s.get_char(1) == Some('e'))
  assert_true(s.get_char(4) == Some('o'))
  assert_true(s.get_char(5) == None)
  assert_true(s.get_char(-1) == None)

  // Contains emoji (surrogate pair)
  let s = "a🀣b"
  assert_true(s.get_char(0) == Some('a'))
  assert_true(s.get_char(1) == Some('🀣'))
  assert_true(s.get_char(2) == None) // Second half of surrogate pair is not a valid char
  assert_true(s.get_char(3) == Some('b'))
  assert_true(s.get_char(4) == None)
}

///|
test "View::get_char basic cases" {
  let s = "a🀣b"
  let v = s[0:3]
  assert_true(v.get_char(0) == Some('a'))
  assert_true(v.get_char(1) == Some('🀣'))
  assert_true(v.get_char(2) == None)
  assert_true(v.get_char(3) == None)
  assert_true(v.get_char(4) == None)

  // Test substring view
  let v2 = s[1:3] // Only contains the emoji surrogate pair
  assert_true(v2.get_char(0) == Some('🀣'))
  assert_true(v2.get_char(1) == None)
  assert_true(v2.get_char(2) == None)
}