moremesecons = {}

function moremesecons.setting(modname, settingname, default, min)
	local setting = "moremesecons_" .. modname .. "." .. settingname

	if type(default) == "boolean" then
		local ret = minetest.settings:get_bool(setting)
		if ret == nil then
			ret = default
		end
		return ret
	elseif type(default) == "string" then
		return minetest.settings:get(setting) or default
	elseif type(default) == "number" then
		local ret = tonumber(minetest.settings:get(setting)) or default
		if not ret then
			minetest.log("warning", "[moremesecons_"..modname.."]: setting '"..setting.."' must be a number. Set to default value ("..tostring(default)..").")
			ret = default
		elseif ret ~= ret then -- NaN
			minetest.log("warning", "[moremesecons_"..modname.."]: setting '"..setting.."' is NaN. Set to default value ("..tostring(default)..").")
			ret = default
		end
		if min and ret < min then
			minetest.log("warning", "[moremesecons_"..modname.."]: setting '"..setting.."' is under minimum value "..tostring(min)..". Set to minimum value ("..tostring(min)..").")
			ret = min
		end
		return ret
	end
end

-- Storage helpers

function moremesecons.get_storage_data(storage, name)
	return {
		tab = minetest.deserialize(storage:get_string(name)) or {},
		name = name,
		storage = storage
	}
end

function moremesecons.set_data_to_pos(sto, pos, data)
	sto.tab[minetest.hash_node_position(pos)] = data
	sto.storage:set_string(sto.name, minetest.serialize(sto.tab))
end

function moremesecons.get_data_from_pos(sto, pos)
	return sto.tab[minetest.hash_node_position(pos)]
end

function moremesecons.remove_data_from_pos(sto, pos)
	sto.tab[minetest.hash_node_position(pos)] = nil
	sto.storage:set_string(sto.name, minetest.serialize(sto.tab))
end

-- Some additional vector helpers

-- The same as minetest.hash_node_position; I copied it to ensure backwards
-- compatibility and used hexadecimal number notation
local function node_position_key(pos)
	return (pos.z + 0x8000) * 0x10000 * 0x10000
		 + (pos.y + 0x8000) * 0x10000
		 +  pos.x + 0x8000
end

