--
-- (C) 2017-24 - ntop.org
--

local ipv4_utils = {}

local function ip_to_bit(ip)
  local x = 0
  local parts = string.split(ip, "%.")

  for i=0,3 do
    x = (x | (tonumber(parts[i+1]) << ((3-i)*8)))
  end

  return x
end

local function bit_to_ip(bits)
  local parts = {}

  for i=0, 3 do
    parts[i+1] = ((bits >> ((3-i)*8)) & 0xFF)
  end

  return table.concat(parts, ".")
end

--
-- Get the number of bits allocated for the network specified by the ip netmask
-- nil is returned on error
function ipv4_utils.netmask(ip)
  if not ipv4_utils.valid(ip) then
    -- bad netmask
    return nil
  end

  local parts = string.split(ip, "%.")
  local netmask = 0

  for i=1,4 do
    local val = tonumber(parts[i])
    local x = nil

    if val == 255 then x = 8
    elseif val == 254 then x = 7
    elseif val == 252 then x = 6
    elseif val == 248 then x = 5
    elseif val == 240 then x = 4
    elseif val == 224 then x = 3
    elseif val == 192 then x = 2
    elseif val == 128 then x = 1
    elseif val == 0 then x = 0
    end

    if x == nil then
      -- bad netmask
      return nil
    end

    netmask = netmask + x

    if val ~= 255 then
      for j=i+1,4 do
        if tonumber(parts[j]) ~= 0 then
          -- bad netmask
          return nil
        end
      end

      break
    end
  end

  return netmask
end

function ipv4_utils.cidr_2_addr(cidr)
  local parts = split(cidr, "/")

  if #parts ~= 2 then
    return nil
  end

  local addr = parts[1]
  local num_netmask = tonumber(parts[2])
  local netmask = 0x100000000 - (1 << (32-num_netmask))

  return addr, bit_to_ip(netmask)
end

--
-- Compares IPv4 addresses
-- Precondition: a and b are valid ipv4 addresses
-- Returns:
--    >0 if a > b
--    <0 if a < b
--    0 if a == b
--
function ipv4_utils.cmp(a, b)
   local a_parts = string.split(a, "%.")
   local b_parts = string.split(b, "%.")

   for i=1,4 do
      local a_part = tonumber(a_parts[i])
      local b_part = tonumber(b_parts[i])

      if a_part > b_part then
         return 1
      elseif a_part < b_part then
         return -1
      end
   end

   return 0
end

-- Mask an IPv4 address with a netmask
-- Example: 192.168.33.1 / 255.255.254.0 -> 192.168.32.0
function ipv4_utils.maskIp(ip, netmask)
   local ip_parts = string.split(ip, "%.")
   local netmask_parts = string.split(netmask, "%.")
   local r = {}

   for i=1,4 do
      local ip_part = tonumber(ip_parts[i])
      local netmask_part = tonumber(netmask_parts[i])
      r[i] = tostring(ip_part & netmask_part)
   end

   return r[1].."."..r[2].."."..r[3].."."..r[4]
end

-- Assumption: 32bit integer are used. This is already assumed by http://bitop.luajit.org/semantics.html
function ipv4_utils.ipToInt(ip)
  local intrepr = 0

  for byte in string.gmatch(ip, "%d+") do
    intrepr = intrepr * 256 + byte
  end

  return intrepr
end

function ipv4_utils.intToIp(int)
  local parts = {
    ((int >> 24) & 0xFF),
    ((int >> 16) & 0xFF),
    ((int >> 8) & 0xFF),
    (int & 0xFF)
  }

  return table.concat(parts, ".")
end

-- Returns possible DHCP range
function ipv4_utils.get_possible_dhcp_range(ip, network, broadcast_addr)
  local ip_int = ipv4_utils.ipToInt(ip)
  local broadcast_int = ipv4_utils.ipToInt(broadcast_addr)
  local first_ip
  local last_ip

  if ip_int >= broadcast_int - 1 then
    first_ip = ipv4_utils.ipToInt(network) + 1
    last_ip = ip_int - 1
  else
    first_ip = ip_int + 1
    last_ip = broadcast_int - 1
  end

  return {
    first_ip = ipv4_utils.intToIp(first_ip),
    last_ip = ipv4_utils.intToIp(last_ip),
  }
end

function ipv4_utils.includes(network, netmask, ip)
  local lower = network
  local upper = ipv4_utils.broadcast_address(network, netmask)

  return (ipv4_utils.cmp(ip, lower) >= 0) and
         (ipv4_utils.cmp(ip, upper) <= 0)
end

-- Get the broadcast address for the given netmask
function ipv4_utils.broadcast_address(ip, netmask)
  local ipbit = ip_to_bit(ip)
  local maskbit = ip_to_bit(netmask)

  return bit_to_ip((~(maskbit) | ipbit))
end

-- https://stackoverflow.com/questions/10975935/lua-function-check-if-ipv4-or-ipv6-or-string
function ipv4_utils.valid(address)
   local chunks = {address:match("(%d+)%.(%d+)%.(%d+)%.(%d+)$")}

  if #chunks == 4 then
    for _, v in pairs(chunks) do
      if (tonumber(v) < 0) or (tonumber(v) > 255) then
        return false
      end
    end

    return true
  end
end

function ipv4_utils.addressToNetwork(address, netmask)
  local mask = ipv4_utils.netmask(netmask)
  return ntop.networkPrefix(address, mask) .. "/" .. mask
end

return ipv4_utils
