local store=minetest.get_mod_storage()

do -- DESTROY minetest's forceload limit with FACTS and LOGIC
	local smt=getmetatable(minetest.settings)
	local oldget=smt.get
	local forceload_block=minetest.forceload_block
	local unlimit
	function smt:get(key,...)
		if self==minetest.settings and key=="max_forceloaded_blocks" and unlimit then
			return "1e1000"
		end
		return oldget(self,key,...)
	end
	function minetest.forceload_block(...)
		unlimit=true
		local ret=forceload_block(...)
		unlimit=false
		return ret
	end
end
local forceload_block=minetest.forceload_block
local forceload_free_block=minetest.forceload_free_block

local function blockpos_to_id(bp)
	return ("%i,%i,%i"):format(bp.x,bp.y,bp.z)
end

local function id_to_blockpos(i)
	local x,y,z=i:match("([^,]+),([^,]+),([^,]+)")
	x,y,z=tonumber(x),tonumber(y),tonumber(z)
	assert(x and y and z,dump{x,y,z})
	return vector.new(x,y,z)
end

local persistent_floads=minetest.deserialize(store:get_string("forceloads")) or {}
local forceloads={
	blocks={},
	floads={}
}

local floaded=false
minetest.after(0,function()
	for name,block in pairs(persistent_floads) do
		forceloads.blocks[block]=forceloads.blocks[block] or {}
		forceloads.blocks[block][name]=1
		forceloads.floads[name]=block
		forceload_block(vector.multiply(id_to_blockpos(block),16),true)
	end
	floaded=true
end)

local planned=false
local function savepf()
	if planned then return end
	planned=true
	minetest.after(0,function()
		if not planned then return end
		store:set_string("forceloads",minetest.serialize(persistent_floads))
		planned=false
	end)
end

local lib={}

lib.FORCELOAD_DURING_INIT="can't forceload during init"
lib.NO_SUCH_FORCELOAD="no such forceload"

local start,stop,query

start = function(id,blockpos,persistent)
	if not floaded then 
		return false,lib.FORCELOAD_DURING_INIT
	end
	blockpos=vector.floor(blockpos)
	assert(type(id)=="string","id must be string")
	local bpi=blockpos_to_id(blockpos)

	if forceloads.floads[id] then
		if forceloads.floads[id]==bpi then
			if (not persistent)~=(not persistent_floads[id]) then
				persistent_floads[id]=persistent and bpi or nil
				forceloads.blocks[bpi][id]=persistent and 1 or 0
				savepf()
			end
			return true
		end
		stop(id)
	end

	forceloads.floads[id]=bpi

	local block=forceloads.blocks[bpi] or {}
	forceloads.blocks[bpi]=block
	local wasfree=not next(block)

	block[id]=persistent and 1 or 0
	if persistent then
		persistent_floads[id]=bpi
		savepf()
	end

	local pos=vector.multiply(blockpos,16)
	if wasfree then
		assert(forceload_block(pos,true),"forceload failed! shame on minetest")
	end
	return true
end

stop = function(id)
	local bpi=forceloads.floads[id]
	if not bpi then
		return false,lib.NO_SUCH_FORCELOAD
	end
	local block=forceloads.blocks[bpi] or {}
	assert(block[id],"forceload inconsistency")
	local persistent=block[id]==1
	if persistent then
		assert(persistent_floads[id],"persistent forceload inconsistency")
		persistent_floads[id]=nil
		savepf()
	end
	local wasfree=not next(block)
	block[id]=nil
	forceloads.floads[id]=nil
	block=next(block) and block or nil
	forceloads.blocks[bpi]=block
	local blockpos=id_to_blockpos(bpi)
	local pos=vector.multiply(blockpos,16)
	if not (wasfree or block) then
		forceload_free_block(pos,true)
	end
	return true
end

