--[[
Copyright (c) 2012-2016 Kaarle Ritvanen
See LICENSE file for license details
--]]

--- @module aconf.model
local M = {}

local raise = require('aconf.error').raise
local Union = require('aconf.model.combination').Union

local fld = require('aconf.model.field')
local String = fld.String

local object = require('aconf.object')
local class = object.class
local super = object.super

local pth = require('aconf.path')
local update = require('aconf.util').update


local stringy = require('stringy')


local BaseIPAddress = class(String)

function BaseIPAddress:abs_mask_addr(context)
   if self.mask_addr then
      return pth.join(pth.parent(context.addr), self.mask_addr)
   end
end

function BaseIPAddress:topology(context)
   local res = super(self, BaseIPAddress):topology(context)
   local maddr = self:abs_mask_addr(context)
   if maddr then
      table.insert(res, {path=context.path, addr=maddr, type=self.mask_type})
   end
   return res
end

function BaseIPAddress:invalid(context)
   raise(context.path, 'Invalid IPv'..self.version..' address')
end

function BaseIPAddress:split(context, value)
   local comps = stringy.split(value, '/')
   if #comps == 1 then return value, self.length end

   if #comps > 2 or not self.cidr then self:invalid(context) end

   local mask = tonumber(comps[2])
   if not mask or mask < 0 or mask > self.length then self:invalid(context) end
   return comps[1], mask
end

function BaseIPAddress:decode(context, value)
   local maddr = self:abs_mask_addr(context)
   if value and maddr then
      return value..'/'..(self:mask2cidr(context.txn:get(maddr)) or self.length)
   end
   return value
end

function BaseIPAddress:encode(context, value)
   local maddr = self:abs_mask_addr(context)
   if maddr then
      local cidr
      if value then value, cidr = self:split(context, value) end
      context.txn:set(maddr, cidr and self:cidr2mask(cidr))
   end
   return value
end


--- IPv4 address field, inherits @{String}.
-- @fclass net.IPv4Address
-- @tparam boolean cidr if set to true, the field accepts a network
-- address in CIDR notation
-- @tparam string mask_addr if set, the network address is decomposed
-- into address and netmask in the back-end. This parameter specifies
-- the back-end address where the netmask is stored in dotted-quad
-- format.
M.IPv4Address = class(BaseIPAddress)

function M.IPv4Address:init(params)
   super(self, M.IPv4Address):init(
      update(params, {version=4, length=32, mask_type='string'})
   )
end

function M.IPv4Address:validate(context, value)
   super(self, M.IPv4Address):validate(context, value)
   local address = self:split(context, value)

   local function test(...)
      if #{...} ~= 4 then return true end
      for _, octet in ipairs{...} do
	 if tonumber(octet) > 255 then return true end
      end
   end
   if test(address:match('^(%d+)%.(%d+)%.(%d+)%.(%d+)$')) then
      self:invalid(context)
   end
end

function M.IPv4Address:mask2cidr(mask)
   local acc = 0
   for i, comp in ipairs(stringy.split(mask, '.')) do
      acc = acc + math.pow(256, 4 - i) * tonumber(comp)
   end
   local res = 32
   while acc % 2 == 0 do
      res = res - 1
      assert(res > -1)
      acc = acc / 2
   end
   return res
end

function M.IPv4Address:cidr2mask(cidr)
   local acc = (math.pow(2, cidr) - 1) * math.pow(2, 32 - cidr)
   local comps = {}
   for i = 4,1,-1 do
      comps[i] = acc % 256
      acc = math.floor(acc / 256)
   end
   return table.concat(comps, '.')
end


--- IPv6 address field, inherits @{String}.
-- @fclass net.IPv6Address
-- @tparam boolean cidr if set to true, the field accepts a network
-- address
-- @tparam string mask_addr if set, the network address is decomposed
-- into address and netmask parts in the back-end. This parameter
-- specifies the back-end address where the netmask length is stored.
M.IPv6Address = class(BaseIPAddress)

function M.IPv6Address:init(params)
   super(self, M.IPv6Address):init(
      update(params, {version=6, length=128, mask_type='number'})
   )
end

function M.IPv6Address:validate(context, value)
   super(self, M.IPv6Address):validate(context, value)
   local address = self:split(context, value)

   local function invalid() self:invalid(context) end

   if address == '' then invalid() end

   local comps = stringy.split(address, ':')
   if #comps < 3 then invalid() end

   local function collapse(i, ofs)
      if comps[i] > '' then return end
      if comps[i + ofs] > '' then invalid() end
      table.remove(comps, i)
   end
   collapse(1, 1)
   collapse(#comps, -1)
   if #comps > 8 then invalid() end

   local short = false
   for _, comp in ipairs(comps) do
      if comp == '' then
	 if short then invalid() end
	 short = true
      elseif not comp:match('^%x%x?%x?%x?$') then invalid() end
   end
   if (
      short and #comps == 3 and comps[2] == ''
   ) or (not short and #comps < 8) then
      invalid()
   end
end

function M.IPv6Address:mask2cidr(mask) return mask end

function M.IPv6Address:cidr2mask(cidr) return cidr end


--- IP address field, inherits @{String}. Accepts both IPv4 and IPv6
-- address values.
-- @fclass net.IPAddress
M.IPAddress = class(Union)

function M.IPAddress:init(params)
   super(self, M.IPAddress):init(
      update(
	 params,
	 {types={M.IPv4Address, M.IPv6Address}, error='Invalid IP address'}
      )
   )
end


--- TCP or UDP port field, inherits @{Integer}. The value can be
-- between 0 and 65535.
-- @fclass net.Port
M.Port = class(fld.Integer)

function M.Port:validate(context, value)
   super(self, M.Port):validate(context, value)
   if value < 0 or value > 65535 then raise(context.path, 'Invalid port') end
end


local domain_pattern = '[A-Za-z%d%.%-]+%.[A-Za-z][A-Za-z]+'

--- domain name field, inherits @{String}.
-- @fclass net.DomainName
M.DomainName = class(String)
function M.DomainName:init(params)
   super(self, M.DomainName):init(update(params, {pattern=domain_pattern}))
end

M.EmailAddress = class(String)
function M.EmailAddress:init(params)
   super(self, M.EmailAddress):init(
      update(params, {pattern='[A-Za-z%d%.%+%-]+@'..domain_pattern})
   )
end


return M
