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

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

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

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

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

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


M.Member = class()

function M.Member:init(params)
   for k, v in pairs(params or {}) do
      if self[k] == nil then self[k] = v end
   end
end

function M.Member:auto_ui_name(name)
   if not name then return end
   if type(name) ~= 'string' then return tostring(name) end
   local res = (name:sub(1, 1):upper()..name:sub(2)):gsub('-', ' ')
   return res
end

function M.Member:meta(context)
   return {
      name=self.name,
      description=self.description,
      ['ui-name']=self.ui_name or self:auto_ui_name(self.name)
   }
end


function M.normalize_name(name) return name:gsub('_', '-') end

function M.conv_filter(filter)
   if not filter then return end
   local res = {}
   for k, v in pairs(filter) do
      res[M.normalize_name(k)] = type(v) == 'table' and v or {v}
   end
   return res
end



--- base class for fields. The constructor accepts a table argument
-- containing field parameters as key&ndash;value pairs. The parameters
-- listed below are valid for all subclasses. Subclasses may define
-- additional parameters.
-- @klass Field
-- @tparam ?string addr back-end address for the field. This can be an
-- absolute address or relative to the parent's address. The top-level
-- component of a back-end address specifies the back-end. The
-- interpretation of the remaining components is specific to the
-- back-end. If not specified, the address is formed by appending the
-- field's name to the address of the parent.
-- @param be_mode (optional <i>**string**</i> or
-- <i>**{[string]=string,...}**</i>) controls how the Augeas back-end
-- will map addresses to Augeas paths. By default, each component of a
-- back-end address is directly mapped to an Augeas path
-- component. This parameter is an exception table applicable to the
-- subtree defined by the field's address. Each key is a relative
-- address pattern, and the corresponding value is a directive applied
-- to matching address components. The *enumerate* directive indicates
-- there can be several Augeas nodes matching the path and the next
-- component is to be interpreted as an index for such nodes. The
-- *parent-value* directive is applicable only to primitive fields and
-- instructs the back-end not to append the last address component at
-- all, causing the parent node's value to be accessed. If the
-- *be\_mode* parameter is defined as a string, it is assumed to be a
-- directive applicable to the field's own address.
-- @param choice (optional <i>**{primitive**</i> or
-- <i>**{primitive,string},...}**</i>) array of allowed values. Each
-- value may be a primitive value or a tuple specifying the value used
-- by the data model and a user-friendly value.
-- @param compute (optional <i><b>function(@{node.TreeNode})</b></i>
-- or <i>**string**</i>) function for computing the value of the field
-- when not provided by the back-end. The function gets a reference to
-- the field's parent as an argument. If defined as a string, a method
-- with the given name is invoked.
-- @tparam ?primitive default default value for the field
-- @tparam ?boolean required field must be assigned a value if set,
-- defaults to false
-- @param store (optional <i><b>function(@{node.TreeNode},
-- primitive)</b></i> or <i>**string**</i>) if this parameter is
-- defined, the value of the field is not stored according to the
-- field's back-end address. Rather, the provided function is invoked
-- with a reference to the parent and the field value. If defined as a
-- string, a method with the given name is invoked.
-- @tparam ?string ui_name user-friendly name for the field.
-- @tparam ?boolean visible the field is visible in the user interface
-- if set, defaults to true
-- @tparam ?string widget widget for rendering the field in the user
-- interface. The default widget for non-leaf objects is *link*, which
-- is a hyperlink to a detailed view to the object. The *inline*
-- widget renders a non-leaf object embedded in the parent's view.
M.Field = class(M.Member)

