///|
/// A general IP address with an optional IPv6 zone identifier.
/// Legacy Address remains an unscoped numeric address for set algebra.
struct IpAddress {
  address_ : Address
  scope_ : String?
} derive(Eq)

///|
pub extend IpAddress with Eq::{equal, not_equal}

///|
pub extend IpAddress with Compare::{compare, op_lt, op_le, op_gt, op_ge}

///|
pub extend IpAddress with Hash::{hash, hash_combine}

///|
pub extend IpAddress with Show::{output}

///|
pub extend IpAddress with @debug.Debug::{to_repr}

///|
pub fn ip_address(text : String) -> IpAddress raise IpError {
  IpAddress::parse(text)
}

///|
pub fn ipv4_address(text : String) -> IpAddress raise IpError {
  let result = IpAddress::parse(text)
  if result.family() != V4 {
    raise InvalidAddress(text)
  }
  result
}

///|
pub fn ipv6_address(text : String) -> IpAddress raise IpError {
  let result = IpAddress::parse(text)
  if result.family() != V6 {
    raise InvalidAddress(text)
  }
  result
}

///|
pub fn v4_int_to_packed(value : @bigint.BigInt) -> Bytes raise IpError {
  IpAddress::from_bigint(value, family=V4).to_bytes()
}

///|
pub fn v6_int_to_packed(value : @bigint.BigInt) -> Bytes raise IpError {
  IpAddress::from_bigint(value, family=V6).to_bytes()
}

///|
pub fn IpAddress::parse(text : String) -> IpAddress raise IpError {
  if text.contains("/") {
    raise InvalidAddress(text)
  }
  match text.split_once("%") {
    None => IpAddress::from_address(Address::parse(text))
    Some((numeric, scope)) => {
      if scope.is_empty() || scope.contains("%") || !numeric.contains(":") {
        raise InvalidAddress(text)
      }
      {
        address_: Address::parse(numeric.to_owned()),
        scope_: Some(scope.to_owned()),
      }
    }
  }
}

///|
pub fn IpAddress::from_address(address : Address) -> IpAddress {
  { address_: address, scope_: None, }
}

///|
pub fn IpAddress::as_address(self : IpAddress) -> Address {
  self.address_
}

///|
pub fn IpAddress::unscoped(self : IpAddress) -> IpAddress {
  IpAddress::from_address(self.address_)
}

///|
pub fn IpAddress::from_bigint(
  value : @bigint.BigInt,
  family? : Family,
) -> IpAddress raise IpError {
  let family = family.unwrap_or_else(fn() {
    if value < 1N << 32 {
      V4
    } else {
      V6
    }
  })
  let width = if family == V4 { 32 } else { 128 }
  if value < 0N || value >= 1N << width {
    raise InvalidAddress(value.to_string())
  }
  let mask = 4294967295N
  IpAddress::from_address({
    family_: family,
    a: ((value >> 96) & mask).to_uint(),
    b: ((value >> 64) & mask).to_uint(),
    c: ((value >> 32) & mask).to_uint(),
    d: (value & mask).to_uint(),
  })
}

///|
pub fn IpAddress::from_bytes(value : Bytes) -> IpAddress raise IpError {
  let family = match value.length() {
    4 => V4
    16 => V6
    _ => raise InvalidAddress("packed address must contain 4 or 16 bytes")
  }
  IpAddress::from_bigint(@bigint.BigInt::from_octets(value[:]), family~)
}

///|
pub fn IpAddress::to_bigint(self : IpAddress) -> @bigint.BigInt {
  let a = self.address_
  (@bigint.BigInt::from_uint(a.a) << 96) |
  (@bigint.BigInt::from_uint(a.b) << 64) |
  (@bigint.BigInt::from_uint(a.c) << 32) |
  @bigint.BigInt::from_uint(a.d)
}

///|
pub fn IpAddress::to_bytes(self : IpAddress) -> Bytes {
  self.to_bigint().to_octets(length=self.max_prefixlen() / 8)
}

