#
#                  Chronos IP Network
#              (c) Copyright 2018-Present
#         Status Research & Development GmbH
#
#              Licensed under either of
#  Apache License, version 2.0, (LICENSE-APACHEv2)
#              MIT license (LICENSE-MIT)

## This module implements various IP network utility procedures.

{.push raises: [].}

import std/strutils
import stew/endians2
import ./common
export common

type
  IpMask* = object
    case family*: AddressFamily
    of AddressFamily.None, AddressFamily.Unix:
      discard
    of AddressFamily.IPv4:
      mask4*: uint32
    of AddressFamily.IPv6:
      mask6*: array[2, uint64]

  IpNet* = object
    host*: TransportAddress
    mask*: IpMask

proc `==`*(m1, m2: IpMask): bool {.inline.} =
  ## Returns ``true`` if masks ``m1`` and ``m2`` are equal in IP family and
  ## by value.
  if m1.family == m2.family:
    case m1.family
    of AddressFamily.IPv4:
      (m1.mask4 == m2.mask4)
    of AddressFamily.IPv6:
      ((m1.mask6[0] == m2.mask6[0]) and (m1.mask6[1] == m2.mask6[1]))
    else:
      true
  else:
    false

proc init*(t: typedesc[IpMask], family: AddressFamily, prefix: int): IpMask =
  ## Initialize mask of IP family ``family`` from prefix length ``prefix``.
  case family
  of AddressFamily.IPv4:
    if prefix <= 0:
      IpMask(family: AddressFamily.IPv4, mask4: 0'u32)
    elif prefix < 32:
      let mask = 0xFFFF_FFFF'u32 shl (32 - prefix)
      IpMask(family: AddressFamily.IPv4, mask4: mask)
    else:
      IpMask(family: AddressFamily.IPv4, mask4: 0xFFFF_FFFF'u32)
  of AddressFamily.IPv6:
    if prefix <= 0:
      IpMask(family: AddressFamily.IPv6, mask6: [0'u64, 0'u64])
    elif prefix >= 128:
      IpMask(family: AddressFamily.IPv6,
             mask6: [0xFFFF_FFFF_FFFF_FFFF'u64, 0xFFFF_FFFF_FFFF_FFFF'u64])
    else:
      if prefix > 64:
        let mask = 0xFFFF_FFFF_FFFF_FFFF'u64 shl (128 - prefix)
        IpMask(family: AddressFamily.IPv6,
               mask6: [0xFFFF_FFFF_FFFF_FFFF'u64, mask])
      elif prefix == 64:
        IpMask(family: AddressFamily.IPv6,
               mask6: [0xFFFF_FFFF_FFFF_FFFF'u64, 0'u64])
      else:
        let mask = 0xFFFF_FFFF_FFFF_FFFF'u64 shl (64 - prefix)
        IpMask(family: AddressFamily.IPv6, mask6: [mask, 0'u64])
  else:
    IpMask(family: family)

proc init*(t: typedesc[IpMask], netmask: TransportAddress): IpMask =
  ## Initialize network mask using address ``netmask``.
  case netmask.family
  of AddressFamily.IPv4:
    IpMask(family: AddressFamily.IPv4,
           mask4: uint32.fromBytesBE(netmask.address_v4))
  of AddressFamily.IPv6:
    IpMask(family: AddressFamily.IPv6,
           mask6: [
             uint64.fromBytesBE(netmask.address_v6.toOpenArray(0, 7)),
             uint64.fromBytesBE(netmask.address_v6.toOpenArray(8, 15))])
  else:
    IpMask(family: netmask.family)

proc initIp*(t: typedesc[IpMask], netmask: string): IpMask =
  ## Initialize network mask using IPv4 or IPv6 address in text representation
  ## ``netmask``.
  ##
  ## If ``netmask`` address string is invalid, result IpMask.family will be
  ## set to ``AddressFamily.None``.
  try:
    let tip = initTAddress(parseIpAddress(netmask), Port(0))
    t.init(tip)
  except ValueError:
    IpMask(family: AddressFamily.None)

proc init*(t: typedesc[IpMask], netmask: string): IpMask =
  ## Initialize network mask using hexadecimal string representation
  ## ``netmask``.
  ##
  ## If ``netmask`` mask is invalid, result IpMask.family will be set to
  ## ``AddressFamily.None``.
  const
    hexNumbers = {'0'..'9'}
    hexCapitals = {'A'..'F'}
    hexLowers = {'a'..'f'}
  let length = len(netmask)
  if length == 8 or length == (2 + 8):
    ## IPv4 mask
    var offset = 0
    if length == 2 + 8:
      offset = 2
    var res = IpMask(family: AddressFamily.IPv4)
    var r, v: uint32
    for i in 0 ..< 8:
      if netmask[offset + i] in hexNumbers:
        v = uint32(ord(netmask[offset + i]) - ord('0'))
      elif netmask[offset + i] in hexCapitals:
        v = uint32(ord(netmask[offset + i]) - ord('A') + 10)
      elif netmask[offset + i] in hexLowers:
        v = uint32(ord(netmask[offset + i]) - ord('a') + 10)
      else:
        return IpMask(family: AddressFamily.None)
      r = (r shl 4) or v
    res.mask4 = r
    res
  elif length == 32 or length == (2 + 32):
    ## IPv6 mask
    var offset = 0
    if length == 2 + 32:
      offset = 2
    var res = IpMask(family: AddressFamily.IPv6)
    for i in 0..1:
      var r, v: uint64
      for i in 0 ..< 16:
        if netmask[offset + i] in hexNumbers:
          v = uint64(ord(netmask[offset + i]) - ord('0'))
        elif netmask[offset + i] in hexCapitals:
          v = uint64(ord(netmask[offset + i]) - ord('A') + 10)
        elif netmask[offset + i] in hexLowers:
          v = uint64(ord(netmask[offset + i]) - ord('a') + 10)
        else:
          return IpMask(family: AddressFamily.None)
        r = (r shl 4) or v
      offset += 16
      res.mask6[i] = r
    res
  else:
    IpMask(family: AddressFamily.None)

proc toIPv6*(address: TransportAddress): TransportAddress =
  ## Map IPv4 ``address`` to IPv6 address.
  ##
  ## If ``address`` is IPv4 address then it will be mapped as:
  ## <80 bits of zeros> + <16 bits of ones> + <32-bit IPv4 address>.
  ##
  ## If ``address`` is IPv6 address it will be returned without any changes.
  case address.family
  of AddressFamily.IPv4:
    var address6: array[16, uint8]
    address6[10] = 0xFF'u8
    address6[11] = 0xFF'u8
    address6[12 .. 15] = toBytesBE(uint32.fromBytesBE(address.address_v4))
    TransportAddress(family: AddressFamily.IPv6, port: address.port,
                     address_v6: address6)
  of AddressFamily.IPv6:
    address
  else:
    raiseAssert "Invalid address family type"

proc isV4Mapped*(address: TransportAddress): bool =
  ## Returns ``true`` if ``address`` is (IPv4 to IPv6) mapped address, e.g.
  ## 0000:0000:0000:0000:0000:FFFF:xxxx:xxxx
  ##
  ## Procedure returns ``false`` if ``address`` family is IPv4.
  case address.family
  of AddressFamily.IPv6:
    let
      data0 = uint64.fromBytesBE(address.address_v6.toOpenArray(0, 7))
      data1 = uint16.fromBytesBE(address.address_v6.toOpenArray(8, 9))
      data2 = uint16.fromBytesBE(address.address_v6.toOpenArray(10, 11))
    (data0 == 0x00'u64) and (data1 == 0x00'u16) and (data2 == 0xFFFF'u16)
  else:
    false

proc toIPv4*(address: TransportAddress): TransportAddress =
  ## Get IPv4 from (IPv4 to IPv6) mapped address.
  ##
  ## If ``address`` is IPv4 address it will be returned without any changes.
  ##
  ## If ``address`` is not IPv4 to IPv6 mapped address, then result family will
  ## be set to AddressFamily.None.
  case address.family
  of AddressFamily.IPv4:
    address
  of AddressFamily.IPv6:
    if isV4Mapped(address):
      let data = uint32.fromBytesBE(address.address_v6.toOpenArray(12, 15))
      TransportAddress(family: AddressFamily.IPv4, port: address.port,
                       address_v4: data.toBytesBE())
    else:
      TransportAddress(family: AddressFamily.None)
  else:
    TransportAddress(family: AddressFamily.None)

proc mask*(a: TransportAddress, m: IpMask): TransportAddress =
  ## Apply IP mask ``m`` to address ``a`` and return result address.
  ##
  ## If ``a`` family is IPv4 and ``m`` family is IPv6, masking is still
  ## possible when ``m`` has ``FFFF:FFFF:FFFF:FFFF:FFFF:FFFF`` prefix. Returned
  ## value will be IPv4 address.
  ##
  ## If ``a`` family is IPv6 and ``m`` family is IPv4, masking is still
  ## possible when ``a`` holds (IPv4 to IPv6) mapped address. Returned value
  ## will be IPv6 address.
  ##
  ## If ``a`` family is IPv4 and ``m`` family is IPv4, returned value will be
  ## IPv4 address.
  ##
  ## If ``a`` family is IPv6 and ``m`` family is IPv6, returned value will be
  ## IPv6 address.
  ##
  ## In all other cases returned address will have ``AddressFamily.None``.
  if (a.family == AddressFamily.IPv4) and (m.family == AddressFamily.IPv6):
    if (m.mask6[0] == 0xFFFF_FFFF_FFFF_FFFF'u64) and
       (m.mask6[1] and 0xFFFF_FFFF_0000_0000'u64) == 0xFFFF_FFFF_0000_0000'u64:
      let
        mask = uint32(m.mask6[1] and 0xFFFF_FFFF'u64)
        data = uint32.fromBytesBE(a.address_v4)
      TransportAddress(family: AddressFamily.IPv4, port: a.port,
                       address_v4: (data and mask).toBytesBE())
    else:
      TransportAddress(family: AddressFamily.None)
  elif (a.family == AddressFamily.IPv6) and (m.family == AddressFamily.IPv4):
    var ip = a.toIPv4()
    if ip.family != AddressFamily.IPv4:
      return TransportAddress(family: AddressFamily.None)
    let data = uint32.fromBytesBE(ip.address_v4)
    ip.address_v4[0 .. 3] = (data and m.mask4).toBytesBE()
    var res = ip.toIPv6()
    res.port = a.port
    res
  elif a.family == AddressFamily.IPv4 and m.family == AddressFamily.IPv4:
    let data = uint32.fromBytesBE(a.address_v4)
    TransportAddress(family: AddressFamily.IPv4, port: a.port,
                     address_v4: (data and m.mask4).toBytesBE())
  elif a.family == AddressFamily.IPv6 and m.family == AddressFamily.IPv6:
    var address6: array[16, uint8]
    let
      data0 = uint64.fromBytesBE(a.address_v6.toOpenArray(0, 7))
      data1 = uint64.fromBytesBE(a.address_v6.toOpenArray(8, 15))
    address6[0 .. 7] = (data0 and m.mask6[0]).toBytesBE()
    address6[8 .. 15] = (data1 and m.mask6[1]).toBytesBE()
    TransportAddress(family: AddressFamily.IPv6, port: a.port,
                     address_v6: address6)
  else:
    TransportAddress(family: AddressFamily.None)

proc prefix*(mask: IpMask): int =
  ## Returns number of bits set `1` in IP mask ``mask``.
  ##
  ## Procedure returns ``-1`` if mask is not canonical, e.g. has holes with
  ## ``0`` bits between ``1`` bits.
  case mask.family
  of AddressFamily.IPv4:
    var
      res = 0
      n = mask.mask4
    while n != 0:
      if (n and 0x8000_0000'u32) == 0'u32: return -1
      n = n shl 1
      inc(res)
    res
  of AddressFamily.IPv6:
    let mask6 = [mask.mask6[0], mask.mask6[1]]
    var res = 0
    if mask6[0] == 0xFFFF_FFFF_FFFF_FFFF'u64:
      res += 64
      if mask6[1] == 0xFFFF_FFFF_FFFF_FFFF'u64:
        res + 64
      else:
        var n = mask6[1]
        while n != 0:
          if (n and 0x8000_0000_0000_0000'u64) == 0'u64: return -1
          n = n shl 1
          inc(res)
        res
    else:
      var n = mask6[0]
      while n != 0:
        if (n and 0x8000_0000_0000_0000'u64) == 0'u64: return -1
        n = n shl 1
        inc(res)
      if mask6[1] != 0x00'u64: return -1
      res
  else:
    -1

proc subnetMask*(mask: IpMask): TransportAddress =
  ## Returns TransportAddress representation of IP mask ``mask``.
  case mask.family
  of AddressFamily.IPv4:
    TransportAddress(family: AddressFamily.IPv4,
                     address_v4: mask.mask4.toBytesBE())
  of AddressFamily.IPv6:
    var address6: array[16, uint8]
    address6[0 .. 7] = mask.mask6[0].toBytesBE()
    address6[8 .. 15] = mask.mask6[1].toBytesBE()
    TransportAddress(family: AddressFamily.IPv6, address_v6: address6)
  else:
    TransportAddress(family: mask.family)

proc `$`*(mask: IpMask, include0x = false): string =
  ## Returns hexadecimal string representation of IP mask ``mask``.
  case mask.family
  of AddressFamily.IPv4:
    var
      res = if include0x: "0x" else: ""
      n = 32
      m = mask.mask4
    while n > 0:
      n -= 4
      var c = int((m shr n) and 0x0F)
      if c < 10:
        res.add(chr(ord('0') + c))
      else:
        res.add(chr(ord('A') + (c - 10)))
    res
  of AddressFamily.IPv6:
    let mask6 = [mask.mask6[0], mask.mask6[1]]
    var res = if include0x: "0x" else: ""
    for i in 0 .. 1:
      var n = 64
      var m = mask6[i]
      while n > 0:
        n -= 4
        var c = int((m shr n) and 0x0F)
        if c < 10:
          res.add(chr(ord('0') + c))
        else:
          res.add(chr(ord('A') + (c - 10)))
    res
  else:
    "Unknown mask family: " & $mask.family

proc ip*(mask: IpMask): string {.raises: [ValueError].} =
  ## Returns IP address text representation of IP mask ``mask``.
  case mask.family
  of AddressFamily.IPv4:
    $IpAddress(family: IpAddressFamily.IPv4, address_v4: mask.mask4.toBytesBE())
  of AddressFamily.Ipv6:
    var address6: array[16, uint8]
    address6[0 .. 7] = mask.mask6[0].toBytesBE()
    address6[8 .. 15] = mask.mask6[1].toBytesBE()
    $IpAddress(family: IpAddressFamily.IPv6, address_v6: address6)
  else:
    raise newException(ValueError, "Invalid mask family type")

proc init*(t: typedesc[IpNet], host: TransportAddress,
           prefix: int): IpNet {.inline.} =
  ## Initialize IP Network using host address ``host`` and prefix length
  ## ``prefix``.
  IpNet(mask: IpMask.init(host.family, prefix), host: host)

proc init*(t: typedesc[IpNet], host, mask: TransportAddress): IpNet {.inline.} =
  ## Initialize IP Network using host address ``host`` and network mask
  ## address ``mask``.
  ##
  ## Note that ``host`` and ``mask`` must be from the same IP family.
  doAssert(host.family == mask.family)
  IpNet(mask: IpMask.init(mask), host: host)

proc init*(t: typedesc[IpNet], host: TransportAddress,
           mask: IpMask): IpNet {.inline.} =
  ## Initialize IP Network using host address ``host`` and network mask
  ## ``mask``.
  IpNet(mask: mask, host: host)

proc init*(t: typedesc[IpNet], network: string): IpNet {.
    raises: [TransportAddressError].} =
  ## Initialize IP Network from string representation in format
  ## <address>/<prefix length> or <address>/<netmask address>.
  var
    parts = network.rsplit("/", maxsplit = 1)
    host, mhost: TransportAddress
    ipaddr: IpAddress
    mask: IpMask
    prefix: int
  try:
    ipaddr = parseIpAddress(parts[0])
    if ipaddr.family == IpAddressFamily.IPv4:
      host = TransportAddress(family: AddressFamily.IPv4)
      host.address_v4 = ipaddr.address_v4
      prefix = 32
    elif ipaddr.family == IpAddressFamily.IPv6:
      host = TransportAddress(family: AddressFamily.IPv6)
      host.address_v6 = ipaddr.address_v6
      prefix = 128
    if len(parts) > 1:
      try:
        prefix = parseInt(parts[1])
      except ValueError:
        prefix = -1
      if prefix == -1:
        ipaddr = parseIpAddress(parts[1])
        if ipaddr.family == IpAddressFamily.IPv4:
          mhost = TransportAddress(family: AddressFamily.IPv4)
          mhost.address_v4 = ipaddr.address_v4
        elif ipaddr.family == IpAddressFamily.IPv6:
          mhost = TransportAddress(family: AddressFamily.IPv6)
          mhost.address_v6 = ipaddr.address_v6
        mask = IpMask.init(mhost)
        if mask.family != host.family:
          raise newException(TransportAddressError,
                             "Incorrect network address!")
      else:
        if (ipaddr.family == IpAddressFamily.IPv4 and
           (prefix < 0 or prefix > 32)) or
           (ipaddr.family == IpAddressFamily.IPv6 and
           (prefix < 0 or prefix > 128)) or
           (prefix == 0 and parts[1][0] notin {'0'..'9'}): # /-0 case
          raise newException(TransportAddressError,
                             "Incorrect network address!")
    if prefix == -1:
      t.init(host, mask)
    else:
      t.init(host, prefix)
  except ValueError as exc:
    raise newException(TransportAddressError, exc.msg)

proc `==`*(n1, n2: IpNet): bool {.inline.} =
  ## Returns ``true`` if networks ``n1`` and ``n2`` are equal in IP family and
  ## by value.
  if n1.host.family != n2.host.family:
    return false
  case n1.host.family
  of AddressFamily.IPv4:
    (n1.host.address_v4 == n2.host.address_v4) and (n1.mask == n2.mask)
  of AddressFamily.IPv6:
    (n1.host.address_v6 == n2.host.address_v6) and (n1.mask == n2.mask)
  else:
    false

proc contains*(net: IpNet, address: TransportAddress): bool =
  ## Returns ``true`` if ``address`` belongs to IP Network ``net``
  if net.host.family != address.family:
    return false
  var host1 = mask(address, net.mask)
  var host2 = mask(net.host, net.mask)
  host2.port = host1.port
  host1 == host2

proc broadcast*(net: IpNet): TransportAddress =
  ## Returns broadcast address for IP Network ``net``.
  case net.host.family
  of AddressFamily.IPv4:
    let
      host = uint32.fromBytesBE(net.host.address_v4)
      mask = net.mask.mask4
    TransportAddress(family: AddressFamily.IPv4,
                     address_v4: (host or (not(mask))).toBytesBE())
  of AddressFamily.IPv6:
    var address6: array[16, uint8]
    let
      host0 = uint64.fromBytesBE(net.host.address_v6.toOpenArray(0, 7))
      host1 = uint64.fromBytesBE(net.host.address_v6.toOpenArray(8, 15))
      data0 = net.mask.mask6[0]
      data1 = net.mask.mask6[1]
    address6[0 .. 7] = (host0 or (not(data0))).toBytesBE()
    address6[8 .. 15] = (host1 or (not(data1))).toBytesBE()
    TransportAddress(family: AddressFamily.IPv6, address_v6: address6)
  else:
    TransportAddress(family: AddressFamily.None)

proc subnetMask*(net: IpNet): TransportAddress =
  ## Returns netmask address for IP Network ``net``.
  subnetMask(net.mask)

proc network*(net: IpNet): TransportAddress {.inline.} =
  ## Returns network address (host address masked with network mask) for
  ## IP Network ``net``.
  mask(net.host, net.mask)

proc `and`*(address1, address2: TransportAddress): TransportAddress =
  ## Bitwise ``and`` operation for ``address1 and address2``.
  ##
  ## Note only IPv4 and IPv6 addresses are supported. ``address1`` and
  ## ``address2`` must be in equal IP family
  doAssert(address1.family == address2.family)
  case address1.family
  of AddressFamily.IPv4:
    let
      data1 = uint32.fromBytesBE(address1.address_v4)
      data2 = uint32.fromBytesBE(address2.address_v4)
    TransportAddress(family: AddressFamily.IPv4,
                     address_v4: (data1 and data2).toBytesBE())
  of AddressFamily.IPv6:
    var address6: array[16, uint8]
    let
      data1 = uint64.fromBytesBE(address1.address_v6.toOpenArray(0, 7))
      data2 = uint64.fromBytesBE(address1.address_v6.toOpenArray(8, 15))
      data3 = uint64.fromBytesBE(address2.address_v6.toOpenArray(0, 7))
      data4 = uint64.fromBytesBE(address2.address_v6.toOpenArray(8, 15))
    address6[0 .. 7] = (data1 and data3).toBytesBE()
    address6[8 .. 15] = (data2 and data4).toBytesBE()
    TransportAddress(family: AddressFamily.IPv6, address_v6: address6)
  else:
    raiseAssert "Invalid address family type"

proc `or`*(address1, address2: TransportAddress): TransportAddress =
  ## Bitwise ``or`` operation for ``address1 or address2``.
  ##
  ## Note only IPv4 and IPv6 addresses are supported. ``address1`` and
  ## ``address2`` must be in equal IP family
  doAssert(address1.family == address2.family)
  case address1.family
  of AddressFamily.IPv4:
    let
      data1 = uint32.fromBytesBE(address1.address_v4)
      data2 = uint32.fromBytesBE(address2.address_v4)
    TransportAddress(family: AddressFamily.IPv4,
                     address_v4: (data1 or data2).toBytesBE())
  of AddressFamily.IPv6:
    var address6: array[16, uint8]
    let
      data1 = uint64.fromBytesBE(address1.address_v6.toOpenArray(0, 7))
      data2 = uint64.fromBytesBE(address1.address_v6.toOpenArray(8, 15))
      data3 = uint64.fromBytesBE(address2.address_v6.toOpenArray(0, 7))
      data4 = uint64.fromBytesBE(address2.address_v6.toOpenArray(8, 15))
    address6[0 .. 7] = (data1 or data3).toBytesBE()
    address6[8 .. 15] = (data2 or data4).toBytesBE()
    TransportAddress(family: AddressFamily.IPv6, address_v6: address6)
  else:
    raiseAssert "Invalid address family type"

proc `not`*(address: TransportAddress): TransportAddress =
  ## Bitwise ``not`` operation for ``address``.
  case address.family
  of AddressFamily.IPv4:
    let data = not(uint32.fromBytesBE(address.address_v4))
    TransportAddress(family: AddressFamily.IPv4, address_v4: data.toBytesBE())
  of AddressFamily.IPv6:
    var address6: array[16, uint8]
    let
      data1 = not(uint64.fromBytesBE(address.address_v6.toOpenArray(0, 7)))
      data2 = not(uint64.fromBytesBE(address.address_v6.toOpenArray(8, 15)))
    address6[0 .. 7] = data1.toBytesBE()
    address6[8 .. 15] = data2.toBytesBE()
    TransportAddress(family: AddressFamily.IPv6, address_v6: address6)
  else:
    address

proc `+`*(address: TransportAddress, v: int|uint): TransportAddress =
  ## Add to IPv4/IPv6 transport ``address`` integer ``v``.
  if v == 0: return address
  case address.family
  of AddressFamily.IPv4:
    let
      av = uint32.fromBytesBE(address.address_v4)
      address4 =
        when v is int:
          if v <= 0:
            # Case when v == 0 is already covered.
            let v32 = uint32(uint64(not(v) + 1) and 0xFFFF_FFFF'u64)
            (av - v32).toBytesBE()
          else:
            let v32 = uint32(uint64(v) and 0xFFFF_FFFF'u64)
            (av + v32).toBytesBE()
        else:
          let v32 = uint32(uint64(v) and 0xFFFF_FFFF'u64)
          (av + v32).toBytesBE()
    TransportAddress(family: AddressFamily.IPv4, port: address.port,
                     address_v4: address4)
  of AddressFamily.IPv6:
    let a2 = uint64.fromBytesBE(address.address_v6.toOpenArray(8, 15))
    var
      a1 = uint64.fromBytesBE(address.address_v6.toOpenArray(0, 7))
      address6: array[16, uint8]
    when v is int:
      if v <= 0:
        # Case when v == 0 is already covered
        let a3 = a2 - uint64(not(int64(v)) + 1)
        if a3 > a2: a1 = a1 - 1'u64
        address6[0 .. 7] = a1.toBytesBE()
        address6[8 .. 15] = a3.toBytesBE()
      else:
        let a3 = a2 + uint64(v)
        if a3 < a2: a1 = a1 + 1'u64
        address6[0 .. 7] = a1.toBytesBE()
        address6[8 .. 15] = a3.toBytesBE()
    else:
      # v is unsigned so it is always bigger than zero.
      let a3 = a2 + uint64(v)
      if a3 < a2: a1 = a1 + 1'u64
      address6[0 .. 7] = a1.toBytesBE()
      address6[8 .. 15] = a3.toBytesBE()

    TransportAddress(family: AddressFamily.IPv6, port: address.port,
                     address_v6: address6)
  else:
    address

proc `-`*(address: TransportAddress, v: int|uint): TransportAddress =
  ## Sub from IPv4/IPv6 transport ``address`` integer ``v``.
  if v == 0: return address
  case address.family
  of AddressFamily.IPv4:
    let
      av = uint32.fromBytesBE(address.address_v4)
      address4 =
        when v is int:
          if v <= 0:
            # Case when v == 0 is already covered.
            let v32 = uint32(uint64(not(v) + 1) and 0xFFFF_FFFF'u64)
            (av + v32).toBytesBE()
          else:
            let v32 = uint32(uint64(v) and 0xFFFF_FFFF'u64)
            (av - v32).toBytesBE()
        else:
          let v32 = uint32(uint64(v) and 0xFFFF_FFFF'u64)
          (av - v32).toBytesBE()
    TransportAddress(family: AddressFamily.IPv4, port: address.port,
                     address_v4: address4)
  of AddressFamily.IPv6:
    let a2 = uint64.fromBytesBE(address.address_v6.toOpenArray(8, 15))
    var
      a1 = uint64.fromBytesBE(address.address_v6.toOpenArray(0, 7))
      address6: array[16, uint8]
    when v is int:
      if v <= 0:
        # Case when v == 0 is already covered
        let a3 = a2 + uint64(not(int64(v)) + 1)
        if a3 < a2: a1 = a1 + 1'u64
        address6[0 .. 7] = a1.toBytesBE()
        address6[8 .. 15] = a3.toBytesBE()
      else:
        let a3 = a2 - uint64(v)
        if a3 > a2: a1 = a1 - 1'u64
        address6[0 .. 7] = a1.toBytesBE()
        address6[8 .. 15] = a3.toBytesBE()
    else:
      # v is unsigned so it is always bigger than zero.
      let a3 = a2 - uint64(v)
      if a3 > a2: a1 = a1 - 1'u64
      address6[0 .. 7] = a1.toBytesBE()
      address6[8 .. 15] = a3.toBytesBE()

    TransportAddress(family: AddressFamily.IPv6, port: address.port,
                     address_v6: address6)
  else:
    address

proc inc*(address: var TransportAddress, v: int = 1) =
  ## Increment IPv4/IPv6 transport ``address`` by integer ``v``.
  address = address + v

proc dec*(address: var TransportAddress, v: int = 1) =
  ## Decrement IPv4/IPv6 transport ``address`` by integer ``v``.
  address = address - v

proc `$`*(net: IpNet): string =
  ## Return string representation of IP network in format:
  ## <IPv4 or IPv6 address>/<prefix length>.
  var res =
    case net.host.family
    of AddressFamily.IPv4:
      $IpAddress(family: IpAddressFamily.IPv4, address_v4: net.host.address_v4)
    of AddressFamily.IPv6:
      $IpAddress(family: IpAddressFamily.IPv6, address_v6: net.host.address_v6)
    else:
      return "Invalid network address family"

  res.add("/")
  let prefix = net.mask.prefix()
  if prefix == -1:
    try:
      res.add(net.mask.ip())
    except ValueError:
      return "Invalid network mask address"
  else:
    res.add($prefix)
  res

template a4(): untyped {.dirty.} =
  address.address_v4
template a6(): untyped {.dirty.} =
  address.address_v6

proc isNone*(address: TransportAddress): bool {.inline.} =
  ## Returns ``true`` if ``address`` is not initialized yet, e.g. its ``family``
  ## field is not set or equal to ``AddressFamily.None``.
  address.family == AddressFamily.None

proc isZero*(address: TransportAddress): bool {.inline.} =
  ## Returns ``true`` if ``address`` is full of zeros, but its ``family`` is
  ## not ``AddressFamily.None``.
  case address.family
  of AddressFamily.IPv4:
    uint32.fromBytesBE(a4()) == 0'u32
  of AddressFamily.IPv6:
    (uint64.fromBytesBE(a6.toOpenArray(0, 7)) == 0'u64) and
    (uint64.fromBytesBE(a6.toOpenArray(8, 15)) == 0'u64)
  of AddressFamily.Unix:
    len($cast[cstring](unsafeAddr address.address_un[0])) == 0
  else:
    false

proc isUnspecified*(address: TransportAddress): bool {.inline.} =
  ## Alias for isZero().
  isZero(address)

proc isMulticast*(address: TransportAddress): bool =
  ## Returns ``true`` if ``address`` is a multicast address.
  ##
  ## ``IPv4``: 224.0.0.0 - 239.255.255.255
  ##
  ## ``IPv6``: FF00:: - FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF
  case address.family
  of AddressFamily.IPv4:
    (a4[0] and 0xF0'u8) == 0xE0'u8
  of AddressFamily.IPv6:
    a6[0] == 0xFF'u8
  else:
    false

proc isUnicast*(address: TransportAddress): bool =
  ## Returns ``true`` if ``address`` is unicast, as defined by [IETF RFC 4291].
  ## Any address that is not a IPv6 multicast address `FF00::/8` is unicast.
  case address.family
  of AddressFamily.IPv4:
    not(isZero(address) and not isMulticast(address))
  of AddressFamily.IPv6:
    not(isMulticast(address))
  else:
    false

proc isInterfaceLocalMulticast*(address: TransportAddress): bool =
  ## Returns ``true`` if ``address`` is interface local multicast address.
  ##
  ## ``IPv4``:  N/A (always returns ``false``)
  case address.family
  of AddressFamily.IPv4:
    false
  of AddressFamily.IPv6:
    (a6[0] == 0xFF'u8) and ((a6[1] and 0x0F'u8) == 0x01'u8)
  else:
    false

proc isLinkLocalMulticast*(address: TransportAddress): bool =
  ## Returns ``true`` if ``address`` is link local multicast address.
  ##
  ## ``IPv4``: 224.0.0.0 - 224.0.0.255
  case address.family
  of AddressFamily.IPv4:
    (a4[0] == 224'u8) and (a4[1] == 0'u8) and (a4[2] == 0'u8)
  of AddressFamily.IPv6:
    (a6[0] == 0xFF'u8) and ((a6[1] and 0x0F'u8) == 0x02'u8)
  else:
    false

proc isUniqueLocal*(address: TransportAddress): bool =
  ## Returns ``true`` if ``address`` is a unique local IPv6 unicast address.
  ##
  ## ``IPv6``: FC00::/7
  case address.family
  of AddressFamily.IPv4:
    false
  of AddressFamily.IPv6:
    (a6[0] and 0xFE'u8) == 0xFC'u8
  else:
    false

proc isUnicastLinkLocal*(address: TransportAddress): bool =
  ## Returns ``true`` if ``address`` is a IPv6 unicast address with link-local
  ## scope.
  ##
  ## NOTE: While [RFC 4291 section 2.5.3] mentions about the [loopback address]
  ## `::1` that "it is treated as having Link-Local scope", this does not mean
  ## that the loopback address actually has link-local scope and procedure
  ## will return `false` on it.
  ##
  ## ``IPv6``: FE80::/10
  case address.family
  of AddressFamily.IPv4:
    false
  of AddressFamily.IPv6:
    ((a6[0] and 0xFF'u8) == 0xFE'u8) and ((a6[1] and 0xC0'u8) == 0x80'u8)
  else:
    false

proc isLoopback*(address: TransportAddress): bool =
  ## Returns ``true`` if ``address`` is loopback address.
  ##
  ## ``IPv4``: 127.0.0.0 - 127.255.255.255
  ##
  ## ``IPv6``: ::1
  case address.family
  of AddressFamily.IPv4:
    a4[0] == 127'u8
  of AddressFamily.IPv6:
    (uint64.fromBytesBE(a6.toOpenArray(0, 7)) == 0x00'u64) and
    (uint64.fromBytesBE(a6.toOpenArray(8, 15)) == 0x01'u64)
  else:
    false

proc isAnyLocal*(address: TransportAddress): bool =
  ## Returns ``true`` if ``address`` is a wildcard address.
  ##
  ## ``IPv4``: 0.0.0.0
  ##
  ## ``IPv6``: ::
  case address.family
  of AddressFamily.IPv4:
    uint32.fromBytesBE(a4) == 0'u32
  of AddressFamily.IPv6:
    (uint64.fromBytesBE(a6.toOpenArray(0, 7)) == 0x00'u64) and
    (uint64.fromBytesBE(a6.toOpenArray(8, 15)) == 0x00'u64)
  else:
    false

proc isLinkLocal*(address: TransportAddress): bool =
  ## Returns ``true`` if ``address`` is link local address.
  ##
  ## ``IPv4``: 169.254.0.0 - 169.254.255.255
  ##
  ## ``IPv6``: FE80:: - FEBF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF
  case address.family
  of AddressFamily.IPv4:
    (a4[0] == 169'u8) and (a4[1] == 254'u8)
  of AddressFamily.IPv6:
    (a6[0] == 0xFE'u8) and ((a6[1] and 0xC0'u8) == 0x80'u8)
  else:
    false

proc isLinkLocalUnicast*(address: TransportAddress): bool {.inline.} =
  isLinkLocal(address)

proc isSiteLocal*(address: TransportAddress): bool =
  ## Returns ``true`` if ``address`` is site local address.
  ##
  ## ``IPv4``: 10.0.0.0 - 10.255.255.255, 172.16.0.0 - 172.31.255.255,
  ##           192.168.0.0 - 192.168.255.255
  ##
  ## ``IPv6``: FEC0:: - FEFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF
  case address.family
  of AddressFamily.IPv4:
    (a4[0] == 10'u8) or ((a4[0] == 172'u8) and ((a4[1] and 0xF0) == 16)) or
    ((a4[0] == 192'u8) and ((a4[1] == 168'u8)))
  of AddressFamily.IPv6:
    (a6[0] == 0xFE'u8) and ((a6[1] and 0xC0'u8) == 0xC0'u8)
  else:
    false

proc isPrivate*(address: TransportAddress): bool =
  ## Alias for ``isSiteLocal()``.
  isSiteLocal(address)

proc isGlobalMulticast*(address: TransportAddress): bool =
  ## Returns ``true`` if the multicast address has global scope.
  ##
  ## ``IPv4``: 224.0.1.0 - 238.255.255.255
  ##
  ## ``IPv6``: FF0E:: - FFFE:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF
  case address.family
  of AddressFamily.IPv4:
    (a4[0] >= 224'u8) and (a4[0] <= 238'u8) and
    not((a4[0] == 224'u8) and (a4[1] == 0'u8) and (a4[2] == 0'u8))
  of AddressFamily.IPv6:
    (a6[0] == 0xFF'u8) and ((a6[1] and 0x0F'u8) == 0x0E'u8)
  else:
    false

proc isShared*(address: TransportAddress): bool =
  ## Returns ``true`` if ``address`` is part of the Shared Address Space
  ## defined in [IETF RFC 6598]
  ##
  ## ``IPv4``: 100.64.0.0/10
  case address.family
  of AddressFamily.IPv4:
    (a4[0] == 100'u8) and ((a4[1] and 0xC0'u8) == 0x40'u8)
  of AddressFamily.IPv6:
    false
  else:
    false

proc isBroadcast*(address: TransportAddress): bool =
  ## Returns ``true`` if ``address`` is a broadcast address.
  ##
  ## ``IPv4``: 255.255.255.255
  case address.family
  of AddressFamily.IPv4:
    uint32.fromBytesBE(a4) == 0xFFFF_FFFF'u32
  of AddressFamily.IPv6:
    false
  else:
    false

proc isBenchmarking*(address: TransportAddress): bool =
  ## Returns ``true`` if ``address`` is part of the `198.18.0.0/15` range,
  ## which is reserved for network devices benchmarking. This range is defined
  ## in [IETF RFC 2544] as `198.18.0.0` through `198.19.255.255` but
  ## [errata 423] corrects it to `198.18.0.0/15`.
  ##
  ## ``IPv4``: 198.18.0.0/15
  ##
  ## ``IPv6``: 2001:2::/48
  case address.family
  of AddressFamily.IPv4:
    (a4[0] == 198'u8) and ((a4[1] and 0xFE'u8) == 18'u8)
  of AddressFamily.IPv6:
    (uint16.fromBytesBE(a6.toOpenArray(0, 1)) == 0x2001'u16) and
    (uint16.fromBytesBE(a6.toOpenArray(2, 3)) == 0x02'u16) and
    (uint16.fromBytesBE(a6.toOpenArray(4, 5)) == 0x00'u16)
  else:
    false

proc isDocumentation*(address: TransportAddress): bool =
  ## Returns ``true`` if ``address`` is in a range designated for documentation.
  ##
  ## ``IPv4``: 192.0.2.0/24 (TEST-NET-1)
  ##           198.51.100.0/24 (TEST-NET-2)
  ##           203.0.113.0/24 (TEST-NET-3)
  ##
  ## ``IPv6``: 2001:DB8::/32
  case address.family
  of AddressFamily.IPv4:
    ((a4[0] == 192'u8) and (a4[1] == 0'u8) and (a4[2] == 2'u8)) or
    ((a4[0] == 198'u8) and (a4[1] == 51'u8) and (a4[2] == 100'u8)) or
    ((a4[0] == 203'u8) and (a4[1] == 0'u8) and (a4[2] == 113'u8))
  of AddressFamily.IPv6:
    (uint16.fromBytesBE(a6.toOpenArray(0, 1)) == 0x2001'u16) and
    (uint16.fromBytesBE(a6.toOpenArray(2, 3)) == 0xDB8'u16)
  else:
    false

proc isReserved*(address: TransportAddress): bool =
  ## Returns ``true`` if ``address`` is reserved by IANA for future use.
  ## [IETF RFC 1112] defines the block of reserved addresses as `240.0.0.0/4`.
  ##
  ## NOTE: As IANA assigns new addresses, this procedure will be updated. This
  ## may result in non-reserved addresses being treated as reserved in code
  ## that relies on an outdated version of this procedure.
  ##
  ## ``IPv4``: 240.0.0.0/4
  case address.family
  of AddressFamily.IPv4:
    (a4[0] and 240'u8) == 240'u8
  of AddressFamily.IPv6:
    false
  else:
    false

proc isGlobal*(address: TransportAddress): bool =
  ## Returns ``true`` if ``address`` appears to be globally reachable as
  ## specified by the [IANA IPv4 Special-Purpose Address Registry].
  case address.family
  of AddressFamily.IPv4:
    not(
      (a4[0] == 0) or
      address.isPrivate() or
      address.isShared() or
      address.isLoopback() or
      address.isLinkLocal() or
      # address reserver for future protocols `192.0.0.0/24`.
      ((a4[0] == 192'u8) and (a4[1] == 0'u8) and (a4[2] == 0'u8)) or
      address.isDocumentation() or
      address.isBenchmarking() or
      address.isReserved() or
      address.isBroadcast()
    )
  of AddressFamily.IPv6:
    not(
      address.isUnspecified() or
      address.isLoopback() or
      (
        # IPv4-Mapped `::FFFF:0:0/96`
        (uint64.fromBytesBE(a6.toOpenArray(0, 7)) == 0x00'u64) and
        (uint16.fromBytesBE(a6.toOpenArray(8, 9)) == 0x00'u16) and
        (uint16.fromBytesBE(a6.toOpenArray(10, 11)) == 0xFFFF'u16)
      ) or
      (
        # IPv4-IPv6 Translation `64:FF9B:1::/48`
        (uint16.fromBytesBE(a6.toOpenArray(0, 1)) == 0x64'u16) and
        (uint16.fromBytesBE(a6.toOpenArray(2, 3)) == 0xFF9B'u16) and
        (uint16.fromBytesBE(a6.toOpenArray(4, 5)) == 0x01'u16)
      ) or
      (
        # Discard-Only Address Block `100::/64`
        (uint16.fromBytesBE(a6.toOpenArray(0, 1)) == 0x100'u16) and
        (uint32.fromBytesBE(a6.toOpenArray(2, 5)) == 0x00'u32) and
        (uint16.fromBytesBE(a6.toOpenArray(6, 7)) == 0x00'u16)
      ) or
      (
        # IETF Protocol Assignments `2001::/23`
        (uint16.fromBytesBE(a6.toOpenArray(0, 1)) == 0x2001'u16) and
        (uint16.fromBytesBE(a6.toOpenArray(2, 3)) < 0x200'u16) and
        not(
          (
            # Port Control Protocol Anycast `2001:1::1`
            (uint32.fromBytesBE(a6.toOpenArray(0, 3)) == 0x20010001'u32) and
            (uint32.fromBytesBE(a6.toOpenArray(4, 7)) == 0x00'u32) and
            (uint32.fromBytesBE(a6.toOpenArray(8, 11)) == 0x00'u32) and
            (uint32.fromBytesBE(a6.toOpenArray(12, 15)) == 0x01'u32)
          ) or
          (
            # Traversal Using Relays around NAT Anycast `2001:1::2`
            (uint32.fromBytesBE(a6.toOpenArray(0, 3)) == 0x20010001'u32) and
            (uint32.fromBytesBE(a6.toOpenArray(4, 7)) == 0x00'u32) and
            (uint32.fromBytesBE(a6.toOpenArray(8, 11)) == 0x00'u32) and
            (uint32.fromBytesBE(a6.toOpenArray(12, 15)) == 0x02'u32)
          ) or
          (
            # AMT `2001:3::/32`
            (uint16.fromBytesBE(a6.toOpenArray(0, 1)) == 0x2001'u16) and
            (uint16.fromBytesBE(a6.toOpenArray(2, 3)) == 0x03'u16)
          ) or
          (
            # AS112-v6 `2001:4:112::/48`
            (uint16.fromBytesBE(a6.toOpenArray(0, 1)) == 0x2001'u16) and
            (uint16.fromBytesBE(a6.toOpenArray(2, 3)) == 0x04'u16) and
            (uint16.fromBytesBE(a6.toOpenArray(4, 5)) == 0x112'u16) and
            (uint16.fromBytesBE(a6.toOpenArray(6, 7)) == 0x00'u16)
          ) or
          (
            # ORCHIDv2 `2001:20::/28`
            (uint16.fromBytesBE(a6.toOpenArray(0, 1)) == 0x2001'u16) and
            (uint16.fromBytesBE(a6.toOpenArray(2, 3)) >= 0x20'u16) and
            (uint16.fromBytesBE(a6.toOpenArray(2, 3)) <= 0x2F'u16)
          )
        )
      ) or
      address.isDocumentation() or
      address.isUniqueLocal() or
      address.isUnicastLinkLocal()
    )
  else:
    false
