--[[

	TechAge
	=======

	Copyright (C) 2019-2020 Joachim Stolberg

	AGPL v3
	See LICENSE.txt for more information

	Data storage system for node related volatile and non-volatile data.
	Non-volatile data is stored from time to time and at shutdown.
	Volatile data is lost at every shutdown.

]]--

local NvmStore = {}  -- non-volatile data cache
local MemStore = {}  -- volatile data cache

local N = function(pos) print(minetest.pos_to_string(pos), minetest.get_node(pos).name) end

-------------------------------------------------------------------
-- Backend
-------------------------------------------------------------------
local MP = minetest.get_modpath("techage")
local techage_use_sqlite = minetest.settings:get_bool('techage_use_sqlite', false)
local backend

if techage_use_sqlite then
    backend = dofile(MP .. "/basis/nodedata_sqlite.lua")
else
    backend = dofile(MP .. "/basis/nodedata_meta.lua")
end

-- return keys for mapblock and inner-mapblock addressing based on the node position
local function get_keys(pos)
	local kx1, kx2 = math.floor(pos.x / 16) + 2048, pos.x % 16
	local ky1, ky2 = math.floor(pos.y / 16) + 2048, pos.y % 16
	local kz1, kz2 = math.floor(pos.z / 16) + 2048, pos.z % 16
	return kx1 * 4096 * 4096 + ky1 * 4096 + kz1, kx2 * 16 * 16 + ky2 * 16 + kz2
end

local function pos_from_key(key1, key2)

	local x1 = (math.floor(key1 / (4096 * 4096)) - 2048)  * 16
	local y1 = ((math.floor(key1 / 4096) % 4096) - 2048)  * 16
	local z1 = ((key1 % 4096) - 2048)  * 16
	local x2 = math.floor(key2 / (16 * 16))
	local y2 = math.floor(key2 / 16) % 16
	local z2 = key2 % 16

	return {x = x1 + x2, y = y1 + y2, z = z1 + z2}
end

local function debug(key1, item)
	--local pos1 = pos_from_key(key1, 0)
	--local pos2 = {x = pos1.x + 15, y = pos1.y + 15, z = pos1.z + 15}
	--techage.mark_region("mapblock", pos1, pos2, "singleplayer", 5)

	local cnt = 0
	for key2, tbl in pairs(item) do
		if key2 ~= "in_use" then
			cnt = cnt + 1
			--N(pos_from_key(key1, key2))
		end
	end
	print("mapblock", string.format("%09X", key1), cnt.." nodes")
end


-------------------------------------------------------------------
-- Storage scheduler
-------------------------------------------------------------------
local CYCLE_TIME = 600  -- store data every 10 min
local JobQueue = {}
local first = 0
local last = -1
local SystemTime = 0

local function push(key)
	last = last + 1
	JobQueue[last] = {key = key, time = SystemTime + CYCLE_TIME}
end

local function pop()
	if first > last then return end
	local item = JobQueue[first]
	if item.time <= SystemTime then
		JobQueue[first] = nil -- to allow garbage collection
		first = first + 1
		return item.key
	end
end

-- check every 100 msec if any data has to be stored
minetest.register_globalstep(function(dtime)
	SystemTime = SystemTime + dtime
	local key = pop()
	if key and NvmStore[key] then
--		minetest.log("warning",
--			string.format("[TA Storage] SystemTime = %.3f, #JobQueue = %d, in_use = %s",
--			SystemTime, last - first, NvmStore[key].in_use))
		local t = minetest.get_us_time()
		if NvmStore[key].in_use then
			NvmStore[key].in_use = nil
			backend.store_mapblock_data(key, NvmStore[key])
			push(key)
		else
			NvmStore[key] = nil -- remove unused data from cache
		end
		t = minetest.get_us_time() - t
		if t > 20000 then
			minetest.log("warning", "[TA Storage] duration = "..(t/1000.0).." ms")
		end
	end
end)

-------------------------------------------------------------------
-- Store/Restore NVM data
-------------------------------------------------------------------
NvmStore = backend.restore_at_startup()

minetest.register_on_shutdown(function()
	backend.freeze_at_shutdown(NvmStore)
end)

-------------------------------------------------------------------
-- API functions
-------------------------------------------------------------------
-- Returns volatile node data as table
function techage.get_mem(pos)
	local hash = minetest.hash_node_position(pos)
	if not MemStore[hash] then
		MemStore[hash] = {}
	end
	return MemStore[hash]
end

-- Returns non-volatile node data as table
function techage.get_nvm(pos)
	local key1, key2 = get_keys(pos)

	if not NvmStore[key1] then
		NvmStore[key1] = backend.get_mapblock_data(key1)
		push(key1)
	end

	local block = NvmStore[key1]
	block.in_use = true
	if not block[key2] then
		block[key2] = backend.get_node_data(pos)
	end
	return block[key2]
end

-- Returns true/false
function techage.has_nvm(pos)
	local key1, key2 = get_keys(pos)

	if not NvmStore[key1] then
		NvmStore[key1] = backend.get_mapblock_data(key1)
		push(key1)
	end

	return NvmStore[key1][key2] ~= nil
end

function techage.peek_nvm(pos)
	local key1, key2 = get_keys(pos)
	local block = NvmStore[key1] or {}
	return block[key2] or {}
end

-- To be called when a node is removed
function techage.del_mem(pos)
	local hash = minetest.hash_node_position(pos)
	MemStore[hash] = nil

	local key1, key2 = get_keys(pos)
	NvmStore[key1] = NvmStore[key1] or backend.get_mapblock_data(key1)
	NvmStore[key1][key2] = nil
	backend.store_mapblock_data(key1, NvmStore[key1])
end
