///|
/// An IPv4 or IPv6 address, and the zone an IPv6 scoped address may carry.
///
/// The value never changes once made, compares by its bits, and can be a key in a
/// map. IPv4 keeps its 32 bits in the low word with `v4` set, so `1.2.3.4` and
/// `::1.2.3.4` are two different addresses that never compare equal.
pub struct Addr {
  hi : UInt64
  lo : UInt64
  v4 : Bool
  zone : String?
}

///|
fn make_addr(hi : UInt64, lo : UInt64, v4 : Bool, zone : String?) -> Addr {
  Addr::{ hi, lo, v4, zone, }
}

///|
/// An IPv4 address from its 32 bits held in the low word.
fn addr_of_u32(value : UInt64) -> Addr {
  make_addr(0UL, value & 0xFFFF_FFFFUL, true, None)
}

///|
/// The 32 bits of an IPv4 address.
fn u32_of(addr : Addr) -> UInt64 {
  addr.lo & 0xFFFF_FFFFUL
}

///|
/// 0.0.0.0, the address that means "this host, no address yet".
pub let unspecified4 : Addr = addr_of_u32(0UL)

///|
/// ::, the same idea in IPv6.
pub let unspecified6 : Addr = Addr::v6(0UL, 0UL)

///|
/// 127.0.0.1, the near end of the loopback range.
pub let loopback4 : Addr = addr_of_u32(0x7F00_0001UL)

///|
/// ::1, the only loopback address IPv6 has.
pub let loopback6 : Addr = Addr::v6(0UL, 1UL)

///|
/// An IPv4 address from its four octets.
pub fn Addr::v4(a : Byte, b : Byte, c : Byte, d : Byte) -> Addr {
  let value = (a.to_uint64() << 24) |
    (b.to_uint64() << 16) |
    (c.to_uint64() << 8) |
    d.to_uint64()
  addr_of_u32(value)
}

///|
/// An IPv6 address from its two halves: `hi` the top 64 bits, `lo` the bottom 64.
pub fn Addr::v6(hi : UInt64, lo : UInt64) -> Addr {
  make_addr(hi, lo, false, None)
}

///|
/// Whether this is an IPv4 address.
pub fn Addr::is_v4(self : Addr) -> Bool {
  self.v4
}

///|
/// Whether this is an IPv6 address. An IPv4-mapped address such as
/// `::ffff:192.0.2.1` is IPv6 here; `unmap` is how it becomes IPv4.
pub fn Addr::is_v6(self : Addr) -> Bool {
  !self.v4
}

///|
/// The zone (RFC 4007), when the address came with one.
pub fn Addr::zone(self : Addr) -> String? {
  self.zone
}

///|
/// The same address on another zone. A zone only means something to a scoped IPv6
/// address, so on IPv4 this returns the address unchanged.
pub fn Addr::with_zone(self : Addr, zone : String?) -> Addr {
  if self.v4 {
    self
  } else {
    make_addr(self.hi, self.lo, false, zone)
  }
}

///|
/// The next address, or `None` at the top of the family.
pub fn Addr::next(self : Addr) -> Addr? {
  if self.v4 {
    if self.lo == 0xFFFF_FFFFUL {
      None
    } else {
      Some(make_addr(0UL, self.lo + 1UL, true, self.zone))
    }
  } else if self.lo == 0xFFFF_FFFF_FFFF_FFFFUL {
    if self.hi == 0xFFFF_FFFF_FFFF_FFFFUL {
      None
    } else {
      Some(make_addr(self.hi + 1UL, 0UL, false, self.zone))
    }
  } else {
    Some(make_addr(self.hi, self.lo + 1UL, false, self.zone))
  }
}

///|
/// The address before this one, or `None` at the bottom of the family.
pub fn Addr::prev(self : Addr) -> Addr? {
  if self.v4 {
    if self.lo == 0UL {
      None
    } else {
      Some(make_addr(0UL, self.lo - 1UL, true, self.zone))
    }
  } else if self.lo == 0UL {
    if self.hi == 0UL {
      None
    } else {
      Some(make_addr(self.hi - 1UL, 0xFFFF_FFFF_FFFF_FFFFUL, false, self.zone))
    }
  } else {
    Some(make_addr(self.hi, self.lo - 1UL, false, self.zone))
  }
}

///|
/// Whether this is an IPv4-mapped IPv6 address, that is, one in `::ffff:0:0/96`.
pub fn Addr::is_v4_mapped(self : Addr) -> Bool {
  !self.v4 && self.hi == 0UL && self.lo >> 32 == 0xFFFFUL
}

