///|
let linkify_punycode_base = 36

///|
let linkify_punycode_tmin = 1

///|
let linkify_punycode_tmax = 26

///|
let linkify_punycode_skew = 38

///|
let linkify_punycode_damp = 700

///|
let linkify_punycode_initial_bias = 72

///|
let linkify_punycode_initial_n = 128

///|
fn linkify_punycode_href(href : StringView) -> String {
  if @syn.starts_with_case_insensitive(href, 0, "mailto:") {
    return href.to_owned()
  }
  let (prefix, rest_start) = if @syn.starts_with_case_insensitive(
      href, 0, "http://",
    ) {
    ("http://", 7)
  } else if @syn.starts_with_case_insensitive(href, 0, "https://") {
    ("https://", 8)
  } else if @syn.starts_with_case_insensitive(href, 0, "ftp://") {
    ("ftp://", 6)
  } else if @syn.starts_with_case_insensitive(href, 0, "//") {
    ("//", 2)
  } else {
    ("", 0)
  }
  let rest = href[rest_start:href.length()]
  let (hostport, tail) = linkify_split_host_and_tail(rest)
  let mut userinfo = ""
  let mut hostport_without_userinfo = hostport
  match linkify_last_char_index(hostport, '@') {
    Some(at) => {
      userinfo = hostport[0:at].to_owned()
      hostport_without_userinfo = hostport[at + 1:hostport.length()]
    }
    None => ()
  }
  if hostport_without_userinfo.has_prefix("[") {
    return href.to_owned()
  }
  let (host, port) = linkify_split_host_port(hostport_without_userinfo)
  let host = linkify_punycode_host(host)
  let out = StringBuilder::new(size_hint=href.length() + 16)
  out.write_string(prefix)
  if userinfo != "" {
    out.write_string(userinfo)
    out.write_char('@')
  }
  out.write_string(host)
  if port != "" {
    out.write_char(':')
    out.write_string(port)
  }
  out.write_stringview(tail)
  out.to_string()
}

///|
fn linkify_split_host_and_tail(raw : StringView) -> (StringView, StringView) {
  let mut pos = 0
  for ch in raw {
    match ch {
      '/' | '?' | '#' => return (raw[0:pos], raw[pos:raw.length()])
      _ => pos += ch.utf16_len()
    }
  }
  (raw, "")
}

///|
fn linkify_punycode_host(host : StringView) -> String {
  if !linkify_has_non_ascii(host) {
    return host.to_owned()
  }
  let labels : Array[String] = []
  let mut start = 0
  let mut pos = 0
  for ch in host {
    if ch == '.' {
      let label = host[start:pos]
      labels.push(linkify_punycode_label(label))
      start = pos + 1
    }
    pos += ch.utf16_len()
  }
  let label = host[start:host.length()]
  labels.push(linkify_punycode_label(label))
  labels.join(".")
}

///|
fn linkify_punycode_label(label : StringView) -> String {
  if !linkify_has_non_ascii(label) {
    return label.to_owned()
  }
  let codepoints = linkify_idna_codepoints(label)
  let encoded = linkify_punycode_encode(codepoints)
  if linkify_codepoints_all_basic(codepoints) {
    encoded
  } else {
    "xn--" + encoded
  }
}

///|
fn linkify_codepoints_all_basic(input : Array[Int]) -> Bool {
  for code in input {
    if code >= 0x80 {
      return false
    }
  }
  true
}

///|
fn linkify_has_non_ascii(value : StringView) -> Bool {
  for ch in value {
    if ch.to_int() >= 128 {
      return true
    }
  }
  false
}

///|
fn linkify_idna_codepoints(label : StringView) -> Array[Int] {
  let out : Array[Int] = []
  for ch in label {
    let code = ch.to_int()
    if code >= 0x41 && code <= 0x5A {
      out.push(code + 0x20)
    } else if (code >= 0xC0 && code <= 0xD6) || (code >= 0xD8 && code <= 0xDE) {
      out.push(code + 0x20)
    } else {
      linkify_push_idna_mapped_codepoint(out, code)
    }
  }
  out
}

///|
fn linkify_push_idna_mapped_codepoint(out : Array[Int], code : Int) -> Unit {
  match code {
    0x00DF => {
      out.push(0x73)
      out.push(0x73)
    }
    0xFE8D => out.push(0x0627)
    0xFE93 => out.push(0x0629)
    0xFE97 | 0xFE98 => out.push(0x062A)
    0xFEAD => out.push(0x0631)
    0xFEAF => out.push(0x0632)
    0xFEBB => out.push(0x0635)
    0xFECB => out.push(0x0639)
    0xFED8 => out.push(0x0642)
    0xFEE1 | 0xFEE2 => out.push(0x0645)
    0xFEED | 0xFEEE => out.push(0x0648)
    0xFEFC => {
      out.push(0x0644)
      out.push(0x0627)
    }
    _ => out.push(code)
  }
}

///|
fn linkify_punycode_encode(input : Array[Int]) -> String {
  let out = StringBuilder::new()
  let input_len = input.length()
  let mut basic_count = 0
  for code in input {
    if code < 0x80 {
      out.write_char(code.unsafe_to_char())
      basic_count += 1
    }
  }
  let mut handled = basic_count
  if basic_count > 0 && handled < input_len {
    out.write_char('-')
  }
  let mut n = linkify_punycode_initial_n
  let mut delta = 0
  let mut bias = linkify_punycode_initial_bias
  while handled < input_len {
    let mut m = 0x110000
    for code in input {
      if code >= n && code < m {
        m = code
      }
    }
    delta += (m - n) * (handled + 1)
    n = m
    for code in input {
      if code < n {
        delta += 1
      } else if code == n {
        let mut q = delta
        let mut k = linkify_punycode_base
        let mut done = false
        while !done {
          let t = if k <= bias {
            linkify_punycode_tmin
          } else if k >= bias + linkify_punycode_tmax {
            linkify_punycode_tmax
          } else {
            k - bias
          }
          if q < t {
            out.write_char(linkify_punycode_digit(q))
            done = true
          } else {
            out.write_char(
              linkify_punycode_digit(t + (q - t) % (linkify_punycode_base - t)),
            )
            q = (q - t) / (linkify_punycode_base - t)
            k += linkify_punycode_base
          }
        }
        bias = linkify_punycode_adapt(
          delta,
          handled + 1,
          handled == basic_count,
        )
        delta = 0
        handled += 1
      }
    }
    delta += 1
    n += 1
  }
  out.to_string()
}

///|
fn linkify_punycode_digit(value : Int) -> Char {
  if value < 26 {
    (0x61 + value).unsafe_to_char()
  } else {
    (0x30 + value - 26).unsafe_to_char()
  }
}

///|
fn linkify_punycode_adapt(delta : Int, numpoints : Int, first : Bool) -> Int {
  let mut delta = if first { delta / linkify_punycode_damp } else { delta / 2 }
  delta += delta / numpoints
  let mut k = 0
  while delta >
        (linkify_punycode_base - linkify_punycode_tmin) *
        linkify_punycode_tmax /
        2 {
    delta /= linkify_punycode_base - linkify_punycode_tmin
    k += linkify_punycode_base
  }
  k +
  (linkify_punycode_base - linkify_punycode_tmin + 1) *
  delta /
  (delta + linkify_punycode_skew)
}