///|
pub fn IpAddress::family(self : IpAddress) -> Family {
  self.address_.family()
}

///|
pub fn IpAddress::version(self : IpAddress) -> Int32 {
  if self.family() == V4 {
    4
  } else {
    6
  }
}

///|
pub fn IpAddress::max_prefixlen(self : IpAddress) -> Int32 {
  self.address_.width()
}

///|
pub fn IpAddress::scope_id(self : IpAddress) -> String? {
  self.scope_
}

///|
pub fn IpAddress::to_string(self : IpAddress) -> String {
  let numeric = match self.ipv4_mapped() {
    Some(v4) => "::ffff:\{v4.to_string()}"
    None => self.address_.to_string()
  }
  match self.scope_ {
    None => numeric
    Some(scope) => "\{numeric}%\{scope}"
  }
}

///|
pub fn IpAddress::compressed(self : IpAddress) -> String {
  self.to_string()
}

///|
fn ip_padded(text : String, width : Int32) -> String {
  "0".repeat(width - text.length()) + text
}

///|
pub fn IpAddress::exploded(self : IpAddress) -> String {
  if self.family() == V4 {
    return self.to_string()
  }
  match self.ipv4_mapped() {
    Some(v4) => {
      let numeric = "0000:0000:0000:0000:0000:ffff:\{v4.to_string()}"
      return match self.scope_ {
        None => numeric
        Some(scope) => "\{numeric}%\{scope}"
      }
    }
    None => ()
  }
  let a = self.address_
  let groups = [
    a.a >> 16,
    a.a & 65535U,
    a.b >> 16,
    a.b & 65535U,
    a.c >> 16,
    a.c & 65535U,
    a.d >> 16,
    a.d & 65535U,
  ]
  let numeric = groups
    .map(fn(g) { ip_padded(g.to_string(radix=16), 4) })
    .join(":")
  match self.scope_ {
    None => numeric
    Some(scope) => "\{numeric}%\{scope}"
  }
}

///|
/// The numeric PTR name. IPv6 zone identifiers do not enter DNS names.
pub fn IpAddress::reverse_pointer(self : IpAddress) -> String {
  if self.family() == V4 {
    let word = self.address_.d
    return "\{word & 255U}.\{(word >> 8) & 255U}.\{(word >> 16) & 255U}.\{word >> 24}.in-addr.arpa"
  }
  let digits = ip_padded(self.to_bigint().to_string(radix=16), 32)
  let out = StringBuilder()
  for index = 31; index >= 0; index = index - 1 {
    out.write_substring(digits, index, 1)
    out.write_string(".")
  }
  out.write_string("ip6.arpa")
  out.to_string()
}

///|
/// String formatting uses [[fill]align][0][width][.precision]s, with <, > or ^
/// alignment, one Unicode fill character and ASCII decimal width/precision
/// up to 2147483647. Width and precision count Unicode characters.
/// Numeric b/x/X/n formatting supports optional # prefix and _ grouping.
pub fn IpAddress::format(
  self : IpAddress,
  spec : String,
) -> String raise IpError {
  if spec == "" || spec == "s" {
    return self.to_string()
  }
  if spec.has_suffix("s") {
    return ip_format_string(self.to_string(), spec)
  }
  let mut rest = spec
  let mut alternate = false
  let mut grouped = false
  if rest.has_prefix("#") {
    alternate = true
    rest = rest[1:].to_owned()
  }
  if rest.has_prefix("_") {
    grouped = true
    rest = rest[1:].to_owned()
  }
  if rest != "b" && rest != "x" && rest != "X" && rest != "n" {
    raise InvalidAddress("unsupported address format: \{spec}")
  }
  let binary = rest == "b" || (rest == "n" && self.family() == V4)
  let radix = if binary { 2 } else { 16 }
  let width = self.max_prefixlen() / (if binary { 1 } else { 4 })
  let digits = ip_padded(self.to_bigint().to_string(radix~), width)
  let digits = if rest == "X" { digits.to_upper() } else { digits }
  let out = StringBuilder()
  if alternate {
    out.write_string(
      if binary {
        "0b"
      } else if rest == "X" {
        "0X"
      } else {
        "0x"
      },
    )
  }
  for i = 0; i < digits.length(); i = i + 1 {
    if grouped && i > 0 && i % 4 == 0 {
      out.write_string("_")
    }
    out.write_substring(digits, i, 1)
  }
  out.to_string()
}