local MapDataStorage = {}
setmetatable(MapDataStorage, {__call = function()
	local obj = {}
	setmetatable(obj, MapDataStorage)
	return obj
end})
MapDataStorage.__index = {
	getAt = function(self, pos)
		return self[node_position_key(pos)]
	end,
	setAt = function(self, pos, data)
		-- If x, y or z is omitted, the key corresponds to a position outside
		-- of the map (hopefully), so it can be used to skip lines and planes
		local vi_z = (pos.z + 0x8000) * 0x10000 * 0x10000
		local vi_zy = vi_z + (pos.y + 0x8000) * 0x10000
		local vi = vi_zy + pos.x + 0x8000
		local is_new = self[vi] == nil
		self[vi] = data
		if is_new then
			self[vi_z] = (self[vi_z] or 0) + 1
			self[vi_zy] = (self[vi_zy] or 0) + 1
		end
	end,
	setAtI = function(self, vi, data)
		local vi_zy = vi - vi % 0x10000
		local vi_z = vi - vi % (0x10000 * 0x10000)
		local is_new = self[vi] == nil
		self[vi] = data
		if is_new then
			self[vi_z] = (self[vi_z] or 0) + 1
			self[vi_zy] = (self[vi_zy] or 0) + 1
		end
	end,
	removeAt = function(self, pos)
		local vi_z = (pos.z + 0x8000) * 0x10000 * 0x10000
		local vi_zy = vi_z + (pos.y + 0x8000) * 0x10000
		local vi = vi_zy + pos.x + 0x8000
		if self[vi] == nil then
			-- Nothing to remove
			return
		end
		self[vi] = nil
		-- Update existence information for the xy plane and x line
		self[vi_z] = self[vi_z] - 1
		if self[vi_z] == 0 then
			self[vi_z] = nil
			self[vi_zy] = nil
			return
		end
		self[vi_zy] = self[vi_zy] - 1
		if self[vi_zy] == 0 then
			self[vi_zy] = nil
		end
	end,
	iter = function(self, pos1, pos2)
		local ystride = 0x10000
		local zstride = 0x10000 * 0x10000

		-- Skip z values where no data can be found
		pos1 = vector.new(pos1)
		local vi_z = (pos1.z + 0x8000) * 0x10000 * 0x10000
		while not self[vi_z] do
			pos1.z = pos1.z + 1
			vi_z = vi_z + zstride
			if pos1.z > pos2.z then
				-- There are no values to iterate through
				return function() return end
			end
		end
		-- Skipping y values is not yet implemented and may require much code

		local xrange = pos2.x - pos1.x + 1
		local yrange = pos2.y - pos1.y + 1
		local zrange = pos2.z - pos1.z + 1

		-- x-only and y-only parts of the vector index of pos1
		local vi_y = (pos1.y + 0x8000) * 0x10000
		local vi_x =  pos1.x + 0x8000

		local y = 0
		local z = 0

		local vi = node_position_key(pos1)
		local pos = vector.new(pos1)
		local nextaction = vi + xrange
		pos.x = pos.x - 1
		vi = vi - 1
		local function iterfunc()
			-- continue along x until it needs to jump
			vi = vi + 1
			pos.x = pos.x + 1
			if vi ~= nextaction then
				local v = self[vi]
				if v == nil then
					-- No data here
					return iterfunc()
				end
				-- The returned position must not be changed
				return pos, v
			end

			-- Reset x position
			vi = vi - xrange
			-- Go along y until pos2.y is exceeded
			while true do
				y = y + 1
				pos.y = pos.y + 1
				-- Set vi to index(pos1.x, pos1.y + y, pos1.z + z)
				vi = vi + ystride
				if y == yrange then
					break
				end
				if self[vi - vi_x] then
					nextaction = vi + xrange

					vi = vi - 1
					pos.x = pos1.x - 1
					return iterfunc()
				end
				-- Nothing along this x line, so increase y again
			end

			-- Go back along y
			vi = vi - yrange * ystride
			y = 0
			pos.y = pos1.y
			-- Go along z until pos2.z is exceeded
			while true do
				z = z + 1
				pos.z = pos.z + 1
				vi = vi + zstride
				if z == zrange then
					-- Cuboid finished, return nil
					return
				end
				if self[vi - vi_x - vi_y] then
					y = 0
					nextaction = vi + xrange

					vi = vi - 1
					pos.x = pos1.x - 1
					return iterfunc()
				end
				-- Nothing in this xy plane, so increase z again
			end
		end
		return iterfunc
	end,
	iterAll = function(self)
		local previous_vi = nil
		local function iterfunc()
			local vi, v = next(self, previous_vi)
			previous_vi = vi
			if not vi then
				return
			end
			local z = math.floor(vi / (0x10000 * 0x10000))
			vi = vi - z * 0x10000 * 0x10000
			local y = math.floor(vi / 0x10000)
			if y == 0 or z == 0 then
				-- The index does not refer to a position inside the map
				return iterfunc()
			end
			local x = vi - y * 0x10000 - 0x8000
			y = y - 0x8000
			z = z - 0x8000
			return {x=x, y=y, z=z}, v
		end
		return iterfunc
	end,
	serialize = function(self)
		local indices = {}
		local values = {}
		local i = 1
		for pos, v in self:iterAll() do
			local vi = node_position_key(pos)
			-- Convert the double reversible to a string;
			-- minetest.serialize does not (yet) do this
			indices[i] = ("%.17g"):format(vi)
			values[i] = v
		end
		result = {
			version = "MapDataStorage_v1",
			indices = "return {" .. table.concat(indices, ",") .. "}",
			values = minetest.serialize(values),
		}
		return minetest.serialize(result)
	end,
}
MapDataStorage.deserialize = function(txtdata)
	local data = minetest.deserialize(txtdata)
	if data.version ~= "MapDataStorage_v1" then
		minetest.log("error", "Unknown MapDataStorage version: " ..
			data.version)
	end
	-- I assume that minetest.deserialize correctly deserializes the indices,
	-- which are in the %a format
	local indices = minetest.deserialize(data.indices)
	local values = minetest.deserialize(data.values)
	if not indices or not values then
		return MapDataStorage()
	end
	data = MapDataStorage()
	for i = 1,#indices do
		local vi = indices[i]
		local v = values[i]
		data:setAtI(vi, v)
	end
	return data