local function areaiter(id,op1,op2,fn)
	for y=op1.y,op2.y do
	for x=op1.x,op2.x do
	for z=op1.z,op2.z do
		local blockpos=vector.new(x,y,z)
		if not vector.equals(blockpos,op1) and
		   not vector.equals(blockpos,op2) then
			local name=("m%s:%s"):format(blockpos_to_id(blockpos),id)
			local ret={fn(blockpos,name)}
			if ret[1] then
				return unpack(ret,2)
			end
		end
	end end end
end

function lib.start(id,p1,D,E)
	local p2,persistent
	if type(D)=="table" and D.x then
		p2,persistent=D,E
	else
		p2,persistent=p1,D
	end
	p1,p2=vector.floor(p1),vector.floor(p2)
	p1,p2=vector.combine(p1,p2,math.min),vector.combine(p1,p2,math.max)

	local oldblocks={}
	local floads=forceloads.floads
	local op1,op2=floads["a:"..id],floads["b:"..id]
	op1=op1 and id_to_blockpos(op1)
	op2=op2 and id_to_blockpos(op2)
	if op2 then
		oldblocks["b:"..id]=true
	end
	if op2 and not op1 then error("forceload area inconsistency") end
	if op1 and op2 then
		areaiter(id,op1,op2,function(blockpos,name)
			assert(floads[name],"forceload area inconsistency")
			oldblocks[name]=true
		end)
	end

	local ok,err=start("a:"..id,p1,persistent)
	if not ok then return ok,err end
	if not vector.equals(p1,p2) then
		assert(start("b:"..id,p2,persistent))
		oldblocks["b:"..id]=nil
		areaiter(id,p1,p2,function(blockpos,name)
			assert(start(name,blockpos,persistent))
			oldblocks[name]=nil
		end)
	end

	for name,_ in pairs(oldblocks) do
		assert(stop(name),"forceload area inconsistency")
	end
	return true
end

function lib.stop(id)
	local floads=forceloads.floads
	local op1,op2=floads["a:"..id],floads["b:"..id]
	op1=op1 and id_to_blockpos(op1)
	op2=op2 and id_to_blockpos(op2)
	local ok,err=stop("a:"..id)
	if not ok then return ok,err end
	if op2 then
		assert(stop("b:"..id),"forceload area inconsistency")
		areaiter(id,op1,op2,function(blockpos,name)
			assert(stop(name),"forceload area inconsistency")
		end)
	end
	return true
end

local function ge(a,b) return a>=b and 0 or 1 end
local function le(a,b) return a<=b and 0 or 1 end

local function process_flist(fl)
	local floads={}
	for k,v in pairs(fl) do
		local c=k:sub(1,1)
		if c=="a" or c=="b" or c=="m" then
			local ct,id=k:match("(.-):(.+)")
			local mi,ma,pers
			local fload=floads[id]
			if not fload then
				local op1,op2=forceloads.floads["a:"..id],forceloads.floads["b:"..id]
				op1=op1 and id_to_blockpos(op1)
				op2=op2 and id_to_blockpos(op2)
				fload={
					pos1=op1,
					pos2=op2 or op1,
					persistent=v.persistent
				}
				floads[id]=fload
			end
		elseif c=="i" then
			local bpi,tra,n=k:match("i:(.-):(.-):(.+)")
			local blockpos=id_to_blockpos(bpi)
			n=tonumber(n) or error("wtf")
			local id="(minetest)"..tra..minetest.pos_to_string(blockpos)
			local fload=floads[id] or {
				pos1 = v.blockpos,
				pos2 = v.blockpos,
				persistent = v.persistent,
				count = -math.huge
			}
			floads[id]=fload
			fload.count=math.max(n+1,fload.count)
		end
	end
	return floads
end

query = function(obj)
	if not obj then
		local floads={}
		for k,v in pairs(forceloads.floads) do
			floads[k]=query(k)
		end
		return floads
	elseif type(obj)=="string" then
		local fload=forceloads.floads[obj]
		return fload and {
			blockpos = id_to_blockpos(fload),
			persistent = forceloads.blocks[fload][obj]==1
		} or nil
	end
	obj=vector.apply(obj,math.floor)
	local bpi=blockpos_to_id(obj)
	local floads={}
	for k,v in pairs(forceloads.blocks[bpi] or {}) do
		floads[k]={
			blockpos = vector.new(obj),
			persistent = v==1
		}
	end
	return floads
