-- LUALOCALS < ---------------------------------------------------------
local get_mod_api, minetest, next, pairs, piredo_schems, posrel,
      startswith, string, type, vector
    = get_mod_api, minetest, next, pairs, piredo_schems, posrel,
      startswith, string, type, vector
local string_format
    = string.format
-- LUALOCALS > ---------------------------------------------------------

local api = get_mod_api()

local modstore = minetest.get_mod_storage()
local roomkey = "changes_room"
local poskey = "changes_pos"
local roompref = "changes_for_"

local suspended

local getchanges
do
	local cached = {}
	getchanges = function(room)
		local found = cached[room]
		if found then return found end
		local s = modstore:get_string(roompref .. room) or ""
		found = s and s ~= "" and minetest.deserialize(s) or {}
		cached[room] = found
		return found
	end
end

function api.get_current_room()
	local room = modstore:get_string(roomkey)
	room = room ~= "" and room or nil
	if not room then return end
	local roompos = modstore:get_string(poskey)
	roompos = roompos and roompos ~= "" and minetest.deserialize(roompos)
	local rsize = startswith(room, "large_") and 40 or 20
	local roommax = posrel(roompos)(rsize, 0, rsize)
	return room, roompos, roommax
end

function api.set_current_room(pos, room)
	modstore:set_string(poskey, minetest.serialize(pos))
	modstore:set_string(roomkey, room)
	local changes = getchanges(room)
	if not next(changes) then return end
	suspended = true
	local count = 0
	for _, change in pairs(changes) do
		local npos = vector.add(pos, change.pos)
		minetest.swap_node(npos, change.node)
		if change.meta then minetest.get_meta(npos):from_table(change.meta) end
		count = count + 1
	end
	suspended = nil
	return minetest.log("action", string_format("loaded %d change(s) for %q", count, room))
end

local function samenode(a, b)
	return a.name == b.name and a.param2 == b.param2
end

local getmeta
do
	local function serializable(obj)
		if type(obj) == "userdata" then
			return obj:to_string()
		elseif type(obj) == "table" then
			local t = {}
			for k, v in pairs(obj) do
				t[serializable(k)] = serializable(v)
			end
			return t
		end
		return obj
	end
	getmeta = function(pos)
		if #minetest.find_nodes_with_meta(pos, pos) < 1 then return end
		return serializable(minetest.get_meta(pos):to_table())
	end
end

local function savedata(room, data)
	local count = 0
	for _ in pairs(data) do count = count + 1 end
	modstore:set_string(roompref .. room, minetest.serialize(data))
	return minetest.log("action", string_format("saved %d change(s) for %q", count, room))
end

function api.transfer_changes(toroom)
	local room = modstore:get_string(roomkey)
	if not (room and room ~= "") then return end

	local dest = getchanges(toroom)
	for k, v in pairs(getchanges(room)) do
		dest[k] = v
	end

	savedata(toroom, dest)
end

local function savechange(pos, oldnode, reason)
	if suspended then return end

	local room = modstore:get_string(roomkey)
	if not (room and room ~= "") then return end
	if piredo_schems[room].disallow_changes then return end
	local roompos = modstore:get_string(poskey)
	roompos = roompos and roompos ~= "" and minetest.deserialize(roompos)
	if not roompos then return end

	local newnode = minetest.get_node(pos)
	local relpos = vector.round(vector.subtract(pos, roompos))

	local key = minetest.pos_to_string(relpos)
	local data = getchanges(room)
	local entry = data[key]
	if entry then
		local meta = getmeta(pos)
		if (not meta) and samenode(newnode, entry.original) then
			minetest.log("action", string_format("restored %s in %q to %q by %q",
					minetest.pos_to_string(relpos), room, newnode.name,
					reason or "unknown"))
			data[key] = nil
		else
			minetest.log("action", string_format("re-changed %s in %q to %q, default %q by %q",
					minetest.pos_to_string(relpos), room, newnode.name,
					entry.original.name, reason or "unknown"))
			entry.node = newnode
			entry.meta = meta
		end
	else
		minetest.log("action", string_format("changed %s in %q to %q, default %q by %q",
				minetest.pos_to_string(relpos), room, newnode.name, oldnode.name,
				reason or "unknown"))
		data[key] = {
			original = oldnode,
			node = newnode,
			meta = getmeta(pos),
			pos = relpos

		}
	end

	savedata(room, data)
end
api.savechange = savechange

local inhook = {}
local function hook(name)
	local old = minetest[name]
	minetest[name] = function(pos, ...)
		local key = minetest.hash_node_position(pos)
		if inhook[key] then return old(pos, ...) end
		inhook[key] = true
		local oldnode = minetest.get_node(pos)
		local function helper(...)
			savechange(pos, oldnode, name)
			inhook[key] = nil
			return ...
		end
		return helper(old(pos, ...))
	end
end
hook("set_node")
hook("remove_node")
hook("swap_node")
hook("add_node")
hook("place_node")
hook("dig_node")
