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

--- @module aconf.model

local ErrorDict = require('aconf.error').ErrorDict
local root = require('aconf.model.root')

local object = require('aconf.object')
local super = object.super

local pth = require('aconf.path')

local util = require('aconf.util')
local copy = util.copy


--- Transaction objects. These objects represent and ongoing
-- transaction.
-- @section ModelTransaction
local ModelTransaction = object.class(
   require('aconf.transaction.base').Transaction
)

function ModelTransaction:init(backend, validate, user)
   super(self, ModelTransaction):init(backend)

   self.validate = validate
   self.validable = {}

   self.user = user

   self.root = root.RootModel{txn=self}
end

function ModelTransaction:committing()
   return self.commit_val and true or false
end

function ModelTransaction:check()
   if not self.backend then error('Transaction already committed') end
end

--- fetch an object.
-- @function get
-- @tparam string path object path
-- @treturn primitive|node.TreeNode object
function ModelTransaction:get(path)
   self:check()
   return super(self, ModelTransaction):get(path)
end

function ModelTransaction:set_multiple(mods)
   super(self, ModelTransaction):set_multiple(mods)
   for _, mod in ipairs(mods) do
      local addr, value = table.unpack(mod)
      if value == nil then
	 for _, val in ipairs{self.validable, self.commit_val} do
	    local done
	    repeat
	       done = true
	       for path, a in pairs(copy(val)) do
		  if a == addr then
		     for p, _ in pairs(copy(val)) do
			if pth.is_subordinate(p, path) then val[p] = nil end
		     end
		     done = false
		     break
		  end
	       end
	    until done
	 end
      end
   end
end

function ModelTransaction:check_deleted(path)
   local top = root.topology(path)
   if top then
      local errors = ErrorDict()
      for _, refs in ipairs(top.referrers) do
	 for _, ref in ipairs(getmetatable(self.root).escalate:search(refs)) do
	    errors:collect(ref.deleted, path)
	 end
      end
      errors:raise()
   end
end

function ModelTransaction:fetch(path, escalate)
   local root = self.root
   return (escalate and getmetatable(root).escalate or root):fetch(path)
end

function ModelTransaction:meta(path) return self.root:meta(path) end

function ModelTransaction:commit()
   self:check()

   if self.validate then
      self.commit_val = copy(self.validable)
      local errors = ErrorDict()

      local function validate(path)
	 if path > '/' then validate(pth.parent(path)) end
	 if not self.commit_val[path] then return end
	 errors:collect(getmetatable(self:fetch(path, true)).validate)
	 self.commit_val[path] = nil
      end

      while next(self.commit_val) do validate(next(self.commit_val)) end
      self.commit_val = nil
      errors:raise()
   end

   super(self, ModelTransaction):commit()

   if not self.validate then
      util.update(self.backend.validable, self.validable)
   end
   
   self.backend = nil
end


local store = require('aconf.persistence')
local def_store = require('aconf.persistence.defer')

return function(options)
   options = options or {}
   return ModelTransaction(
      options.parent or (options.allow_commit_defer and def_store or store),
      not (options.parent and options.defer_validation),
      options.parent and options.parent.user or options.user
   )
end
