local assert, error, math_huge, modlib, minetest, setmetatable, type, table_insert, table_sort, pairs, ipairs
	= assert, error, math.huge, modlib, minetest, setmetatable, type, table.insert, table.sort, pairs, ipairs

local sqlite3 = ...

--! experimental

--[[
	Currently uses reference counting to immediately delete tables which aren't reachable from the root table anymore, which has two issues:
	1. Deletion might trigger a large deletion chain
	TODO defer deletion, clean up unused tables on startup, delete & iterate tables partially
	2. Reference counting is unable to handle cycles. `:collectgarbage()` implements a tracing "stop-the-world" garbage collector which handles cycles.
	TODO take advantage of Lua's garbage collection by keeping a bunch of "twin" objects in a weak structure using proxies (Lua 5.1) or the __gc metamethod (Lua 5.2)
	See https://wiki.c2.com/?ReferenceCountingCanHandleCycles, https://www.memorymanagement.org/mmref/recycle.html#mmref-recycle and https://wiki.c2.com/?GenerationalGarbageCollectio
	Weak tables are of no use here, as we need to be notified when a reference is dropped
]]

local _ENV = {}
setfenv(1, _ENV)
local metatable = {__index = _ENV}
_ENV.metatable = metatable

-- Note: keys may not be marked as weak references: wouldn't close the database: see persistence/lua_log_file.lua
local databases = {}

local types = {
	boolean = 1,
	number = 2,
	string = 3,
	table = 4
}

local function increment_highest_table_id(self)
	self.highest_table_id = self.highest_table_id + 1
	if self.highest_table_id > 2^50 then
		-- IDs are approaching double precision limit (52 bits mantissa), defragment them
		self:defragment_ids()
	end
	return self.highest_table_id
end

function new(file_path, root)
	return setmetatable({
		database = sqlite3.open(file_path),
		root = root
	}, metatable)
end

function _ENV.setmetatable(self)
	assert(self.database and self.root)
	return setmetatable(self, metatable)
end

local set

local function add_table(self, table)
	if type(table) ~= "table" then return end
	if self.counts[table] then
		self.counts[table] = self.counts[table] + 1
		return
	end
	self.table_ids[table] = increment_highest_table_id(self)
	self.counts[table] = 1
	for k, v in pairs(table) do
		set(self, table, k, v)
	end
end

local decrement_reference_count

local function delete_table(self, table)
	local id = assert(self.table_ids[table])
	self.table_ids[table] = nil
	self.counts[table] = nil
	for k, v in pairs(table) do
		decrement_reference_count(self, k)
		decrement_reference_count(self, v)
	end
	local statement = self._prepared.delete_table
	statement:bind(1, id)
	statement:step()
	statement:reset()
end

function decrement_reference_count(self, table)
	if type(table) ~= "table" then return end
	local count = self.counts[table]
	if not count then return end
	count = count - 1
	if count == 0 then return delete_table(self, table) end
	self.counts[table] = count
end

function set(self, table, key, value)
	local deletion = value == nil
	if not deletion then
		add_table(self, key)
		add_table(self, value)
	end
	if type(previous_value) == "table" then
		decrement_reference_count(self, previous_value)
	end
	if deletion and type(key) == "table" then
		decrement_reference_count(self, key)
	end
	local statement = self._prepared[deletion and "delete" or "insert"]
	local function bind_type_and_content(n, value)
		local type_ = type(value)
		statement:bind(n, assert(types[type_]))
		if type_ == "boolean" then
			statement:bind(n + 1, value and 1 or 0)
		elseif type_ == "number" then
			if value ~= value then
				statement:bind(n + 1, "nan")
			elseif value == math_huge then
				statement:bind(n + 1, "inf")
			elseif value == -math_huge then
				statement:bind(n + 1, "-inf")
			else
				statement:bind(n + 1, value)
			end
		elseif type_ == "string" then
			-- Use bind_blob instead of bind as Lua strings are effectively byte strings
			statement:bind_blob(n + 1, value)
		elseif type_ == "table" then
			statement:bind(n + 1, self.table_ids[value])
		end
	end
	statement:bind(1, assert(self.table_ids[table]))
	bind_type_and_content(2, key)
	if not deletion then
		bind_type_and_content(4, value)
	end
	statement:step()
	statement:reset()
end

local function exec(self, sql)
	if self.database:exec(sql) ~= sqlite3.OK then
		error(self.database:errmsg())
	end
end

