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

local M = {}

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

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

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

local util = require('acf2.util')


local function contains(list, value)
   for k, v in ipairs(list) do if v == value then return true end end
   return false
end

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
   return (name:sub(1, 1):upper()..name:sub(2)):gsub('-', ' ')
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


M.Field = class(M.Member)

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

   if self.choice and not self['ui-choice'] then
      self['ui-choice'] = util.map(
	 function(name) return self:auto_ui_name(name) end,
	 self.choice
      )
   end

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

function M.Field:meta(context)
   assert(self.dtype)
   local res = super(self, M.Field):meta(context)

   res.type = self.dtype
   res.required = self.required
   res.default = self.default
   res.choice = self.choice
   res.widget = self.widget
   res['ui-choice'] = self['ui-choice']

   return res
end

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

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

function M.Field:_validate(context, value)
   if self.required and value == nil then
      raise(context.path, 'Required value not set')
   end
   if self.choice and value ~= nil and not contains(self.choice, value) then
      raise(context.path, 'Invalid value')
   end
   if value ~= nil then self:validate(context, value) end
   return value
end

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

function M.Field:save(context, value)
   context.txn:set(context.addr, self:_validate(context, value))
end

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


local Primitive = class(M.Field)

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:_validate(context, value)
   return super(self, M.Number):_validate(
      context,
      value and tonumber(value) or value
   )
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 = self.dtype
end


M.TreeNode = class(M.Field)

function M.TreeNode:init(params)
   if not params.widget then params.widget = 'link' end
   super(self, M.TreeNode):init(params)
end

function M.TreeNode:topology(context)
   local res = super(self, M.TreeNode):topology(context)
   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 (
      (
	 options and options.create
      ) or self.create or context.txn:get(context.addr)
   ) then return end
   return self.itype(context, self.iparams)
end

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

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

   context.txn:set(context.addr)

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

      context.txn:set(context.addr, {})
      local new = self:load(context, {create=true})

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

function M.TreeNode.save_member(node, k, v) node[k] = v 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


return M
