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

local topology = require('aconf.model.root').topology
local class = require('aconf.object').class
local pth = require('aconf.path')
local tostr = require('aconf.persistence.util').tostring

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


local stringy = require('stringy')


local function array_join(tbl, value)
   local res = copy(tbl)
   table.insert(res, value)
   return res
end

local function array_without_last(tbl)
   local res = copy(tbl)
   res[#res] = nil
   return res
end


local function basename(path)
   local res, pred = path:match('^.*/([@#$%w._-]+)([^/]*)$')
   assert(res)
   assert(res ~= '#')
   assert(pred == '' or pred:match('^%[.+%]$'))
   return res
end

local function append_pred(path, pred) return path..'['..pred..']' end

local function append_key_pred(path, key, value)
   return append_pred(path, key.." = '"..value.."'")
end

local function key(mode)
   if mode == 'value' then return '.' end
   if mode and stringy.startswith(mode, 'child-value:') then
      return mode:sub(13, -1)
   end
end

local function is_selector(mode) return mode and mode ~= 'parent-value' end



local function aug_top(path)
   path = copy(path)
   table.insert(path, 1, 'augeas')
   return topology(path)
end


local function conv_path(path)
   local res = '/files'
   if #path == 0 then return res, nil, {} end

   path = copy(path)
   local top = aug_top{}
   local mode, parent
   local keys = {}

   repeat
      local comp = path[1]
      assert(tostring(comp):match('^[%w %_%-%.%@%#%$]+$'))

      if mode then
	 if mode == 'enumerate' then
	    assert(type(comp) == 'number')
	    res = append_pred(res, comp)
	 else
	    local k = key(mode)
	    assert(k and type(comp) == 'string')
	    res = append_key_pred(res, k, comp)
	    table.insert(keys, k)
	 end

	 parent = nil

      else
	 parent = res
	 res = res..'/'..comp
	 keys = {}
      end

      top = top and top(pth.escape(comp))
      mode = top and top.mode

      table.remove(path, 1)
   until #path == 0

   if mode == 'parent-value' then
      assert(parent)
      res = parent
   end

   return res, mode, keys
end


local backend = class()

function backend:init() self.aug = require('augeas').init() end

function backend:get(path, top)
   local apath, mode, keys = conv_path(path)
   local existence = top == true
   if existence or not top then top = aug_top(path) end

   local tpe = top and top.type
   local tbl = tpe == 'table'
   if tbl and top.subtype == 'model' then mode = nil end

   local leaf = not tbl and not mode
   if mode == 'parent-value' then
      assert(not tbl)
      leaf = true
   end

   local matches = self.aug:match(apath..(not leaf and not mode and '/*' or ''))

   if #matches == 0 and not is_selector(mode) then return end

   if is_selector(mode) and #path > 1 and not self:get(
      array_without_last(path), true
   ) then
      return
   end

   if not tpe and not mode then
      if #matches > 1 then
	 leaf = false
	 mode = 'enumerate'
      else
	 local children = self.aug:match(apath..'/*')
	 if #children > 0 then
	    leaf = false
	    matches = children
	 end
      end
   end
   
   if leaf then
      assert(#matches == 1)
      return self.aug:get(apath)
   end

   if existence then return true end

   local names = {}

   for i, child in ipairs(matches) do
      local name
      if not mode then
	 name = basename(child)
	 name = tonumber(name) or name
	 if contains(keys, name) then name = nil end
      elseif mode == 'enumerate' then name = i
      else
	 local k = key(mode)
	 assert(k)
	 name = self.aug:get(child..(k == '.' and '' or '/'..k))
      end

      if name and self:get(array_join(path, name), true) then
	 names[name] = true
      end
   end

   return util.keys(names)
end

function backend:set(mods)
   local gc = {}

   for _, mod in ipairs(mods) do
      local path, value = table.unpack(mod)

      local function insert(path, new)
	 local apath, mode, keys = conv_path(path)
	 if #path == 0 then return apath, keys end
	 local name = path[#path]

	 local parent = array_without_last(path)
	 local ppath, pmode = conv_path(parent)

	 if is_selector(pmode) then
	    gc[pth.join('/', table.unpack(parent))] = false

	    if pmode == 'enumerate' then
	       local count = #self.aug:match(ppath)
	       while count < name do
		  insert(parent, true)
		  count = count + 1
	       end
	       return apath, keys
	    end
	 end

	 local matches = self.aug:match(apath)
	 local count = #matches

	 if count > 0 and not new then return apath, keys end

	 if key(pmode) then
	    apath = key(pmode) and append_pred(
	       ppath, 'count('..key(pmode)..') = 0'
	    ) or append_key_pred(ppath, '.', '')

	    matches = self.aug:match(apath)
	    assert(#matches < 2)
	    apath = matches[1] or insert(parent, true)

	    self.aug:set(apath..'/'..key(pmode), name)

	    return apath, keys
	 end

	 if #matches == 0 then
	    matches = self.aug:match(ppath..'/*')

	    local function order(path)
	       local top = aug_top(path)
	       return top and top.order
	    end
	    local ord = order(path)

	    for _, sibling in ipairs(matches) do
	       local sord = order(array_join(parent, basename(sibling)))
	       if sord and sord > ord then
		  self.aug:insert(sibling, name, true)
		  return apath, keys
	       end
	    end
	 end

	 if #matches == 0 then
	    if new then self.aug:set(apath, nil) end
	    return apath, keys
	 end

	 self.aug:insert(matches[#matches], name)
	 return append_pred(apath, count + 1), keys
      end

      local apath, keys = insert(path)
      local is_table = type(value) == 'table'

      if not (is_table or contains(keys, '.')) then
	 self.aug:set(apath, value ~= nil and tostr(value) or nil)
      end

      util.setdefault(gc, pth.join('/', table.unpack(path)), true)
   end

   for path, _ in pairs(gc) do
      local p = pth.split(path)
      while #p > 0 do
	 local value = self:get(p)

	 if (
	    type(value) == 'string' and value ~= ''
	 ) or (
	    type(value) == 'table' and #value > 0
	 ) then
	    break
	 end

	 if gc[pth.join('/', table.unpack(p))] ~= false then
	    self.aug:rm(conv_path(p))
	 end
	 p[#p] = nil
      end
   end

   if self.aug:save() ~= 0 then
      print('Augeas save failed')
      for _, ep in ipairs(self.aug:match('/augeas//error')) do
	 print(ep, self.aug:get(ep))
      end
      assert(false)
   end
end


return backend
