///|
/// A parsed IPv4 address stored as an unsigned 32-bit integer.
pub struct IPv4 {
  value : UInt
} derive(Eq, Debug)

///|
pub fn IPv4::new(value : UInt) -> IPv4 {
  { value, }
}

///|
pub fn IPv4::value(self : IPv4) -> UInt {
  self.value
}

///|
pub fn IPv4::parse(input : String) -> Result[IPv4, String] {
  let text = trim_ascii(input)
  let chars = text.to_array()
  guard chars.length() > 0 else { return Err("empty IPv4 address") }
  let octets : Array[Int] = []
  let mut current = 0
  let mut digits = 0
  let mut dots = 0
  for char in chars {
    if is_ascii_digit(char) {
      current = current * 10 + digit_value(char)
      digits = digits + 1
      guard digits <= 3 else {
        return Err("IPv4 octet has too many digits: " + input)
      }
      guard current <= 255 else {
        return Err("IPv4 octet is greater than 255: " + input)
      }
    } else if char == '.' {
      guard digits > 0 else {
        return Err("IPv4 dot is not preceded by an octet: " + input)
      }
      octets.push(current)
      current = 0
      digits = 0
      dots = dots + 1
      guard dots <= 3 else {
        return Err("IPv4 address has too many dots: " + input)
      }
    } else {
      return Err("IPv4 address contains an invalid character: " + input)
    }
  }
  guard digits > 0 else {
    return Err("IPv4 address ends after a dot: " + input)
  }
  octets.push(current)
  guard octets.length() == 4 else {
    return Err("IPv4 address must contain four octets: " + input)
  }
  let value = (octets[0].reinterpret_as_uint() << 24) |
    (octets[1].reinterpret_as_uint() << 16) |
    (octets[2].reinterpret_as_uint() << 8) |
    octets[3].reinterpret_as_uint()
  Ok(IPv4::new(value))
}

///|
pub fn IPv4::to_dotted(self : IPv4) -> String {
  let a = ((self.value >> 24) & 255U).reinterpret_as_int()
  let b = ((self.value >> 16) & 255U).reinterpret_as_int()
  let c = ((self.value >> 8) & 255U).reinterpret_as_int()
  let d = (self.value & 255U).reinterpret_as_int()
  "\{a}.\{b}.\{c}.\{d}"
}

///|
pub fn IPv4::is_private(self : IPv4) -> Bool {
  CidrBlock::new_unchecked(IPv4::new(0x0a000000U), 8).contains_ip(self) ||
  CidrBlock::new_unchecked(IPv4::new(0xac100000U), 12).contains_ip(self) ||
  CidrBlock::new_unchecked(IPv4::new(0xc0a80000U), 16).contains_ip(self)
}

///|
pub fn IPv4::is_loopback(self : IPv4) -> Bool {
  CidrBlock::new_unchecked(IPv4::new(0x7f000000U), 8).contains_ip(self)
}

///|
pub fn IPv4::is_link_local(self : IPv4) -> Bool {
  CidrBlock::new_unchecked(IPv4::new(0xa9fe0000U), 16).contains_ip(self)
}

///|
pub fn IPv4::is_multicast(self : IPv4) -> Bool {
  CidrBlock::new_unchecked(IPv4::new(0xe0000000U), 4).contains_ip(self)
}

///|
pub fn IPv4::is_unspecified(self : IPv4) -> Bool {
  self.value == 0U
}

///|
pub fn IPv4::is_limited_broadcast(self : IPv4) -> Bool {
  self.value == 0xffffffffU
}

///|
/// Returns true when the address is not one of the private or special scopes
/// currently recognized by this package.
pub fn IPv4::is_public(self : IPv4) -> Bool {
  !self.is_unspecified() &&
  !self.is_limited_broadcast() &&
  !self.is_loopback() &&
  !self.is_private() &&
  !self.is_link_local() &&
  !self.is_multicast()
}

///|
pub fn IPv4::scope_label(self : IPv4) -> String {
  if self.is_unspecified() {
    "unspecified"
  } else if self.is_limited_broadcast() {
    "limited_broadcast"
  } else if self.is_loopback() {
    "loopback"
  } else if self.is_private() {
    "private"
  } else if self.is_link_local() {
    "link_local"
  } else if self.is_multicast() {
    "multicast"
  } else {
    "public"
  }
}

///|
fn is_ascii_digit(char : Char) -> Bool {
  let code = char.to_int()
  code >= '0' && code <= '9'
}

///|
fn digit_value(char : Char) -> Int {
  char.to_int() - '0'
}

///|
fn is_ascii_space(char : Char) -> Bool {
  char == ' ' || char == '\t' || char == '\r' || char == '\n'
}

///|
fn trim_ascii(input : String) -> String {
  let chars = input.to_array()
  let mut start = 0
  let mut end = chars.length()
  while start < end && is_ascii_space(chars[start]) {
    start = start + 1
  }
  while end > start && is_ascii_space(chars[end - 1]) {
    end = end - 1
  }
  String::from_array(chars[start:end])
}