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

local M = {}

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

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

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


M.BoundMember = class()

function M.BoundMember:init(parent, name, field)
   local pmt = getmetatable(parent)
   local mt = {}

   function mt.__index(t, k)
      local member = field[k]
      if type(member) ~= 'function' then return member end
      return function(self, ...)
		return member(
		   field,
		   {
		      txn=pmt.txn,
		      parent=parent,
		      path=pth.join(pmt.path, name),
		      addr=pth.to_absolute(
			 field.addr or pth.escape(name), pmt.addr
		      )
		   },
		   ...
		)
	     end
   end

   setmetatable(self, mt)
end


M.TreeNode = class()

function M.TreeNode:init(context)
   local mt = getmetatable(self)
   util.update(mt, context)

   mt.dereference = true
   mt.meta = {}
   function mt.get(k, create) return mt.load(k, {create=create}) end

   if not mt.txn then return end

   if mt.parent then
      mt.meta['ui-name'] = getmetatable(mt.parent).mmeta(
	 pth.name(mt.path)
      )['ui-name']
   end

   function mt.save(k, v) rawset(self, k, v) end
   function mt.__index(t, k) return mt.get(k) end
   function mt.__newindex(t, k, v) mt.save(k, v) end

   function mt.has_permission(user, permission)
      local p = permission..mt.path
      if mt.txn:fetch('/auth/permissions')[p] then
	 return user:check_permission(p)
      end

      if ({create=true, delete=true})[permission] then
	 permission = 'modify'
      end
      return M.has_permission(mt.parent, user, permission)
   end

   mt.txn.validable[mt.path] = mt.addr
end

function M.TreeNode:fetch(path, create)
   if type(path) == 'string' then path = pth.split(path) end

   if #path == 0 then return self end

   local mt = getmetatable(self)
   local name = path[1]
   if not mt.member(name) then
      raise(mt.path, 'Member does not exist: '..name)
   end

   local next = mt.get(name, create)
   if next == nil and (not create or #path > 1) then
      raise(mt.path, 'Subordinate does not exist: '..name)
   end

   table.remove(path, 1)
   if #path > 0 and type(next) ~= 'table' then
      raise(pth.join(mt.path, name), 'Is a primitive value')
   end

   return M.TreeNode.fetch(next, path, create)
end

function M.TreeNode:search_refs(path)
   if type(path) == 'string' then path = pth.split(path) end

   if #path == 0 then return {} end

   local mt = getmetatable(self)
   local name = path[1]
   table.remove(path, 1)

   local function collect(name)
      local next = mt.load(name)
      if not next then return {} end
      
      local member = mt.member(name)
      if member.deleted then return {member} end
      
      return isinstance(next, M.TreeNode) and M.TreeNode.search_refs(
	 next, path
      ) or {}
   end

   if name == pth.wildcard then
      local res = {}
      for _, member in ipairs(mt.members()) do
	 util.extend(res, collect(member))
      end
      return res
   end

   return collect(name)
end


M.Collection = class(M.TreeNode)

function M.Collection:init(context, params)
   super(self, M.Collection):init(context)

   self.init = nil
   self.fetch = nil
   self.search_refs = nil

   local mt = getmetatable(self)
   local field = M.BoundMember(self, pth.wildcard, params.field)

   function mt.topology() return field:topology() end

   function mt.member(name)
      return M.BoundMember(self, name, params.field)
   end

   function mt.load(k, options) return mt.member(k):load(options) end

   if not mt.txn then return end

   mt.meta.type = 'collection'
   mt.meta.members = field:meta()
   mt.meta['ui-member'] = params.ui_member or mt.meta['ui-name']:gsub('s$', '')
   mt.meta.widget = params.layout

   function mt.mmeta(name)
      local res = util.copy(mt.meta.members)
      if name ~= pth.wildcard then
	 res['ui-name'] = mt.meta['ui-member']..' '..name
      end
      return res
   end

   function mt.members() return mt.txn:get(mt.addr) or {} end

   function mt.validate()
      if #mt.members() > 0 then return end
      if params.required then raise(mt.path, 'Collection cannot be empty') end
      if params.destroy then
	 mt.txn:set(mt.addr)
	 validate(mt.parent)
      end
   end
   
   function mt.save(k, v) mt.member(k):save(v) end
end


M.List = class(M.Collection)

function M.List:init(context, params)
   super(self, M.List):init(context, params)

   local mt = getmetatable(self)
   mt.meta.type = 'list'

   local save = mt.save
   function mt.save(k, v)
      assert(type(k) == 'number')
      if v == nil then
	 local len = #mt.members()
	 while k < len do
	    mt.save(k, mt.load(k + 1, {dereference=false}))
	    k = k + 1
	 end
      end
      save(k, v)
   end

   function mt.insert(v, i)
      local len = #mt.members()
      if not i then i = len + 1 end
      for j = len,i,-1 do mt.save(j + 1, mt.load(j, {dereference=false})) end
      mt.save(i, v)
   end
end


-- experimental
M.Mixed = class(M.Collection)

function M.Mixed:init(context, params)
   super(self, M.Mixed):init(context, params)

   -- TODO dynamic meta: list non-leaf children
   local mt = getmetatable(self)
   mt.meta = {type='mixed', ['ui-name']=mt.path}
   function mt.mmeta(name)
      return {type='mixed', ['ui-name']=pth.join(mt.path, name)}
   end
end


local function meta_func(attr)
   return function(node, ...)
	     local res = getmetatable(node)[attr]
	     if type(res) == 'function' then return res(...) end
	     return res
	  end
end

for _, mf in ipairs{
   'addr', 'has_permission', 'insert', 'meta', 'mmeta', 'path', 'topology'
} do M[mf] = meta_func(mf) end


function M.pairs(tbl, dereference)
   if not isinstance(tbl, M.TreeNode) then return pairs(tbl) end

   local mt = getmetatable(tbl)
   if dereference == nil then dereference = mt.dereference end

   local res = {}
   for _, member in ipairs(mt.members()) do
      res[member] = mt.load(member, {dereference=dereference})
   end
   return pairs(res)
end

local function _ipairs(mt, i)
   i = i + 1
   local v = mt.load(i)
   if v == nil then return end
   return i, v
end
function M.ipairs(tbl)
   if not isinstance(tbl, M.TreeNode) then return ipairs(tbl) end
   return _ipairs, getmetatable(tbl), 0
end


return M
