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

local M = {}

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

local combination = require('acf2.model.combination')
M.Union = combination.Union
M.Range = combination.Range

local fld = require('acf2.model.field')
local Field = fld.Field
M.Boolean = fld.Boolean
M.Integer = fld.Integer
M.Number = fld.Number
M.String = fld.String

local model = require('acf2.model.model')
M.Action = model.Action
M.new = model.new
local to_field = model.to_field

M.net = require('acf2.model.net')

local node = require('acf2.model.node')
M.node = {}
for _, m in ipairs{
   'List',
   'Set',
   'TreeNode',
   'has_permission',
   'insert',
   'meta',
   'mmeta',
   'path',
   'pairs',
   'ipairs'
} do M.node[m] = node[m] end

M.permission = require('acf2.model.permission')
M.register = require('acf2.model.root').register
M.node.Set = require('acf2.model.set').Set

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

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


local stringy = require('stringy')


M.Reference = class(Field)

function M.Reference:init(params)
   if not params.widget then params.widget = 'reference' end
   super(self, M.Reference):init(params)
   self.dtype = 'reference'
   if not self.scope then self.scope = '/' end
end

function M.Reference:topology(context)
   local res = super(self, M.Reference):topology(context)
   res[1].scope = self.scope
   return res
end

function M.Reference:abs_scope(context)
   return pth.to_absolute(self.scope, node.path(context.parent))
end

function M.Reference:meta(context)
   local res = super(self, M.Reference):meta(context)
   res.scope = self:abs_scope(context)
   return res
end

function M.Reference:follow(context, value)
   return context.txn:fetch(pth.rawjoin(self:abs_scope(context), value))
end

function M.Reference:load(context, options)
   local ref = super(self, M.Reference):load(context)
   return (
      (not options or options.dereference ~= false) and context.txn and ref
   ) and self:follow(context, ref) or ref
end

function M.Reference:_validate(context, value)
   super(self, M.Reference):_validate(context, value)

   if value == nil then return end

   if isinstance(value, node.TreeNode) then value = node.path(value) end

   local path = context.path
   if type(value) ~= 'string' then raise(path, 'Path name must be string') end

   if pth.is_absolute(value) then
      local scope = self:abs_scope(context)
      local prefix = scope..'/'
      if not stringy.startswith(value, prefix) then
	 raise(path, 'Reference out of scope ('..scope..')')
      end
      value = value:sub(prefix:len() + 1, -1)
   end

   -- assume one-level ref for now
   if #pth.split(value) > 1 then
      raise(path, 'Subtree references not yet supported')
   end

   -- TODO check instance type
   M.error.relabel(path, self.follow, self, context, value)

   return value
end

function M.Reference:deleted(context, addr)
   local target = self:load(context)
   if target and node.addr(target) == addr then
      -- TODO raise error for the target object
      raise(context.path, 'Refers to '..addr)
   end
end


M.Model = fld.Model


M.Collection = class(fld.TreeNode)

function M.Collection:init(params, itype)
   if params.create == nil then params.create = true end
   super(self, M.Collection):init(params)

   assert(self.type)
   self.itype = itype or node.Collection
   self.iparams = {
      destroy=self.destroy,
      layout=self.layout,
      required=self.required,
      ui_member=self.ui_member
   }

   self.dtype = 'collection'
end

function M.Collection:auto_ui_name(name)
   if not name then return end
   if name:sub(-1, -1) ~= 's' then name = name..'s' end
   return super(self, M.Collection):auto_ui_name(name)
end

function M.Collection:load(context, options)
   if not self.iparams.field then self.iparams.field = to_field(self.type) end
   return super(self, M.Collection):load(context, options)
end


M.List = class(M.Collection)
function M.List:init(params) super(self, M.List):init(params, node.List) end


M.Set = class(M.Collection)
function M.Set:init(params)
   if not params.widget and isinstance(params.type, M.Reference) then
      params.widget = 'checkboxes'
   end
   super(self, M.Set):init(params, M.node.Set)
end
function M.Set.save_member(tn, k, v) node.insert(tn, v) end


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

function M.Mixed:init(params)
   params.type = M.Mixed
   super(self, M.Mixed):init(params, node.Mixed)
   self.pfield = Field()
end

function M.Mixed:topology(context) return {} end

function M.Mixed:load(context)
   local value = self.pfield:load(context)
   if type(value) == 'table' then return super(self, M.Mixed):load(context) end
   return value
end

function M.Mixed:save(context, value)
   if type(value) == 'table' then super(self, M.Mixed):save(context, value)
   else self.pfield:save(context, value) end
end


return M
