///| The type of a word (16-bit) appears in a UTF-16 sequence:  
/// 
///  - `BMP`: Basic Multilingual Plane. (including `0x0000–0xD7FF` and `0xE000–0xFFFF`)  
///  - `HighSurrogate`: The first word of a surrogate pair. (including `0xD800–0xDBFF`)  
///  - `LowSurrogate`: The second word of a surrogate pair. (including `0xDC00–0xDFFF`)  
pub(all) enum Utf16CodePoint {
  BMP(Char)
  HighSurrogate(Char)
  LowSurrogate(Char)
} derive(Eq, Show)

///|
pub fn Utf16CodePoint::to_len(self : Utf16CodePoint) -> Int {
  match self {
    HighSurrogate(_) => 2
    _ => 1
  }
}

///|
pub fn Utf16CodePoint::to_char(self : Utf16CodePoint) -> Char {
  match self {
    HighSurrogate(char) | LowSurrogate(char) | BMP(char) => char
  }
}

///|
pub fn Utf16CodePoint::to_tuple(self : Utf16CodePoint) -> (Char, Int) {
  match self {
    HighSurrogate(char) => (char, 2)
    LowSurrogate(char) | BMP(char) => (char, 1)
  }
}

///| Get the char appeared at n-th word (16-bit) of input string.
/// 
/// # Examples
/// 
/// Iterate a string including `U+10000` by valid chars (unicode code point) using a `while` loop:
///
/// 
/// # Panics
/// 
/// Panics if index out of range, or any invalid value (code point or surrogate pair) appears at specified position.
pub fn[T : GetUtf16CodePoint] get_utf16_char_at(
  input : T,
  idx_word : Int
) -> Utf16CodePoint {
  &GetUtf16CodePoint::get_utf16(input, idx_word)
}

///|
test "" {
  let str = "\u{6708}\u{1f430}\u{5154}" // 月🐰兔
  let mut i = 0
  while i < str.length() {
    let ty = get_utf16_char_at(str, i)
    // println(ch)

    // advance by 2 words on a surrogate pair
    i += if ty is Utf16CodePoint::BMP(_) { 1 } else { 2 }
  }
}

///|
fn _get_utf16_char_at(input : String, idx_word : Int) -> Utf16CodePoint {
  input |> get_utf16_char_at(idx_word)
}

///|
test "get_utf16_char_at" {
  // standard test
  let str = "a\u{6587}\u{10000}\u{1F408}"
  let expected = [
    ('a', BMP('a')),
    ('\u{6587}', BMP('\u{6587}')),
    ('\u{10000}', HighSurrogate('\u{10000}')),
    ('\u{10000}', LowSurrogate('\u{10000}')),
    ('\u{1F408}', HighSurrogate('\u{1F408}')),
    ('\u{1F408}', LowSurrogate('\u{1F408}')),
  ]
  for i = 0; i < str.length(); i = i + 1 {
    let ch = _get_utf16_char_at(str, i)
    // assert_eq!(ch.to_char(), expected[i].0)
    assert_eq(ch, expected[i].1)
  }
  inspect("👨🏿‍🦱".length(), content="7")
  inspect(
    _get_utf16_char_at("👨🏿‍🦱", 0),
    content="HighSurrogate('👨')",
  )
  inspect(
    _get_utf16_char_at("👨🏿‍🦱", 1),
    content="LowSurrogate('👨')",
  )
  inspect(
    _get_utf16_char_at("👨🏿‍🦱", 2),
    content="HighSurrogate('🏿')",
  )
  inspect(
    _get_utf16_char_at("👨🏿‍🦱", 3),
    content="LowSurrogate('🏿')",
  )
  inspect(_get_utf16_char_at("👨🏿‍🦱", 4), content="BMP('\\u{200d}')")
  inspect(
    _get_utf16_char_at("👨🏿‍🦱", 5),
    content="HighSurrogate('🦱')",
  )
  inspect(
    _get_utf16_char_at("👨🏿‍🦱", 6),
    content="LowSurrogate('🦱')",
  )

  // doctest (keep this part same to the doc comments)
  let str = "\u{6708}\u{1f430}\u{5154}" // 月🐰兔
  let mut i = 0
  while i < str.length() {
    let ch = _get_utf16_char_at(str, i)
    // println(ch) // comment out to omit display

    // advance by 2 words on a surrogate pair
    i += ch.to_len()
  }
  assert_eq(i, 4)
}

///|
pub fn combine_surrogate_pair_int(high : Int, low : Int) -> Char {
  if (high & 0xFFFFFC00) != 0x0000D800 {
    if DEBUGGER {
      abort("invalid high surrogate \{high}")
    } else {
      panic()
    }
  } else if (low & 0xFFFFFC00) != 0x0000DC00 {
    if DEBUGGER {
      abort("invalid low surrogate \{low}")
    } else {
      panic()
    }
  }
  let codepoint = ((high & 0x000003FF) << 10) + (low & 0x000003FF) + 0x00010000
  codepoint.unsafe_to_char()
}

///|
pub fn safe_string_each(str : String, f : (Char) -> Unit) -> Unit {
  let mut cursor = 0
  while cursor < str.length() {
    let char = get_utf16_char_at(str, cursor)
    f(char.to_char())
    cursor += char.to_len()
  }
}

///|
pub fn safe_string_eachi(str : String, f : (Int, Char) -> Unit) -> Unit {
  let mut cursor = 0
  while cursor < str.length() {
    let char = get_utf16_char_at(str, cursor)
    f(cursor, char.to_char())
    cursor += char.to_len()
  }
}

///|
pub fn[A : ToString] to_string(target : A) -> String {
  target |> ToString::to_string()
}