-- LUALOCALS < ---------------------------------------------------------
local minetest, nodecore, pairs, vector
    = minetest, nodecore, pairs, vector
-- LUALOCALS > ---------------------------------------------------------

local modstore = minetest.get_mod_storage()

local hashpos = minetest.pos_to_string

local function hingeaxis(pos, node)
	local fd = node and node.param2 or 0
	fd = nodecore.facedirs[fd]
	fd = vector.multiply(vector.add(fd.f, fd.r), 0.5)
	return {
		x = fd.x == 0 and 0 or pos.x + fd.x,
		y = fd.y == 0 and 0 or pos.y + fd.y,
		z = fd.z == 0 and 0 or pos.z + fd.z
	}
end

local convey = {}

local function movecheck(okay, seg, to, tkey, collide)
	if (not nodecore.buildable_to(to))
	or nodecore.obstructed(to) or collide[tkey]
	then return end
	for x in pairs(seg) do okay[x] = true end
	collide[tkey] = true
	return true
end

local function conveytrace(okay, seg, u, collide)
	if u.tkey2 then
		local w = convey[u.tkey2]
		if w then return w end
		if movecheck(okay, seg, u.to2, u.tkey2, collide) then
			u.to = u.to2
			u.tkey = u.tkey2
			return
		end
	end
	local w = convey[u.tkey]
	if w then return w end
	movecheck(okay, seg, u.to, u.tkey, collide)
end

local function set_node(pos, node)
	local exists = minetest.get_node(pos)
	if exists.name ~= node.name
	or exists.param ~= node.param
	or exists.param2 ~= node.param2 then
		return minetest.set_node(pos, node)
	end
end

nodecore.register_globalstep("door conveyance", function()
		local nonheads = {}
		for k, v in pairs(convey) do
			local node = minetest.get_node(v.from)
			if node.name ~= v.node.name
			or node.param ~= v.node.param
			or node.param2 ~= v.node.param2 then
				convey[k] = nil
			else
				if v.tkey2 and (convey[v.tkey2] or nodecore.buildable_to(v.to2)) then
					nonheads[v.tkey2] = true
				else
					nonheads[v.tkey] = true
				end
			end
		end
		local okay = {}
		local collide = {}
		for k, v in pairs(convey) do
			if not nonheads[k] then
				local seg = {}
				local u = v
				while u and not seg[u] do
					seg[u] = true
					u = conveytrace(okay, seg, u, collide)
				end
				for x in pairs(seg) do
					convey[x.fkey] = nil
				end
			end
		end
		for _, v in pairs(convey) do
			okay[v] = true
		end
		convey = {}

		local air = {name = "air"}
		local toset = {}
		for v in pairs(okay) do
			toset[v.fkey] = {pos = v.from, node = air}
		end
		for v in pairs(okay) do
			toset[v.tkey] = {
				pos = v.to,
				node = v.node,
				meta = minetest.get_meta(v.from):to_table()
			}
		end
		for _, v in pairs(toset) do
			set_node(v.pos, v.node)
			if v.meta then
				minetest.get_meta(v.pos):from_table(v.meta)
				nodecore.visinv_update_ents(v.pos)
			end
			local def = minetest.registered_nodes[v.node.name]
			if def and def.on_door_conveyed then
				def.on_door_conveyed(v.pos, v.node)
			end
			nodecore.fallcheck(v.pos)
		end
	end)

local is_falling = {groups = {falling_node = true}}

local function trypush(pos, dir, dir2)
	local node = minetest.get_node(pos)
	if not nodecore.match(node, is_falling) then return end

	local data = {
		from = pos,
		fkey = hashpos(pos),
		to = vector.add(pos, dir),
		node = node,
	}
	data.tkey = hashpos(data.to)
	if dir2 then
		data.to2 = vector.add(pos, dir2)
		data.tkey2 = hashpos(data.to2)
	end
	convey[data.fkey] = data
end

