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

local M = {}

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

local fld = require('acf2.model.field')
local Field = fld.Field
local Member = fld.Member

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

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

local pth = require('acf2.path')
local util = require('acf2.util')


local function to_member(obj, params)
   if not params then params = {} end
   if object.issubclass(obj, M.Model) then
      params.model = obj
      return fld.Model(params)
   end
   local res = getmetatable(obj).class and obj or obj(params)
   assert(isinstance(res, Member))
   return res
end

function M.to_field(obj, params)
   local res = to_member(obj, params)
   assert(isinstance(res, 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 = '/null/action'
   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


function M.new(base)
   if not base then base = M.Model end

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

   local mt = util.copy(getmetatable(res))

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

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

      local override = t[k]
      if type(v) == 'table' then v = to_member(v) 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

M.Model = M.new(node.TreeNode)

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

   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]
      local create = options and options.create

      if isinstance(v, Field) then
	 v = BoundMember(self, k, v)
	 if v.compute then return v:compute() end
	 return v:load{create=create}
      end

      assert(mt.txn)

      if isinstance(v, M.Action) then
	 local f = v.field and BoundMember(self, k, v.field)
	 if create then return f and f:load{create=true} 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(self, f and f:load())
		   if f then mt.txn:set(v.field.addr) end
		   return res
		end
      end

      return v
   end

   if not mt.txn then return end


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

   function mt.save(k, v) return mt.member(k, false, Field):save(v) end

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

   mt.meta.type = 'model'
   mt.meta.fields = tmeta(Field)
   mt.meta.actions = tmeta(M.Action)

   function mt.members()
      return util.map(function(f) return f.name end, mt.meta.fields)
   end

   function mt.validate()
      for _, f in ipairs(_members(Field)) do
	 if not f.compute then f:validate_saved() end
      end
   end

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


return M