end

function lib.query(obj,o2)
	if not obj then
		return process_flist(query())
	elseif type(obj)=="string" then
		local mt,tra,p=obj:match("(%(minetest%))(.)(.+)")
		if mt then
			local blockpos=minetest.string_to_pos(p)
			local floads=process_flist(query(blockpos))
			return floads[obj]
		end
		local id="a:"..obj
		local floads=process_flist({[id]=query(id)})
		return floads[obj]
	end
	if o2 then
		local floads={}
		for x=obj.x,o2.x do
		for y=obj.y,o2.y do
		for z=obj.z,o2.z do
			for k,v in pairs(query(vector.new(x,y,z))) do
				floads[k]=v
			end
		end end end
		return process_flist(floads)
	end
	return process_flist(query(obj))
end

minetest.forceload_block=nil
minetest.forceload_free_block=nil

local function mtfbp(pos,transient,n)
	local blockpos=vector.apply(vector.multiply(pos,1/16),math.floor)
	local id=("i:%s:%s:%s"):format(blockpos_to_id(blockpos),transient and "T" or "P",n)
	return id,blockpos
end

function minetest.forceload_block(pos,transient)
	local n=0
	local id,blockpos=mtfbp(pos,transient,n)
	while query(id) do
		n=n+1
		id,blockpos=mtfbp(pos,transient,n)
	end
	return not not start(id,blockpos,not transient)
end

function minetest.forceload_free_block(pos,transient)
	local n=0
	local id=mtfbp(pos,transient,n)
	while query(id) do
		oldid=id
		n=n+1
		id=mtfbp(pos,transient,n)
	end
	if not oldid then return nil end
	return not not (stop(oldid))
end

_G[minetest.get_current_modname()]=lib

---[[ TEST ('---[[' = enabled, '--[[' = disabled)
do
	local function pwrap(fn)
		return function(...)
			local args={...}
			local ret={xpcall(function()return fn(unpack(args))end,debug.traceback)}
			if not ret[1] then
				return unpack(ret)
			end
			return unpack(ret,2)
		end
	end

	local function chatify(fname,fn)
		return pwrap(function(name,param)
			local ret={fn(unpack(minetest.deserialize(("return {%s}"):format(param))))}
			return true,fname..": "..dump(ret)
		end)
	end

	minetest.register_chatcommand("afl:start",{
		params = "{...}",
		privs = {privs=true},
		func = chatify("start",function(id,blockpos,b2,...)
			if type(b2)=="string" then
				b2=assert(minetest.string_to_pos(b2))
			end
			return lib.start(id,minetest.string_to_pos(blockpos),b2,...)
		end)
	})

	minetest.register_chatcommand("afl:stop",{
		params = "{...}",
		privs = {privs=true},
		func = chatify("stop",lib.stop)
	})

	minetest.register_chatcommand("afl:forceload_block",{
		params = "{...}",
		privs = {privs=true},
		func = chatify("forceload_block",minetest.forceload_block)
	})

	minetest.register_chatcommand("afl:forceload_free_block",{
		params = "{...}",
		privs = {privs=true},
		func = chatify("forceload_free_block",minetest.forceload_free_block)
	})

	minetest.register_chatcommand("afl:query",{
		params = "{...}",
		privs = {privs=true},
		func = chatify("query",lib.query)
	})

	minetest.register_chatcommand("afl:query_raw",{
		params = "{...}",
		privs = {privs=true},
		func = chatify("query_raw",query)
	})
	
	for k,v in ipairs{"unknown","emerging","loaded","active"} do
		minetest.register_chatcommand("afl:check_"..v,{
			params="{...}",
			privs = {privs=true},
			func = chatify("check_"..v,function(bp)
				return minetest.compare_block_status(vector.multiply(minetest.string_to_pos(bp),16),v)
			end)
		})
	end
end
--]]