local squelch = modstore:get_string("squelch")
squelch = squelch and squelch ~= "" and minetest.deserialize(squelch) or {}
nodecore.register_globalstep("door squelch", function(dtime)
		for k, v in pairs(squelch) do
			squelch[k] = (v > dtime) and (v - dtime) or nil
		end
		modstore:set_string("squelch", squelch)
	end)

local is_door = {groups = {door = true}}

function nodecore.operate_door(pos, node, dir)
	local key = hashpos(pos)
	if squelch[key] then return end
	node = node or minetest.get_node_or_nil(pos)
	if (not node) or (not nodecore.match(node, is_door)) then return end

	local fd = nodecore.facedirs[node.param2 or 0]
	local rotdir
	if vector.equals(dir, fd.k) or vector.equals(dir, fd.r) then
		rotdir = "r"
	elseif vector.equals(dir, fd.l) or vector.equals(dir, fd.f) then
		rotdir = "f"
	else return end

	local found = {}
	local hinge = hingeaxis(pos, node)
	if nodecore.scan_flood(pos, 128, function(p)
			local n = minetest.get_node_or_nil(p)
			if not n then return true end
			if (not nodecore.match(n, is_door))
			or (not vector.equals(hingeaxis(p, n), hinge)) then return false end
			found[hashpos(p)] = {pos = p, node = n}
		end
	) then return end

	local press
	local toop = {}
	for k, v in pairs(found) do
		local ffd = nodecore.facedirs[v.node.param2 or 0]
		v.dir = ffd[rotdir]
		v.dir2 = rotdir == "r" and ffd.k or ffd.l
		local to = vector.add(v.pos, v.dir)

		if (not found[hashpos(to)])
		and (not nodecore.buildable_to(to))
		then
			if press then return end
			press = {
				pos = to,
				dir = v.dir,
				from = v
			}
		end

		if nodecore.obstructed(to) then return end

		local str = hashpos(to)
		if squelch[str] then return end

		v.str = str
		v.to = to
		v.fd = ffd

		toop[k .. "l"] = {
			pos = vector.add(v.pos, ffd.l),
			dir = rotdir == "r" and ffd.k or ffd.f,
			from = v
		}
		toop[k .. "k"] = {
			pos = vector.add(v.pos, ffd.k),
			dir = rotdir == "r" and ffd.r or ffd.l,
			from = v
		}
	end
	if press then
		local data = {
			action = "press",
			pointed = {
				["type"] = "node",
				above = vector.subtract(press.pos, press.dir),
				under = press.pos
			},
			axis = hinge
		}
		if nodecore.craft_check(press.pos, minetest.get_node(press.pos), data) then
			nodecore.sound_play("nc_doors_operate",
				{pos = press.pos, gain = 0.5})
			return true
		end
		return
	end

	local toset = {}
	for k, v in pairs(found) do
		toset[k] = {pos = v.pos, name = "air", param2 = 0}
		squelch[k] = 0.5
		squelch[v.str] = 0.5
	end
	for _, v in pairs(found) do
		for i, xfd in pairs(nodecore.facedirs) do
			if vector.equals(xfd.t, v.fd.t)
			and vector.equals(xfd.r, rotdir == "r" and v.fd.f or v.fd.k) then
				toset[hashpos(v.to)] = {
					pos = v.to,
					name = v.node.name,
					param2 = i
				}
				break
			end
		end
	end

	for _, v in pairs(toset) do
		set_node(v.pos, v)
		if v.name ~= "air" then
			local p = vector.round(vector.multiply(v.pos, 0.25))
			local k = "sfx" .. hashpos(p)
			if not squelch[k] then
				squelch[k] = 0
				nodecore.sound_play("nc_doors_operate",
					{pos = v.pos, gain = 0.5})
			end
		else
			nodecore.fallcheck({x = pos.x, y = pos.y + 1, z = pos.z})
		end
	end
	for _, v in pairs(found) do
		trypush({x = v.pos.x, y = v.pos.y + 1, z = v.pos.z}, v.dir, v.dir2)
	end
	for _, v in pairs(toop) do
		nodecore.operate_door(v.pos, nil, v.dir)
		trypush(v.pos, v.dir)
	end
	return true
end
