///|
struct Wtf16(FixedArray[UInt16]) derive(Eq, Show, Hash, Compare, Default)

///|
pub suberror Malformed Int derive(Show, ToJson)

///|
fn is_valid_utf16(array : FixedArray[UInt16]) -> Int {
  let len = array.length()
  for pos = 0; pos < len; {
    let b = array.unsafe_get(pos)
    match b {
      _..<0xD800 | 0xE000..<_ => continue pos + 1
      0xDC00..=0xDFFF => return -pos - 1
      0xD800..=0xDBFF => {
        if pos + 1 >= len {
          return -pos - 1
        }
        let b2 = array.unsafe_get(pos + 1)
        if !(b2 is (0xDC00..=0xDFFF)) {
          return -pos - 2
        }
        continue pos + 2
      }
    }
  } else {
    0
  }
}

///|
fn FixedArray::unsafe_reinterpret_as_string(
  self : FixedArray[UInt16],
) -> String = "%identity"

///|
fn String::unsafe_reinterpret_as_fixedarray_uint16(
  self : String,
) -> FixedArray[UInt16] = "%identity"

///|
#as_free_fn
pub fn Wtf16::of_string(s : String) -> Wtf16 {
  Wtf16(s.unsafe_reinterpret_as_fixedarray_uint16())
}

///|
#as_free_fn
pub fn Wtf16::of_codeunits(cu : FixedArray[UInt16]) -> Wtf16 {
  let array : FixedArray[UInt16] = FixedArray::make(cu.length(), 0)
  array.unsafe_blit(0, cu, 0, cu.length())
  array
}

///|
#as_free_fn
pub fn Wtf16::of_codepoints(cps : FixedArray[@codepoint.CodePoint]) -> Wtf16 {
  let len = cps.length()
  let buf = buffer(capacity=len)
  for i in 0.. String raise Malformed {
  let err = is_valid_utf16(self.0)
  if err < 0 {
    raise Malformed(-err - 1)
  }
  self.0.unsafe_reinterpret_as_string()
}

///|
pub fn Wtf16::to_codeunits(self : Wtf16) -> FixedArray[UInt16] {
  let array : FixedArray[UInt16] = FixedArray::make(self.0.length(), 0)
  array.unsafe_blit(0, self.0, 0, self.0.length())
  array
}

///|
pub fn Wtf16::to_codepoints(self : Wtf16) -> FixedArray[@codepoint.CodePoint] {
  let len = self.0.length()
  let buf : Array[@codepoint.CodePoint] = []
  for pos = 0; pos < len; {
    let p = self.0.unsafe_get(pos)
    if p < 0xD800 || p > 0xDFFF {
      buf.push(@codepoint.of_int_unchecked(p.to_int()))
      continue pos + 1
    }
    if p is (0xD800..=0xDBFF) &&
      pos + 1 < len &&
      self.0.unsafe_get(pos + 1) is (0xDC00..=0xDFFF) {
      let p2 = self.0.unsafe_get(pos + 1)
      let v = ((p.to_int() - 0xD800) << 10) + (p2.to_int() - 0xDC00) + 0x10000
      buf.push(@codepoint.of_int_unchecked(v))
      continue pos + 2
    }
    buf.push(@codepoint.of_int_unchecked(p.to_int()))
    continue pos + 1
  }
  FixedArray::from_array(buf)
}

///|
pub fn Wtf16::codepoints(self : Wtf16) -> Iter[@codepoint.CodePoint] {
  Iter::new(each => {
    let len = self.0.length()
    for pos = 0; pos < len; {
      let p = self.0.unsafe_get(pos)
      if p < 0xD800 || p > 0xDFFF {
        match each(@codepoint.of_int_unchecked(p.to_int())) {
          IterEnd => break
          IterContinue => continue pos + 1
        }
        continue pos + 1
      }
      if p is (0xD800..=0xDBFF) &&
        pos + 1 < len &&
        self.0.unsafe_get(pos + 1) is (0xDC00..=0xDFFF) {
        let p2 = self.0.unsafe_get(pos + 1)
        let v = ((p.to_int() - 0xD800) << 10) + (p2.to_int() - 0xDC00) + 0x10000
        match each(@codepoint.of_int_unchecked(v)) {
          IterEnd => break
          IterContinue => continue pos + 2
        }
      }
      match each(@codepoint.of_int_unchecked(p.to_int())) {
        IterEnd => break
        IterContinue => continue pos + 1
      }
    }
    IterContinue
  })
}

///|
pub impl ToJson for Wtf16 with to_json(self : Wtf16) -> Json {
  self.0.to_json()
}