function M.Field:init(params)
   if not params then params = {} end
   setdefaults(
      params, {addr=params.compute and node.null_addr or nil, visible=true}
   )

   super(self, M.Field):init(params)

   for _, param in ipairs{'compute', 'store', 'editable'} do
      local func = self[param]
      if func then
	 if type(func) == 'string' then
	    local method = func
	    function func(obj, ...) return obj[method](obj, ...) end
	 end
	 if type(func) == 'function' then
	    self[param] = function(obj, ...)
	       return func(node.escalate(obj), ...)
	    end
	 end
      end
   end

   self.condition = M.conv_filter(self.condition)

   if self.choice then
      self.choice = map(
	 function(choice)
	    if type(choice) ~= 'table' then choice = {choice} end
	    for i, k in ipairs{'value', 'ui-value'} do
	       if choice[i] then
		  assert(not choice[k])
		  choice[k] = choice[i]
		  choice[i] = nil
	       end
	    end
	    return setdefaults(
	       choice,
	       {
		  be_value=choice.value,
		  enabled=true,
		  ['ui-value']=self:auto_ui_name(choice.value)
	       }
	    )
	 end,
	 self.choice
      )
   end

   if not self.widget then
      self.widget = self.choice and 'combobox' or 'field'
   end
end

function M.Field:_editable(context)
   if self.editable == nil then
      if not self.visible then return false end
      if self.store or not self.compute then return true end
      if self.compute then return self:_compute(context) == nil end
      return false
   end

   if type(self.editable) == 'function' then
      return self.editable(context.parent) and true or false
   end

   return self.editable
end

function M.Field:_choice(context) return self.choice end

function M.Field:meta(context)
   assert(self.dtype)
   local choice = self:_choice(context)
   return update(
      super(self, M.Field):meta(context),
      {
	 type=self.dtype,
	 visible=self.visible,
	 editable=self:_editable(context) and
	    node.has_permission(context.parent, 'modify'),
	 condition=self.condition,
	 required=self.required,
	 default=self.default,
	 choice=choice and map(
	    function(ch)
	       ch = util.copy(ch)
	       ch.be_value = nil
	       return ch
	    end,
	    choice
	 ),
	 widget=self.widget,
	 detail=self.detail
      }
   )
end

function M.Field:topology(context)
   return {
      {
	 path=context.path,
	 addr=context.addr,
	 be_mode=self.be_mode,
	 type=self.dtype
      }
   }
end

function M.Field:load(context)
   if not context.txn then return setmetatable({}, context) end
   local value = self:_load(context)
   if value == nil and self.compute then value = self:_compute(context) end
   if value == nil then return self.default end
   return value
end

function M.Field:_compute(context)
   return self.compute(context.parent, context.txn)
end

function M.Field:_load(context)
   local value = context.txn:get(context.addr)
   if value ~= nil then return self:decode(context, value) end
end

function M.Field:decode(context, value) return value end

function M.Field:_validate(context, value)
   if value == nil then
      self:check_required(context)
      return
   end

   value = self:normalize(context, value)

   local committing = context.txn:committing()
   local choice = self:_choice(context)
   local be_value
   if choice then
      for _, ch in ipairs(choice) do
	 if ch.value == value and (committing or ch.enabled) then
	    be_value = ch.be_value
	    break
	 end
      end
      if be_value == nil then raise(context.path, 'Invalid value') end
   end

   self:validate(context, value)

   if choice then return be_value end
   return value
end

function M.Field:check_required(context)
   if self.required then raise(context.path, 'Required value not set') end
end

function M.Field:normalize(context, value) return value end

function M.Field:validate(context, value) end

function M.Field:save(context, value)
   if not (context.privileged or self:_editable(context)) then
      raise(context.path, 'Is not editable')
   end

   if self.store then self.store(context.parent, value, context.txn)
   else self:_save(context, self:_validate(context, value)) end
end

function M.Field:_save(context, value)
   if value ~= nil then value = self:encode(context, value) end
   context.txn:set(context.addr, value)
end

function M.Field:encode(context, value) return value end

function M.Field:validate_saved(context)
   if self:_editable(context) then self:save(context, self:load(context)) end
end


local Primitive = class(M.Field)

function Primitive:_load(context)
   local value = super(self, Primitive):_load(context)
   if value == nil then return end

   local choice = self:_choice(context)
   if not choice then return value end

   for _, ch in ipairs(choice) do
      if ch.be_value == value then return ch.value end
   end
   assert(false)
