///|
pub suberror InvalidAddr derive(@debug.Debug)

///|
pub suberror ResolveHostnameError {
  ResolveHostnameError(String)
} derive(@debug.Debug)

///|
pub(all) enum IpProtocolPreference {
  OnlyV4
  OnlyV6
  FavorV4
  FavorV6
  NoPreference
} derive(@debug.Debug)

///|
pub struct Addr {
  priv host : String
  priv port : Int
  priv ip : UInt
  priv is_ipv6 : Bool
} derive(Eq, Compare, Hash)

///|
fn ipv4_part(ip : UInt, shift : Int) -> String {
  ((ip >> shift) & 0xffU).to_string()
}

///|
fn ipv4_host_from_ip(ip : UInt) -> String {
  ipv4_part(ip, 24) +
  "." +
  ipv4_part(ip, 16) +
  "." +
  ipv4_part(ip, 8) +
  "." +
  ipv4_part(ip, 0)
}

///|
fn parse_port(raw : StringView) -> Int raise InvalidAddr {
  let port = @string.parse_int(raw, base=10) catch { _ => raise InvalidAddr }
  guard 0 <= port && port < 65536 else { raise InvalidAddr }
  port
}

///|
fn parse_ipv4_host(raw : StringView) -> UInt raise InvalidAddr {
  let parts = raw.split(".").collect()
  guard parts.length() == 4 else { raise InvalidAddr }
  let mut ip = 0U
  for part in parts {
    let value = @string.parse_int(part, base=10) catch {
      _ => raise InvalidAddr
    }
    guard 0 <= value && value <= 255 else { raise InvalidAddr }
    ip = (ip << 8) | value.reinterpret_as_uint()
  }
  ip
}

///|
fn parse_ipv6(src : String) -> Addr raise InvalidAddr {
  let mut close = -1
  for i = 1; i < src.length(); i = i + 1 {
    if src[i] == ']' {
      close = i
      break
    }
  }
  guard close > 1 && close + 1 < src.length() && src[close + 1] == ':' else {
    raise InvalidAddr
  }
  let host = src[1:close].to_owned()
  let port = parse_port(src[close + 2:])
  Addr::{ host, port, ip: 0U, is_ipv6: true, }
}

///|
pub fn Addr::new(ip : UInt, port : Int) -> Addr {
  Addr::{ host: ipv4_host_from_ip(ip), port, ip, is_ipv6: false, }
}

///|
pub fn Addr::parse(src : String) -> Addr raise InvalidAddr {
  if src.has_prefix("[") {
    parse_ipv6(src)
  } else {
    let parts = src.split(":").collect()
    guard parts.length() == 2 else { raise InvalidAddr }
    let ip = parse_ipv4_host(parts[0])
    let port = parse_port(parts[1])
    Addr::{ host: parts[0].to_owned(), port, ip, is_ipv6: false, }
  }
}

///|
pub fn Addr::ip(self : Addr) -> UInt {
  self.ip
}

///|
pub fn Addr::port(self : Addr) -> Int {
  self.port
}

///|
pub fn Addr::is_ipv6(self : Addr) -> Bool {
  self.is_ipv6
}

///|
pub fn Addr::is_multicast(self : Addr) -> Bool {
  if self.is_ipv6 {
    self.host.to_lower().has_prefix("ff")
  } else {
    let first = ((self.ip >> 24) & 0xffU).reinterpret_as_int()
    224 <= first && first <= 239
  }
}

///|
pub impl Show for Addr with fn output(self, logger) {
  if self.is_ipv6 {
    logger
    ..write_char('[')
    ..write_string(self.host)
    ..write_char(']')
    ..write_char(':')
    .write_object(self.port)
  } else {
    logger..write_string(self.host)..write_char(':').write_object(self.port)
  }
}