///|
/// The IPv4 address inside an IPv4-mapped IPv6 one; anything else comes back
/// unchanged. The zone goes with the IPv6 form and is dropped.
pub fn Addr::unmap(self : Addr) -> Addr {
  if self.is_v4_mapped() {
    addr_of_u32(self.lo)
  } else {
    self
  }
}

///|
/// The IPv4-mapped IPv6 form of an IPv4 address; a real IPv6 address comes back
/// unchanged.
pub fn Addr::mapped(self : Addr) -> Addr {
  if self.v4 {
    make_addr(0UL, 0xFFFF_0000_0000UL | self.lo, false, None)
  } else {
    self
  }
}

///|
/// The IPv4 address carried by a 6to4 address, `2002::/16` (RFC 3056).
pub fn Addr::sixtofour(self : Addr) -> Addr? {
  if self.v4 || self.hi >> 48 != 0x2002UL {
    None
  } else {
    Some(addr_of_u32((self.hi >> 16) & 0xFFFF_FFFFUL))
  }
}

///|
/// The server and client addresses carried by a Teredo address, `2001::/32`
/// (RFC 4380). The client is stored inverted (RFC 5991), so the last 32 bits are
/// read as a complement.
pub fn Addr::teredo(self : Addr) -> (Addr, Addr)? {
  if self.v4 || self.hi >> 32 != 0x2001_0000UL {
    None
  } else {
    let server = addr_of_u32(self.hi & 0xFFFF_FFFFUL)
    let client = addr_of_u32(UInt64::lnot(self.lo) & 0xFFFF_FFFFUL)
    Some((server, client))
  }
}

///|
/// The four or sixteen bytes of the address, big-endian, the way a packet or a
/// certificate option carries them. A zone is not part of the address and is not
/// written.
pub fn Addr::encode(self : Addr) -> Bytes {
  if self.v4 {
    let out = Array::makei(4, fn(i) {
      ((self.lo >> (24 - i * 8)) & 0xFFUL).to_byte()
    })
    Bytes::from_array(out[:])
  } else {
    let out = Array::makei(16, fn(i) {
      if i < 8 {
        ((self.hi >> (56 - i * 8)) & 0xFFUL).to_byte()
      } else {
        ((self.lo >> (56 - (i - 8) * 8)) & 0xFFUL).to_byte()
      }
    })
    Bytes::from_array(out[:])
  }
}

///|
/// Read the address back from four (IPv4) or sixteen (IPv6) bytes.
pub fn Addr::decode(bytes : BytesView) -> Addr raise Refused {
  match bytes.length() {
    4 => {
      let mut lo = 0UL
      for i = 0; i < 4; i = i + 1 {
        lo = (lo << 8) | bytes.at(i).to_uint64()
      }
      addr_of_u32(lo)
    }
    16 => {
      let mut hi = 0UL
      let mut lo = 0UL
      for i = 0; i < 8; i = i + 1 {
        hi = (hi << 8) | bytes.at(i).to_uint64()
      }
      for i = 0; i < 8; i = i + 1 {
        lo = (lo << 8) | bytes.at(8 + i).to_uint64()
      }
      make_addr(hi, lo, false, None)
    }
    n => raise Refused::Length(want=16, got=n)
  }
}

///|
/// How many bits the address has: 32 for IPv4, 128 for IPv6.
pub fn Addr::bits(self : Addr) -> Int32 {
  if self.v4 {
    32
  } else {
    128
  }
}

///|
pub impl Eq for Addr with fn equal(self, other) {
  self.v4 == other.v4 &&
  self.hi == other.hi &&
  self.lo == other.lo &&
  self.zone == other.zone
}

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

///|
pub impl Hash for Addr with fn hash_combine(self, hasher) {
  Hash::hash_combine(self.v4, hasher)
  Hash::hash_combine(self.hi, hasher)
  Hash::hash_combine(self.lo, hasher)
  Hash::hash_combine(self.zone, hasher)
}

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

///|
/// The order is IPv4 before IPv6, then the address as an unsigned number, then the
/// zone. Sorting addresses by it gives the same order as sorting their encoded
/// bytes, with the two families kept apart instead of interleaved.
pub impl Compare for Addr with fn compare(self, other) {
  if self.v4 != other.v4 {
    if self.v4 {
      -1
    } else {
      1
    }
  } else if self.hi != other.hi {
    if self.hi < other.hi {
      -1
    } else {
      1
    }
  } else if self.lo != other.lo {
    if self.lo < other.lo {
      -1
    } else {
      1
    }
  } else {
    match (self.zone, other.zone) {
      (None, None) => 0
      (None, Some(_)) => -1
      (Some(_), None) => 1
      (Some(a), Some(b)) => a.compare(b)
    }
  }
}

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

///|
pub impl Debug for Addr with fn to_repr(self) {
  Repr::Repr(self.to_string())
}

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