end

function Primitive:validate(context, value)
   local t = self.dtype
   if type(value) ~= t then raise(context.path, 'Not a '..t) end
end


--- string field, inherits @{Field}.
-- @klass String
M.String = class(Primitive)

function M.String:init(params)
   super(self, M.String):init(params)
   self.dtype = 'string'
end

function M.String:validate(context, value)
   super(self, M.String):validate(context, value)
   if self['max-length'] and value:len() > self['max-length'] then
      raise(context.path, 'Maximum length exceeded')
   end
   if self.pattern and not value:match('^'..self.pattern..'$') then
      raise(context.path, 'Invalid value')
   end
end

function M.String:meta(context)
   local res = super(self, M.String):meta(context)
   res['max-length'] = self['max-length']
   return res
end


M.Number = class(Primitive)

function M.Number:init(params)
   super(self, M.Number):init(params)
   self.dtype = 'number'
end

function M.Number:normalize(context, value)
   return value and tonumber(value) or value
end

function M.Number:validate(context, value)
   super(self, M.Number):validate(context, value)
   if self.min and value < self.min then
      raise(context.path, 'Minimum value is '..self.min)
   end
   if self.max and value > self.max then
      raise(context.path, 'Maximum value is '..self.max)
   end
end


M.Integer = class(M.Number)

function M.Integer:validate(context, value)
   super(self, M.Integer):validate(context, value)
   if math.floor(value) ~= value then raise(context.path, 'Not an integer') end
end


--- boolean field, inherits @{Field}.
-- @klass Boolean
M.Boolean = class(Primitive)

function M.Boolean:init(params)
   super(self, M.Boolean):init(params)
   self.dtype = 'boolean'
   self.widget = 'checkbox'
end


M.TreeNode = class(M.Field)

function M.TreeNode:init(params)
   super(self, M.TreeNode):init(
      setdefaults(params, {detail=true, widget='link'})
   )
   self.iparams = {}
end

function M.TreeNode:topology(context)
   local res = super(self, M.TreeNode):topology(context)
   res[1].subtype = res[1].type
   res[1].type = 'table'
   util.extend(res, node.topology(self:load(context, {create=true})))
   return res
end

function M.TreeNode:load(context, options)
   if context.txn and not (
      util.setdefault(
	 options or {}, 'create', self.create
      ) or self:_load(context)
   ) then return end
   local res = self.itype(
      context, update({editable=self:_editable(context)}, self.iparams)
   )
   return node.has_permission(res, 'read') and res or nil
end

function M.TreeNode:save(context, value)
   local path = context.path

   if value == path then return end
   if type(value) == 'string' then
      value = node.fetch(context.parent, value)
   end
   if object.isinstance(value, node.TreeNode) and node.path(value) == path then
      return
   end

   self:_save(context)

   if value then
      if type(value) ~= 'table' then
	 raise(path, 'Cannot assign primitive value')
      end

      self:_save(context, {})
      local new = self:load(context, {create=true})

      local errors = err.ErrorDict()
      for k, v in pairs(value) do
	 errors:collect(self.save_member, new, k, v)
      end
      errors:raise()

      return new
   end
end

function M.TreeNode.save_member(node, k, v) node[k] = v end

function M.TreeNode:validate_saved(context)
   if self:load(context) == nil then self:check_required(context) end
end


--- model field, inherits @{Field}. The value of this field is a <a
-- href="#Model_objects">model object</a> conforming to the specified
-- model. A model field with default parameters is implicitly created
-- when a model is used in lieu of a @{Field}.
-- @klass Model
-- @param model (<i>[&lt;Model&gt;](#new)</i>) model describing the
-- structure of the model objects
M.Model = class(M.TreeNode)

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

   assert(self.model)
   self.itype = self.model
   self.dtype = 'model'
end

function M.Model:save(context, value)
   local new = super(self, M.Model):save(context, value)
   if new then node.check_permission(new, 'create') end
end


return M
