-- LUALOCALS < ---------------------------------------------------------
local ipairs, klots, math, minetest, next, pairs, vector
    = ipairs, klots, math, minetest, next, pairs, vector
local math_random
    = math.random
-- LUALOCALS > ---------------------------------------------------------

local cooldown = {}
klots.klot_move_cooldown = cooldown

local vector_round = vector.round
local vector_equals = vector.equals
local vector_add = vector.add
local hash = minetest.hash_node_position

klots.tweenfrom = klots.tweenfrom or {}
local function trymovecore(startpos, dir)
	local now = minetest.get_us_time() / 1000000

	-- Results array; setting to nil aborts everything.
	local results = {}
	local function fail()
		results = nil
		return false
	end

	-- Detect which nodes are blocked by players
	local pblocked = {}
	for _, player in ipairs(minetest.get_connected_players()) do
		local ppos = player:get_pos()
		local pname = player:get_player_name()
		local function addpb(dx, dy, dz)
			pblocked[hash(vector_round({
						x = ppos.x + dx,
						y = ppos.y + dy,
						z = ppos.z + dz
					}))] = pname
		end
		addpb(-0.3, 0, -0.3)
		addpb(-0.3, 0, 0.3)
		addpb(0.3, 0, -0.3)
		addpb(0.3, 0, 0.3)
		addpb(-0.3, 1, -0.3)
		addpb(-0.3, 1, 0.3)
		addpb(0.3, 1, -0.3)
		addpb(0.3, 1, 0.3)
		addpb(-0.3, 1.8, -0.3)
		addpb(-0.3, 1.8, 0.3)
		addpb(0.3, 1.8, -0.3)
		addpb(0.3, 1.8, 0.3)
	end

	-- Scan for all matching nodes.
	local rootcolor
	local blockplayers = {}
	klots.scan_flood(startpos, function(pos)
			if not results then return false end

			-- Cooldown on any node blocks the whole thing from moving.
			local key = hash(pos)
			local cd = cooldown[key]
			if cd and cd > now then return fail() end

			-- Any ignores abort the whole opertion, as we cannot tell
			-- whether something that should move might be sheared off.
			local node = minetest.get_node_or_nil(pos)
			if not node then return fail() end

			-- Unknown nodes do not engage with klots.
			local def = minetest.registered_nodes[node.name]
			if not def then return false end

			-- Skip anything that isn't a klot or is a non-matching color.
			local color = def.groups and def.groups.klots_color
			color = color and color > 0 and color
			if not color then return false end
			if (not rootcolor) and vector_equals(pos, startpos) then
				rootcolor = color
			elseif color ~= rootcolor then
				return false
			end

			-- Make sure destination isn't blocked by a player, but
			-- don't abort propagating movement yet, so we can find
			-- all players affected.
			local to_pos = vector_add(pos, dir)
			local to_hash = hash(to_pos)
			local blocker = pblocked[to_hash]
			if blocker then
				blockplayers[blocker] = pos
			end

			-- If a matching klot is locked and not allowed to move in this
			-- direction, then abort everything.
			if def.klot_move_allow and not def.klot_move_allow(pos, node, dir)
			then return fail() end

			local res = {from = pos, to = to_pos, def = def}
			results[key] = res
		end)
	if not results then return end

	-- Validate all results and ensure they can actually move to their
	-- given destination.
	for _, res in pairs(results) do
		-- Spots occupied by a klot node that will also be moving can
		-- be occupied by a klot node.
		local key = hash(res.to)
		res.tohash = key
		local overwritten = results[key]
		if overwritten then
			overwritten.overwritten = true
		else
			-- Cannot move into an unloaded area.
			local node = minetest.get_node_or_nil(res.to)
			if not node then return end
			-- Only known nodes that are buildable_to can be overwritten.
			local def = minetest.registered_nodes[node.name]
			if not (def and def.buildable_to) then return end
		end
	end

	-- If any players are blocking movement, return an "on failure"
	-- function to have the klot shove them.
	if next(blockplayers) then
		return false, function()
			for pname, pos in pairs(blockplayers) do
				klots.node_sound(pos, "hitplayer")
				klots.player_control_apply(pname, function(obj)
						return obj and obj:add_velocity({
								x = (dir.x + math_random() - 0.5) * 2,
								y = (dir.y + math_random() - 0.5) * 2,
								z = (dir.z + math_random() - 0.5) * 2
							})
					end)
			end
		end
	end

	-- Return a "commit" function.
	return function()
		-- "Pick up" all nodes.
		for _, res in pairs(results) do
			res.node = minetest.get_node(res.from)
			if not res.overwritten then
				minetest.remove_node(res.from)
				klots.klot_update_ents(res.from)
			end
		end
		-- "Put down" all nodes in new places and set cooldown
		local cd = now + 0.25
		local prob = 1
		for _, res in pairs(results) do
			local oldres = results[res.tohash]
			local oldnode = oldres and oldres.node or minetest.get_node(res.to)
			klots.tweenfrom[res.tohash] = res.from
			if oldnode.name ~= res.node.name or oldnode.param2 ~= res.node.param2
			then minetest.set_node(res.to, res.node) end
			klots.klot_update_ents(res.to)
			cooldown[res.tohash] = cd
			if math_random() <= prob then
				klots.node_sound(res.to, "place")
				prob = prob / 2
			end
		end
		return true
	end
end

function klots.klot_try_move(startpos, dir)
	local len = vector.length(dir)
	if len == 1 then return trymovecore(startpos, dir) end
	local norm = vector.normalize(dir)
	local fallback = nil
	for i = 1, len do
		local ok, fail = trymovecore(startpos, vector.multiply(norm, i))
		if ok then
			fallback = ok
		else
			return (not fail) and fallback or nil, fail
		end
	end
	return fallback
end
