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

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

local err = require('aconf.error')
local raise = err.raise

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

local pth = require('aconf.path')

local util = require('aconf.util')
local copy = util.copy
local setdefaults = util.setdefaults
local update = util.update


function M.null_addr(path, name)
   local comps = pth.split(path)
   table.insert(comps, pth.escape(name))
   return pth.join('/null', table.unpack(comps))
end


M.BoundMember = class()

function M.BoundMember:init(parent, name, field)
   local pmt = getmetatable(parent)

   if pmt.maddr and name ~= pth.wildcard then self.addr = pmt.maddr(name)
   else
      self.addr = field.addr or pth.escape(name)
      if type(self.addr) == 'function' then
	 self.addr = self.addr(pmt.path, name)
      end
      self.addr = pth.to_absolute(self.addr, pmt.addr)
   end

   --- get parent object of an object.
   -- @function node.parent
   -- @tparam node.TreeNode node object
   -- @treturn node.TreeNode parent object
   local context = {
      txn=pmt.txn,
      privileged=pmt.privileged,
      parent=parent,
      path=pth.join(pmt.path, name),
      addr=self.addr
   }

   local mt = {}

   function mt.__index(t, k)
      local member = field[k]
      if type(member) ~= 'function' then return member end
      return function(self, ...) return member(field, context, ...) end
   end

   setmetatable(self, mt)
end


--- non-leaf object. An instance of this class represents
-- a non-leaf object in the data model, in the context of a specific
-- transaction. These instances appear as Lua tables, such that their
-- children may be read and written by using their relative path name
-- as the key. If the child is of complex type, it is possible to
-- assign a structure of Lua tables as the value. This results in
-- replacing the entire subtree with the provided sturcture. Providing
-- a string in lieu of a complex value is handled by interpreting the
-- string as a path and copying the subtree it refers to. Comparison
-- of these instances with the == operator returns true if the
-- instances represent the same object.
-- @klass node.TreeNode
M.TreeNode = class()

local function equal_tns(tn1, tn2)
   return getmetatable(tn1).path == getmetatable(tn2).path
end

