Case

// Case operations: upper / lower / title / fold over `String box`.
//
// Per UAX #21, "full" case mappings can expand to multiple codepoints
// (e.g., ß → SS, fi → FI). For each codepoint we prefer the full
// mapping from SpecialCasing.txt; if absent, we fall back to the
// single-codepoint Simple mapping from UnicodeData.txt.
//
// Casefolding ("fold") flattens case differences for caseless
// comparison. Default folding uses CaseFolding.txt's C+F entries
// (full folding). M5 normalization is NOT applied — for true caseless
// equality you'd typically normalize-then-fold or fold-then-normalize.
//
// Locale- and context-sensitive rules (Turkic I-dot, Greek final
// sigma, Lithuanian dot-above) are deferred — they need conditional
// SpecialCasing entries that M1.D explicitly skipped. The current
// implementation produces the "Default" case mappings.
//
// Titlecase here is the "simple word-start" form: a cp is titlecased
// if it's at the start of the string or follows a White_Space cp;
// otherwise it's lowercased. This isn't full UAX #21 (which uses UAX
// #29 word boundaries) but is the common-case useful behavior.

primitive Case
  fun upper(s: String box): (String iso^ | InvalidUtf8) =>
    """
    Uppercase every cp via its full (multi-cp) or simple (single-cp)
    Uppercase mapping. Returns `InvalidUtf8` for ill-formed input.
    """
    match Bytes.first_bad_utf8_offset(s)
    | AllValid => _upper(s)
    | let off: USize => InvalidUtf8(off)
    end

  fun lower(s: String box): (String iso^ | InvalidUtf8) =>
    """
    Lowercase every cp via its full or simple Lowercase mapping.
    """
    match Bytes.first_bad_utf8_offset(s)
    | AllValid => _lower(s)
    | let off: USize => InvalidUtf8(off)
    end

  fun title(s: String box): (String iso^ | InvalidUtf8) =>
    """
    Titlecase using a "simple word-start" rule: cps at the start of
    the string or following a White_Space cp are titlecased; others
    are lowercased. See the file header for limitations.
    """
    match Bytes.first_bad_utf8_offset(s)
    | AllValid => _title(s)
    | let off: USize => InvalidUtf8(off)
    end

  fun fold(s: String box): (String iso^ | InvalidUtf8) =>
    """
    Default full case folding for caseless comparison. Each cp is
    replaced by its full casefold expansion if present, else its
    simple casefold, else itself.
    """
    match Bytes.first_bad_utf8_offset(s)
    | AllValid => _fold(s)
    | let off: USize => InvalidUtf8(off)
    end

  // ============================================================
  // Workhorses (assume valid UTF-8)
  // ============================================================

  fun _upper(utf8: String box): String iso^ =>
    let out = recover iso String(utf8.size()) end
    for cp in utf8.runes() do
      match Codepoints.full_upper(cp)
      | let a: Array[U32] val =>
        for u in a.values() do out.push_utf32(u) end
      | None =>
        out.push_utf32(Codepoints.simple_upper(cp))
      end
    end
    consume out

  fun _lower(utf8: String box): String iso^ =>
    let out = recover iso String(utf8.size()) end
    for cp in utf8.runes() do
      match Codepoints.full_lower(cp)
      | let a: Array[U32] val =>
        for u in a.values() do out.push_utf32(u) end
      | None =>
        out.push_utf32(Codepoints.simple_lower(cp))
      end
    end
    consume out

  fun _title(utf8: String box): String iso^ =>
    let out = recover iso String(utf8.size()) end
    var at_word_start: Bool = true
    for cp in utf8.runes() do
      if Codepoints.has_binary_property(cp, PropWhiteSpace) then
        out.push_utf32(cp)
        at_word_start = true
      elseif at_word_start then
        match Codepoints.full_title(cp)
        | let a: Array[U32] val =>
          for u in a.values() do out.push_utf32(u) end
        | None =>
          out.push_utf32(Codepoints.simple_title(cp))
        end
        at_word_start = false
      else
        match Codepoints.full_lower(cp)
        | let a: Array[U32] val =>
          for u in a.values() do out.push_utf32(u) end
        | None =>
          out.push_utf32(Codepoints.simple_lower(cp))
        end
      end
    end
    consume out

  fun _fold(utf8: String box): String iso^ =>
    let out = recover iso String(utf8.size()) end
    for cp in utf8.runes() do
      match Codepoints.full_casefold(cp)
      | let a: Array[U32] val =>
        for u in a.values() do out.push_utf32(u) end
      | None =>
        out.push_utf32(Codepoints.simple_casefold(cp))
      end
    end
    consume out