-- Localize globals
local assert, error, io, loadfile, math, minetest, modlib, pairs, setfenv, setmetatable, type
	= assert, error, io, loadfile, math, minetest, modlib, pairs, setfenv, setmetatable, type

-- Set environment
local _ENV = {}
setfenv(1, _ENV)

-- Default value
reference_strings = true

-- Note: keys may not be marked as weak references: garbage collected log files wouldn't close the file:
-- The `__gc` metamethod doesn't work for tables in Lua 5.1; a hack using `newproxy` would be needed
-- See https://stackoverflow.com/questions/27426704/lua-5-1-workaround-for-gc-metamethod-for-tables)
-- Therefore, :close() must be called on log files to remove them from the `files` table
local files = {}
local metatable = {__index = _ENV}
_ENV.metatable = metatable

function new(file_path, root, reference_strings)
	local self = setmetatable({
		file_path = assert(file_path),
		root = root,
		reference_strings = reference_strings
	}, metatable)
	if minetest then
		files[self] = true
	end
	return self
end

local function set_references(self, table)
	-- Weak table keys to allow the collection of dead reference tables
	-- TODO garbage collect strings in the references table
	self.references = setmetatable(table, {__mode = "k"})
end

function load(self)
	-- Bytecode is blocked by the engine
	local read = assert(loadfile(self.file_path))
	-- math.huge is serialized to inf
	local env = {inf = math.huge}
	setfenv(read, env)
	read()
	env.R = env.R or {{}}
	local reference_count = #env.R
	for ref in pairs(env.R) do
		if ref > reference_count then
			-- Ensure reference count always has the value of the largest reference
			-- in case of "holes" (nil values) in the reference list
			reference_count = ref
		end
	end
	self.reference_count = reference_count
	self.root = env.R[1]
	set_references(self, {})
end

function open(self)
	self.file = io.open(self.file_path, "a+")
end

function init(self)
	if modlib.file.exists(self.file_path) then
		self:load()
		self:_rewrite()
		self:open()
		return
	end
	self:open()
	self.root = {}
	self:_write()
end

function log(self, statement)
	self.file:write(statement)
	self.file:write"\n"
end

function flush(self)
	self.file:flush()
end

function close(self)
	self.file:close()
	self.file = nil
	files[self] = nil
end

if minetest then
	minetest.register_on_shutdown(function()
		for self in pairs(files) do
			self.file:close()
		end
	end)
end

local function _dump(self, value, is_key)
	if value == nil then
		return "nil"
	end
	if value == true then
		return "true"
	end
	if value == false then
		return "false"
	end
	if value ~= value then
		-- nan
		return "0/0"
	end
	local _type = type(value)
	if _type == "number" then
		return ("%.17g"):format(value)
	end
	local reference = self.references[value]
	if reference then
		return "R[" .. reference .."]"
	end
	reference = self.reference_count + 1
	local key = "R[" .. reference .."]"
	local function create_reference()
		self.reference_count = reference
		self.references[value] = reference
	end
	if _type == "string" then
		local reference_strings = self.reference_strings
		if is_key and ((not reference_strings) or value:len() <= key:len()) and value:match"^[%a_][%a%d_]*$" then
			-- Short key
			return value, true
		end
		local formatted = ("%q"):format(value)
		if (not reference_strings) or formatted:len() <= key:len()  then
			-- Short string
			return formatted
		end
		-- Use reference
		create_reference()
		self:log(key .. "=" .. formatted)
	elseif _type == "table" then
		-- Tables always need a reference before they are traversed to prevent infinite recursion
		create_reference()
		-- TODO traverse tables to determine whether this is actually needed
		self:log(key .. "={}")
		local tablelen = #value
		for k, v in pairs(value) do
			if type(k) ~= "number" or k % 1 ~= 0 or k < 1 or k > tablelen then
				local dumped, short = _dump(self, k, true)
				self:log(key .. (short and ("." .. dumped) or ("[" .. dumped .. "]")) .. "=" .. _dump(self, v))
			end
		end
	else
		error("unsupported type: " .. _type)
	end
	return key
end

function set(self, table, key, value)
	if not self.references[table] then
		error"orphan table"
	end
	if table[key] == value then
		-- No change
		return
	end
	table[key] = value
	table = _dump(self, table)
	local key, short_key = _dump(self, key, true)
	self:log(table .. (short_key and ("." .. key) or ("[" .. key .. "]")) .. "=" .. _dump(self, value))
end

function set_root(self, key, value)
	return self:set(self.root, key, value)
end

function _write(self)
	set_references(self, {})
	self.reference_count = 0
	self:log"R={}"
	_dump(self, self.root)
end

function _rewrite(self)
	self.file = io.open(self.file_path, "w+")
	self:_write()
	self.file:close()
end

function rewrite(self)
	if self.file then
		self.file:close()
	end
	self:_rewrite()
	self:open()
end

-- Export environment
return _ENV