function M.TreeNode:init(context, params)
   local mt = getmetatable(self)
   update(mt, context)

   --- get the name of the object, i.e. the last component of its
   -- absolute path.
   -- @function node.name
   -- @tparam node.TreeNode node object
   -- @treturn string name
   mt.name = pth.name(mt.path)

   mt.__eq = equal_tns
   mt.__pairs = M.pairs

   if not (mt.txn and mt.txn.user) then mt.privileged = true end
   mt.escalate = mt.privileged and self or mt.class(
      setdefaults(
	 {
	    parent=mt.parent and getmetatable(mt.parent).escalate,
	    privileged=true
	 },
	 context
      ),
      params
   )

   function mt.get(k, options) return mt.load(k, options) end


   function mt._fetch(path, create)
      if #path == 0 then return self end

      local name = path[1]
      table.remove(path, 1)

      if name == pth.up then
	 if not mt.parent then
	    raise(mt.path, 'Root object does not have parent')
	 end
	 return getmetatable(mt.parent)._fetch(path, create)
      end

      local options = {}
      if create then
	 options.create = true
	 if #path == 0 then options.dereference = false end
      end
      local next = mt.get(name, options)
      if next == nil and (not create or #path > 0) then
	 raise(mt.path, 'Subordinate does not exist: '..name)
      end
      if #path == 0 then return next end

      if type(next) ~= 'table' then
	 raise(pth.join(mt.path, name), 'Is a primitive value')
      end

      return getmetatable(next)._fetch(path, create)
   end

   function mt.fetch(path, create)
      if pth.is_absolute(path) and mt.path > '/' then
	 assert(not create)
	 return mt.txn:fetch(path, mt.privileged)
      end

      return mt._fetch(pth.split(path), create)
   end


   function mt._search(path, prefix)
      if #path == 0 then return {{path=prefix, value=self}} end

      local mt = getmetatable(self)

      local name = path[1]
      table.remove(path, 1)

      local function collect(name)
	 local next = mt.load(name, {dereference=false})
	 if not next then return {} end

	 if isinstance(next, M.TreeNode) then
	    return getmetatable(next)._search(
	       copy(path), pth.join(prefix, name)
	    )
	 end

	 assert(#path == 0)
	 return {
	    {
	       value=next,
	       deleted=function(path) mt.member(name):deleted(path) end
	    }
	 }
      end

      if name == pth.wildcard then
	 local res = {}
	 for _, member in ipairs(mt.members()) do
	    util.extend(res, collect(member))
	 end
	 return res
      end

      return collect(name)
   end

   function mt.search(path) return mt._search(pth.split(path), '') end


   function mt._has_permission(permission) end

   -- TODO audit trail
   function mt.has_permission(permission)
      if mt.privileged then return true end

      local user = mt.txn.user
      local res = user.superuser or mt._has_permission(permission)
      if res ~= nil then return res end

      res = mt.txn.user:check_permission(permission..mt.path)
      if res ~= nil then return res end

      if ({create=true, delete=true})[permission] then
	 permission = 'modify'
      end
      return getmetatable(mt.parent).has_permission(permission)
   end

   function mt.check_permission(permission)
      if not mt.has_permission(permission) then
	 raise('forbidden', permission..mt.path)
      end
   end


   function mt.removable() end

   function mt.value_removable(v)
      if isinstance(v, M.TreeNode) then return getmetatable(v).removable() end
   end

   local function key_removable(k)
      if not mt.removing_permitted() then return false end

      local res = mt.value_removable(mt.load(k, {dereference=false}))
      if res == nil then return params.editable end
      return res
   end

   function mt.check_removable(k)
      if not (mt.privileged or key_removable(k)) then
	 raise(pth.join(mt.path, k), 'Cannot be deleted')
      end
   end

   function mt.meta()
      if not mt._meta then
	 mt._meta = {type=params.dtype, ['ui-name']=mt.ui_name}
	 if mt.txn then mt.init_meta(mt._meta) end
      end

      local res = copy(mt._meta)
      res.removable = {}
      for _, key in ipairs(mt.members()) do
	 if key_removable(key) then table.insert(res.removable, key) end
      end
      return res
   end

   if not mt.txn then return end

   mt.ui_name = mt.parent and getmetatable(mt.parent).member_ui_name(mt.name)

   function mt.save(k, v) rawset(self, k, v) end
   function mt.__index(t, k) return mt.get(k, {private=true}) end
   function mt.__newindex(t, k, v) mt.save(k, v) end

   mt.txn.validable[mt.path] = mt.addr
end


--- collection object, inherits @{node.TreeNode}. An instance of this class
-- represents a collection object in the data model, in the context of
-- a specific transaction. The path of a collection member is that of
-- the collection augmented by the respective key. The size of the
-- collection can be queried using the # operator.
-- @klass node.Collection
M.Collection = class(M.TreeNode)

function M.Collection:init(context, params)
   super(self, M.Collection):init(
      context, setdefaults(params, {dtype='collection'})
   )

   self.init = nil

   local mt = getmetatable(self)
   mt.field = M.BoundMember(self, pth.wildcard, params.field)

   function mt.topology() return mt.field:topology() end

   function mt.member(name)
      return M.BoundMember(self, name, params.field)
   end

   function mt.load(k, options) return mt.member(k):load(options) end

   function mt.removing_permitted() return mt.has_permission('delete') end

   if not mt.txn then return end

   mt.ui_member = params.ui_member or mt.ui_name:gsub('s$', '')

   function mt.member_ui_name(name)
      return name ~= pth.wildcard and mt.ui_member..' '..name or nil
   end

   function mt.mmeta(name)
      return update(mt.field:meta(), {['ui-name']=mt.member_ui_name(name)})
   end

   function mt.init_meta(meta)
      update(
	 meta,
	 {
	    editable=params.editable and mt.has_permission('create'),
	    members=mt.field:meta(),
	    required=params.required,
	    ['ui-member']=mt.ui_member,
	    widget=params.layout
	 }
      )
   end

   local function validate_key(k)
      if params.key then
	 local kf = M.BoundMember(self, k, object.toinstance(params.key))
	 if kf:normalize(k) ~= k then
	    raise(mt.path, 'Invalid member name: '..k)
	 end
	 kf:validate(k)
      end
   end

   function mt.members()
      return util.filter(
	 function(k) return err.call(validate_key, k) end,
	 mt.txn:get(mt.addr) or {}
      )
   end

   function mt.__len() return #mt.members() end

   function mt.validate()
      if #mt.members() > 0 then return end
      if params.required then raise(mt.path, 'Collection cannot be empty') end
      if params.destroy then
	 mt.txn:set(mt.addr)
	 validate(mt.parent)
      end
   end

   function mt.save(k, v)
      if not mt.privileged then
	 local delete = v == nil
	 local old = mt.load(k, {dereference=false})

	 if old == nil then
	    if delete then return end
	    if not params.editable then
	       raise(mt.path, 'Collection is not editable')
	    end
	    mt.check_permission('create')

	 elseif delete then mt.check_removable(k)

	 elseif type(old) == 'table' then
	    mt.check_removable(k)
	    mt.check_permission('create')

	 else mt.check_permission('modify') end
      end

      validate_key(k)

      mt.member(k):save(v)
   end
end


--- list object, inherits @{node.Collection}. An instance of this
-- class represents a list object in the data model, in the context of
-- a specific transaction. A list is an ordered collection of
-- members. The path of a list member is that of the list augmented by
-- the index of the member.
-- @klass node.List
M.List = class(M.Collection)

local function _ipairs(mt, i)
   i = i + 1
   local v = mt.load(i, {create=false})
   if v == nil then return end
   return i, v
end

function M.List:init(context, params)
   super(self, M.List):init(context, setdefaults(params, {dtype='list'}))

   local mt = getmetatable(self)
   if not mt.txn then return end

   local function expand() return mt.txn:expand(mt.field.addr) end

   function mt.maddr(i)
      local addrs = expand()

      if not addrs[1] then
	 addrs[1] = pth.join(
	    '/',
	    table.unpack(
	       util.map(
		  function(c) return c == pth.wildcard and 1 or c end,
		  pth.split(mt.field.addr)
	       )
	    )
	 )
      end

      if addrs[i] then return addrs[i] end

      local comps = pth.split(addrs[#addrs])
      comps[#comps] = comps[#comps] + i - #addrs
      return pth.join('/', table.unpack(comps))
   end

   function mt.members() return util.keys(expand()) end

   local tmt = getmetatable(mt.escalate)
   mt._save = mt.save

   local function check_index(i, max)
      if type(i) ~= 'number' or math.floor(i) ~= i or i < 1 or i > max then
	 raise(mt.path, 'Invalid list index: '..i)
      end
   end

   function mt.save(k, v)
      local len = #mt.members()

      if v == nil then
	 check_index(k, len)
	 mt.check_removable(k)
	 while k < len do
	    tmt._save(k, tmt.load(k + 1, {dereference=false}))
	    k = k + 1
	 end
	 tmt._save(len)

      else
	 check_index(k, len + 1)
	 mt._save(k, v)
      end
   end

   --- inserts a member to a list or set
   -- @function node.insert
   -- @tparam node.List list list or set
   -- @param v value to be inserted
   -- @tparam ?int i index. By default, the element is inserted at the
   -- end of the list. This parameter is not applicable when inserting
   -- to a @{node.Set} instance.
   function mt.insert(v, i)
      assert(v ~= nil)

      local len = #mt.members()
      local max = len + 1
      if i then check_index(i, max)
      else i = max end

      mt.check_permission('create')

      for j = len,i,-1 do
	 tmt._save(j + 1, tmt.load(j, {dereference=false}))
      end
      tmt._save(i, v)
   end

   function mt.__ipairs(t) return _ipairs, mt, 0 end
end


-- experimental
M.Mixed = class(M.Collection)

function M.Mixed:init(context, params)
   super(self, M.Mixed):init(context, params)

   -- TODO dynamic meta: list non-leaf children
   local mt = getmetatable(self)
   mt.meta = {type='mixed', ['ui-name']=mt.path}
   function mt.mmeta(name)
      return {type='mixed', ['ui-name']=pth.join(mt.path, name)}
   end
end


local function meta_func(attr)
   return function(node, ...)
	     local res = getmetatable(node)[attr]
	     if type(res) == 'function' then return res(...) end
	     return res
	  end
end

for _, mf in ipairs{
   'addr',
   'check_permission',
   'contains',
   'escalate',
   'fetch',
   'has_permission',
   'insert',
   'meta',
   'mmeta',
   'name',
   'parent',
   'path',
   'save',
   'search',
   'topology'
} do M[mf] = meta_func(mf) end


function M.pairs(tbl, dereference)
   if not isinstance(tbl, M.TreeNode) then return pairs(tbl) end

   local mt = getmetatable(tbl)

   local res = {}
   for _, member in ipairs(mt.members()) do
      res[member] = mt.load(member, {dereference=dereference})
   end
   return pairs(res)
end


return M