function init(self)
	local database = self.database
	local function prepare(sql)
		local stmt = database:prepare(sql)
		if not stmt then error(database:errmsg()) end
		return stmt
	end
	exec(self, [[
CREATE TABLE IF NOT EXISTS table_entries (
	table_id INTEGER NOT NULL,
	key_type INTEGER NOT NULL,
	key BLOB NOT NULL,
	value_type INTEGER NOT NULL,
	value BLOB NOT NULL,
	PRIMARY KEY (table_id, key_type, key)
)]])
	self._prepared = {
		insert = prepare"INSERT OR REPLACE INTO table_entries(table_id, key_type, key, value_type, value) VALUES (?, ?, ?, ?, ?)",
		delete = prepare"DELETE FROM table_entries WHERE table_id = ? AND key_type = ? AND key = ?",
		delete_table = prepare"DELETE FROM table_entries WHERE table_id = ?",
		update = {
			id = prepare"UPDATE table_entries SET table_id = ? WHERE table_id = ?",
			keys = prepare("UPDATE table_entries SET key = ? WHERE key_type = " .. types.table .. " AND key = ?"),
			values = prepare("UPDATE table_entries SET value = ? WHERE value_type = " .. types.table .. " AND value = ?")
		}
	}
	-- Default value
	self.highest_table_id = 0
	for id in self.database:urows"SELECT MAX(table_id) FROM table_entries" do
		-- Gets a single value
		self.highest_table_id = id
	end
	increment_highest_table_id(self)
	local tables = {}
	local counts = {}
	self.counts = counts
	local function get_value(type_, content)
		if type_ == types.boolean then
			if content == 0 then return false end
			if content == 1 then return true end
			error("invalid boolean value: " .. content)
		end
		if type_ == types.number then
			if content == "nan" then
				return 0/0
			end
			if content == "inf" then
				return math_huge
			end
			if content == "-inf" then
				return -math_huge
			end
			assert(type(content) == "number")
			return content
		end
		if type_ == types.string then
			assert(type(content) == "string")
			return content
		end
		if type_ == types.table then
			-- Table reference
			tables[content] = tables[content] or {}
			counts[content] = counts[content] or 1
			return tables[content]
		end
		-- Null is unused
		error("unsupported type: " .. type_)
	end
	-- Order by key_content to have retrieve list parts in the correct order, making it easier for Lua
	for table_id, key_type, key, value_type, value in self.database:urows"SELECT * FROM table_entries ORDER BY table_id, key_type, key" do
		local table = tables[table_id] or {}
		counts[table] = counts[table] or 1
		table[get_value(key_type, key)] = get_value(value_type, value)
		tables[table_id] = table
	end
	if tables[1] then
		self.root = tables[1]
		counts[self.root] = counts[self.root] + 1
		self.table_ids = modlib.table.flip(tables)
		self:collectgarbage()
	else
		self.highest_table_id = 0
		self.table_ids = {}
		add_table(self, self.root)
	end
	databases[self] = true
end

function rewrite(self)
	exec(self, "DELETE FROM table_entries")
	self.highest_table_id = 0
	self.table_ids = {}
	self.counts = {}
	add_table(self, self.root)
end

function _ENV.set(self, table, key, value)
	local previous_value = table[key]
	if previous_value == value then
		-- no change
		return
	end
	set(self, table, key, value)
	table[key] = value
end

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

function collectgarbage(self)
	local marked = {}
	local function mark(table)
		if type(table) ~= "table" or marked[table] then return end
		marked[table] = true
		for k, v in pairs(table) do
			mark(k)
			mark(v)
		end
	end
	mark(self.root)
	for table in pairs(self.table_ids) do
		if not marked[table] then
			delete_table(self, table)
		end
	end
end

function defragment_ids(self)
	local ids = {}
	for _, id in pairs(self.table_ids) do
		table_insert(ids, id)
	end
	table_sort(ids)
	local update = self._prepared.update
	local tables = modlib.table.flip(self.table_ids)
	for new_id, old_id in ipairs(ids) do
		for _, stmt in pairs(update) do
			stmt:bind_values(new_id, old_id)
			stmt:step()
			stmt:reset()
		end
		self.table_ids[tables[old_id]] = new_id
	end
	self.highest_table_id = #ids
end

local function finalize_statements(table)
	for _, stmt in pairs(table) do
		if type(stmt) == "table" then
			finalize_statements(stmt)
		else
			local errcode = stmt:finalize()
			assert(errcode == sqlite3.OK, errcode)
		end
	end
end

function close(self)
	finalize_statements(self._prepared)
	self.database:close()
	databases[self] = nil
end

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

return _ENV