end
moremesecons.MapDataStorage = MapDataStorage


-- Legacy

-- vector_extras there: https://github.com/HybridDog/vector_extras
-- Creates a MapDataStorage object from old vector_extras generated table
function moremesecons.load_old_data_from_pos(t)
	local data = MapDataStorage()
	for z, yxv in pairs(t) do
		for y, xv in pairs(yxv) do
			for x, v in pairs(xv) do
				data:setAt({x=x, y=y, z=z}, v)
			end
		end
	end
	return data
end

function moremesecons.load_old_dfp_storage(modstorage, name)
	local data = minetest.deserialize(modstorage:get_string(name))
	if not data then
		return
	end
	return moremesecons.load_old_data_from_pos(data)
end

function moremesecons.load_MapDataStorage_legacy(modstorage, name, oldname)
	local t_old = moremesecons.load_old_dfp_storage(modstorage, oldname)
	local t
	if t_old and t_old ~= "" then
		t = t_old
		modstorage:set_string(name, t:serialize())
		modstorage:set_string(oldname, nil)
		return t
	end
	t = modstorage:get_string(name)
	if t and t ~= "" then
		return MapDataStorage.deserialize(t)
	end
	return MapDataStorage()
end



-- This testing code shows an example usage of the MapDataStorage code
local function do_test()
	print("Test if iter returns correct positions when a lot is set")
	local data = MapDataStorage()
	local k = 0
	for x = -5, 3 do
		for y = -5, 3 do
			for z = -5, 3 do
				k = k + 1
				data:setAt({x=x, y=y, z=z}, k)
			end
		end
	end
	local expected_positions = {}
	for z = -4, 2 do
		for y = -4, 2 do
			for x = -4, 2 do
				expected_positions[#expected_positions+1] = {x=x, y=y, z=z}
			end
		end
	end
	local i = 0
	for pos, v in data:iter({x=-4, y=-4, z=-4}, {x=2, y=2, z=2}) do
		i = i + 1
		assert(vector.equals(pos, expected_positions[i]))
	end

	print("Test if iter works correctly on a corner")
	local found = false
	for pos, v in data:iter({x=-8, y=-7, z=-80}, {x=-5, y=-5, z=-5}) do
		assert(not found)
		found = true
		assert(vector.equals(pos, {x=-5, y=-5, z=-5}))
	end
	assert(found)

	print("Test if iter finds all corners")
	local expected_positions = {}
	local k = 1
	for _, z in ipairs({-9, -6}) do
		for _, y in ipairs({-9, -6}) do
			for _, x in ipairs({-8, -6}) do
				local pos = {x=x, y=y, z=z}
				expected_positions[#expected_positions+1] = pos
				data:setAt(pos, k)
				k = k + 1
			end
		end
	end
	local i = 1
	for pos, v in data:iter({x=-8, y=-9, z=-9}, {x=-6, y=-6, z=-6}) do
		assert(v == i)
		assert(vector.equals(pos, expected_positions[i]))
		i = i + 1
		--~ print("found " .. minetest.pos_to_string(pos))
	end
	assert(i == 8 + 1, "Not enough or too many corners found")

	--~ data:iterAll()
end
--~ do_test()