-- LUALOCALS < ---------------------------------------------------------
local core, math, nc, pairs, table, type, vector
    = core, math, nc, pairs, table, type, vector
local math_pi, table_remove
    = math.pi, table.remove
-- LUALOCALS > ---------------------------------------------------------

local modname = core.get_current_modname()

local seethru = {}
core.after(0, function()
		for name, def in pairs(core.registered_nodes) do
			if (core.get_item_group(name, "witness_opaque") == 0)
			and (
				core.get_item_group(name, "witness_transparent") > 0
				or def.sunlight_propagates
				or (
					def.paramtype == "light"
					and def.alpha
					and def.alpha > 0
					and def.alpha < 255
				)

			) then
				seethru[name] = true
			end
		end
	end)

local metakey = modname .. "_witness"
local cache = {}
local function witnessdata(player)
	local pname = player:get_player_name()
	local meta = player:get_meta()
	local data = cache[pname]
	if not data then
		data = meta:get_string(metakey) or ""
		data = data and data ~= "" and core.deserialize(data)
		or {queue = {}, lookup = {}}
		cache[pname] = data
	end
	return data, function() return meta:set_string(metakey, core.serialize(data)) end
end

local function canwitnessnow(player, pos)
	local ppos = player:get_pos()
	ppos.y = ppos.y + player:get_properties().eye_height

	if vector.distance(ppos, pos) < 0.5 then return true end

	local look = player:get_look_dir()
	local targ = vector.normalize(vector.subtract(pos, ppos))
	if vector.angle(look, targ) > math_pi / 8 then return end

	local rp = vector.round(pos)
	for pt in core.raycast(pos, ppos, false, true) do
		if not pt.under then return end
		if vector.equals(pt.under, rp) then return true end
		local node = core.get_node(pt.under)
		if not seethru[node.name] then return end
	end
	return true
end

local function witnesslater(player, pos, disc)
	disc = nc.flatkeys(disc)
	local data, save = witnessdata(player)
	local newdata = {
		node = core.get_node(pos).name,
		stack = nc.stack_get(pos):get_name(),
		disc = disc
	}
	local posstr = core.pos_to_string(pos)
	local olddata = data.lookup[posstr]
	if olddata and (olddata.node == newdata.node) and (olddata.stack
		== newdata.stack) and (type(olddata.disc) == "table") then
		for k in pairs(disc) do
			olddata.disc[k] = true
		end
	else
		data.queue[#data.queue + 1] = pos
		while #data.queue > 100 do table_remove(data.queue, 1) end
		data.lookup[posstr] = newdata
	end
	save()
end

local function witness_core(pos, disc, maxdist)
	maxdist = maxdist or 16
	for _, player in pairs(core.get_connected_players()) do
		local ppos = player:get_pos()
		if vector.distance(ppos, pos) <= maxdist then
			local deferred = nc.player_discover_deferred(
				player, disc, "witness:")
			if deferred then
				if canwitnessnow(player, pos) then
					deferred()
				else
					witnesslater(player, pos, disc)
				end
			end
		end
	end
end
function nc.witness(pos, disc, maxdist)
	if (not pos.x) and pos[1] and pos[1].x then
		for i = 1, #pos do
			witness_core(pos[i], disc, maxdist)
		end
	else
		return witness_core(pos, disc, maxdist)
	end
end

local function delayed_witness(pos, node, player)
	local data, save = witnessdata(player)
	local posstr = core.pos_to_string(pos)
	local found = data.lookup[posstr]
	if not found then return end
	data.lookup[posstr] = nil
	save()
	node = node or core.get_node(pos)
	if (found.node ~= node.name) or (nc.stack_get(pos):get_name()
		~= found.stack) then return end
	return nc.player_discover(player, found.disc, "witness:")
end
core.register_on_punchnode(delayed_witness)
nc.register_on_node_stare(function(player, pt)
		delayed_witness(pt.under, core.get_node(pt.under), player)
	end)
