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

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

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

local fld = require('aconf.model.field')
local Field = fld.Field
local Member = fld.Member
local Model = fld.Model
local normalize_name = fld.normalize_name

local node = require('aconf.model.node')
local BoundMember = node.BoundMember

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

local pth = require('aconf.path')

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


function M.to_field(obj, params, cls)
   if object.issubclass(obj, M.Model) then
      return Model(update(params, {model=obj}))
   end
   local res = object.toinstance(obj, params)
   assert(isinstance(res, cls or Field))
   return res
end


M.Action = class(Member)

function M.Action:init(params)
   super(self, M.Action):init(params)

   if not self.func then error('Function not defined for action') end

   if self.field then
      assert(type(self.field) == 'table')
      self.field = M.to_field(self.field)
      self.field.addr = node.null_addr
   end
   
   getmetatable(self).__newindex = function(t, k, v)
				      assert(k == 'name')
				      rawset(t, k, v)
				      if t.field then t.field.name = v end
				   end
end

function M.Action:meta(context)
   local res = super(self, M.Action):meta(context)
   if self.field then res.arg = self.field:meta(context) end
   return res
end


--- create a new model, representing a data model with a pre-defined
-- structure. The model's fields can be defined by assigning it
-- key&ndash;value pairs. The key will be the name of the field and
-- the value will determine its type. The value shall be a [field
-- class](#Overview_of_field_classes), an instance of field, or
-- another **&lt;Model&gt;**. The model's behavior can be customized
-- by overriding some of its [methods](#Model_objects).
-- @param base (optional <i>**&lt;Model&gt;**</i>) base model
-- inherited by the new model.
-- @return <i>**&lt;Model&gt;**</i> new model, subclass of
-- @{node.TreeNode}
function M.new(base)
   if not base then base = M.Model end

   local res = class(base)
   res._members = base == node.TreeNode and {} or copy(base._members)

   local mt = copy(getmetatable(res))

   function mt.__index(t, k) return base[k] end

   function mt.__newindex(t, k, v)
      assert(v)
      k = normalize_name(k)

      local override = t[k]
      if type(v) == 'table' then v = M.to_field(v, nil, Member) end

      rawset(t, k, v)

      if isinstance(v, Member) then
	 v.name = k
	 if not override then table.insert(t._members, k) end
      end
   end

   setmetatable(res, mt)

   if isinstance(base, M.Model) then util.setdefaults(res, base) end

   return res
end

--- Model objects. Each of these represents an actual model
-- object in the data model, in the context of a specific transaction.
-- These are instances of @{node.TreeNode} and the model class created
-- e.g. using @{new}.
-- @section Model
M.Model = M.new(node.TreeNode)

function M.Model:init(context)
   super(self, M.Model):init(context, {dtype='model', editable=true})

   local mt = getmetatable(self)

   function mt.member(name, loose, tpe)
      local m = mt.class[name]

      if not tpe then tpe = Member end
      if not isinstance(m, tpe) then m = nil end

      if m == nil then
	 if loose then return end
	 raise(mt.path, 'Does not exist: '..name)
      end

      return BoundMember(self, name, m)
   end

   local function _members(tpe)
      local res = {}
      for _, name in ipairs(self._members) do
	 local m = mt.member(name, true, tpe)
	 if m then table.insert(res, m) end
      end
      return res
   end

   function mt.topology()
      local res = {}
      for _, f in ipairs(_members(Field)) do util.extend(res, f:topology()) end
      return res
   end

   function mt.load(k, options)
      local v = mt.class[k]
      k = normalize_name(k)
      if not v then v = mt.class[k] end

      if isinstance(v, Field) then
	 return BoundMember(self, k, v):load(options)
      end

      assert(mt.txn)

      if isinstance(v, M.Action) then
	 mt.check_permission(k)

	 local f = v.field and BoundMember(self, k, v.field)
	 if options.create then return f and f:load(options) end

	 return function(var)
	    if f then f:save(var)
	    elseif var ~= nil then
	       raise(
		  pth.join(mt.path, v.name),
		  'Action does not accept an input argument'
	       )
	    end
	    local res = v.func(mt.escalate, f and f:load())
	    if f then f:_save() end
	    return res
	 end
      end

      if options.private then return v end
   end

   if self.is_removable then
      function mt.removable()
	 --- determine whether this model object can be removed from
	 -- the data model.
	 -- @function is_removable
	 -- @treturn boolean true if the object is removable
	 return mt.has_permission('delete') and self:is_removable()
      end
   end

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

   local value_removable = mt.value_removable
   function mt.value_removable(v)
      return type(v) == 'table' and value_removable(v)
   end

   if not mt.txn then return end


   function mt.mmeta(name) return mt.member(name):meta() end

   function mt.member_ui_name(name) return mt.mmeta(name)['ui-name'] end

   function mt.save(k, v)
      k = normalize_name(k)
      local field = mt.member(k, false, Field)

      if v == nil and isinstance(field, fld.TreeNode) then
	 mt.check_removable(k)
      else mt.check_permission('modify') end

      return field:save(v)
   end

   local function tmeta(tpe)
      return map(function(m) return m:meta() end, _members(tpe))
   end

   function mt.init_meta(meta)
      update(
	 meta,
	 {
	    fields=tmeta(Field),
	    actions=util.filter(
	       function(a) return mt.has_permission(a.name) end,
	       tmeta(M.Action)
	    )
	 }
      )
   end

   function mt.members()
      return map(function(f) return f.name end, tmeta(Field))
   end

   function mt.validate()
      for _, f in ipairs(_members(Field)) do
	 if self:match(f.condition or {}) then f:validate_saved()
	 elseif f:_editable() then f:_save() end
      end
      --- check the validity of the model. This method may raise an
      -- error or change the values of the model's fields. By default,
      -- this method is not implemented.
      -- @function validate
      if self.validate then self:validate() end
   end

   if self.has_permission then
      function mt._has_permission(permission)
	 return self:has_permission(mt.txn.user, permission)
      end
   end

   local tload = getmetatable(mt.escalate).load
   for _, f in ipairs(_members(Model)) do tload(f.name) end
end

--- fetch an object in the context of the model object's transaction.
-- @function fetch
-- @tparam string path path of the object to fetch. A path relative to
-- the model object's own path may be used.
-- @return (<i>**primitive**</i> or <i>@{node.TreeNode}</i>) fetched
-- object
function M.Model:fetch(path, create)
   return getmetatable(self).fetch(path, create)
end

function M.Model:search(path) return getmetatable(self).search(path) end

function M.Model:match(filter)
   local tload = getmetatable(getmetatable(self).escalate).load
   for k, v in pairs(filter) do
      if not util.contains(v, tload(k)) then return false end
   end
   return true
end


return M