///|
fn ip_format_alignment(character : Char) -> Bool {
  character == '<' || character == '>' || character == '^'
}

///|
/// The string subset used by address formatting; precision truncates text,
/// not address bits. Numeric formatting deliberately has its own grammar.
fn ip_format_string(text : String, spec : String) -> String raise IpError {
  let characters = spec.iter().to_array()
  let end = characters.length() - 1
  let mut index = 0
  let mut fill = ' '
  let mut alignment = '<'
  let mut explicit_fill = false
  if end >= 2 && ip_format_alignment(characters[1]) {
    fill = characters[0]
    alignment = characters[1]
    explicit_fill = true
    index = 2
  } else if end >= 1 && ip_format_alignment(characters[0]) {
    alignment = characters[0]
    index = 1
  }
  if index < end && characters[index] == '0' {
    if !explicit_fill {
      fill = '0'
    }
    index = index + 1
  }
  let mut width = 0
  while index < end && characters[index] != '.' {
    let digit = characters[index].to_int() - 48
    if digit < 0 || digit > 9 || width > (2147483647 - digit) / 10 {
      raise InvalidAddress("unsupported address format: \{spec}")
    }
    width = width * 10 + digit
    index = index + 1
  }
  let mut text = text
  if index < end {
    index = index + 1
    if index == end {
      raise InvalidAddress("unsupported address format: \{spec}")
    }
    let mut precision = 0
    while index < end {
      let digit = characters[index].to_int() - 48
      if digit < 0 || digit > 9 || precision > (2147483647 - digit) / 10 {
        raise InvalidAddress("unsupported address format: \{spec}")
      }
      precision = precision * 10 + digit
      index = index + 1
    }
    text = String::from_iter(text.iter().take(precision))
  }
  let padding = width - text.char_length()
  if padding <= 0 {
    return text
  }
  if alignment == '>' {
    String::make(padding, fill) + text
  } else if alignment == '^' {
    let left = padding / 2
    String::make(left, fill) + text + String::make(padding - left, fill)
  } else {
    text + String::make(padding, fill)
  }
}

///|
pub fn IpAddress::add(
  self : IpAddress,
  delta : @bigint.BigInt,
) -> IpAddress raise IpError {
  IpAddress::from_bigint(self.to_bigint() + delta, family=self.family())
}

///|
pub fn IpAddress::subtract(
  self : IpAddress,
  delta : @bigint.BigInt,
) -> IpAddress raise IpError {
  IpAddress::from_bigint(self.to_bigint() - delta, family=self.family())
}

///|
/// A total ordering: family, numeric value, then optional zone identifier.
pub impl Compare for IpAddress with fn compare(self, other) {
  let numeric = self.address_.compare(other.address_)
  if numeric != 0 {
    return numeric
  }
  match (self.scope_, other.scope_) {
    (None, None) => 0
    (None, Some(_)) => -1
    (Some(_), None) => 1
    (Some(a), Some(b)) => a.compare(b)
  }
}

///|
pub impl Hash for IpAddress with fn hash_combine(self, hasher) {
  hasher.combine(self.version())
  hasher.combine(self.address_.a)
  hasher.combine(self.address_.b)
  hasher.combine(self.address_.c)
  hasher.combine(self.address_.d)
  hasher.combine(self.scope_)
}

///|
pub impl Show for IpAddress with fn output(self, logger) {
  logger.write_string(self.to_string())
}

///|
pub impl @debug.Debug for IpAddress with fn to_repr(self) {
  @debug.Repr(self.to_string())
}