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

--- Alpine Configurator (aconf) data model. This module is imported by
-- all aconf modules. The aconf modules are installed to
-- */usr/share/aconf/aconf/modules* and imported automatically from
-- there. On import, the module may return an initialization function,
-- which receives a [transaction object](#Transaction_objects) as an
-- argument.
-- @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


--- Overview of field classes. In the Alpine Configurator object
-- system, classes are instantiated by invoking the Lua table
-- representing the class as the constructor. There are a number of
-- [pre-defined field classes](#Field_classes). More field classes may
-- be defined by extending them using @{object.class} and overriding
-- the appropriate methods described in this section. All field class
-- constructors accept one [argument](#Field_constructor_parameters).
-- @section Field
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

--- transform the value's back-end representation to data model
-- representation. The default implementation is the identity
-- transformation.
-- @function decode
-- @param context transaction context
-- @tparam primitive value back-end representation
-- @treturn primitive data model representation
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

--- transform the value's data model representation to back-end
-- representation. The default implementation is the identity
-- transformation.
-- @function encode
-- @param context transaction context
-- @tparam primitive value data model representation
-- @treturn primitive back-end representation
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


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


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


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
