-- Max. number of steps in the laser travel algorithm
local MAX_LASER_ITERATIONS = 10000

-- TODO: Don't use these variables, instead transport it between functions
local burning_cache = {}
local destroy_cache = {}

-- Add a laser node to pos with the direction `dir`.
-- Dir is a direction vector, and only one direction must be set
function lzr_laser.add_laser(pos, dir, varea, vdata)
	-- Check if laser is still within the playfield
	if pos.x < lzr_globals.PLAYFIELD_START.x or pos.x > lzr_globals.PLAYFIELD_END.x or
			pos.y < lzr_globals.PLAYFIELD_START.y or pos.y > lzr_globals.PLAYFIELD_END.y or
			pos.z < lzr_globals.PLAYFIELD_START.z or pos.z > lzr_globals.PLAYFIELD_END.z then
		return false
	end
	local vi = varea:indexp(pos)
	local content_id = vdata[vi]
	local nodename = minetest.get_name_from_content_id(content_id)
	local ld = minetest.get_item_group(nodename, "laser_destroys")
	-- Laser through air or destroyable block
	if content_id == minetest.CONTENT_AIR or ld == 1 then
		local dirs = lzr_laser.vector_to_dirs(dir)
		local dirstring = lzr_laser.dirs_to_dirstring(dirs)
		vdata[vi] = minetest.get_content_id("lzr_laser:laser_"..dirstring)
		if ld == 1 then
			table.insert(destroy_cache, {pos=pos, nodename=nodename})
		end
		pos = vector.add(pos, dir)
		return {{pos, dir}}
	elseif ld == 2 then
		local def = minetest.registered_nodes[nodename]
		local active = def._lzr_active
		if active then
			-- Only burn in-game (for editor convenience)
			if lzr_gamestate.get_state() == lzr_gamestate.LEVEL then
				vdata[vi] = minetest.get_content_id(active)
				table.insert(burning_cache, pos)
			end
		else
			minetest.log("error", "[lzr_laser] Node definition of "..nodename.." has laser_destroys=2 but no _lzr_active")
		end
		return false
	-- Laser through laser (laser intersection)
	elseif minetest.get_item_group(nodename, "laser") > 0 then
		local dirnum = minetest.get_item_group(nodename, "laser")
		local dirstring_old = lzr_laser.dec2bin(dirnum, 3)

		local dirs_new = lzr_laser.vector_to_dirs(dir)
		local dirstring_new = lzr_laser.dirs_to_dirstring(dirs_new)

		local place_dirstring = lzr_laser.bitwise_or(dirstring_old, dirstring_new)
		vdata[vi] = minetest.get_content_id("lzr_laser:laser_"..place_dirstring)
		pos = vector.add(pos, dir)
		return {{pos, dir}}
	-- Laser through skull
	elseif minetest.get_item_group(nodename, "skull_shy") > 0 or minetest.get_item_group(nodename, "skull_cursed") > 0 then
		local def = minetest.registered_nodes[nodename]
		local active = def._lzr_active
		if active then
			-- Activate skull node
			vdata[vi] = minetest.get_content_id(def._lzr_active)
		end
		pos = vector.add(pos, dir)
		return {{pos, dir}}
	-- Mirror laser
	elseif minetest.get_item_group(nodename, "mirror") > 0 then
		local mirror_dir = lzr_laser.get_mirrored_laser_dir(pos, dir)
		if mirror_dir then
			local def = minetest.registered_nodes[nodename]
			local active = def._lzr_active
			if not active then
				return false
			end
			-- Activate mirror node
			vdata[vi] = minetest.get_content_id(def._lzr_active)
			-- Set new pos and dir
			pos = vector.add(pos, mirror_dir)
			dir = mirror_dir
			return {{pos, dir}}
		else
			return false
		end
	-- Mirror and split laser
	elseif minetest.get_item_group(nodename, "transmissive_mirror") > 0 then
		local mirror_dir, mirror_side = lzr_laser.get_mirrored_laser_dir(pos, dir)
		if mirror_dir then
			local def = minetest.registered_nodes[nodename]
			local state = def._lzr_transmissive_mirror_state
			if not state then
				minetest.log("error", "[lzr_laser] Transmissive mirror node '"..nodename.."' does not have _lzr_transmissive_mirror_state!")
				return false
			end
			if mirror_side == true then
				state = lzr_laser.bitwise_or(state, "01")
			else
				state = lzr_laser.bitwise_or(state, "10")
			end
			local active = def["_lzr_transmissive_mirror_"..state]
			if not active then
				minetest.log("error", "[lzr_laser] Missing transmissive mirror node state for '"..nodename.."!")
				return false
			end
			local is_takable = minetest.get_item_group(nodename, "takable") ~= 0
			if is_takable then
				local def_active = minetest.registered_nodes[active]
				active = def_active._lzr_takable
			end
			-- Set new transmissive node state
			vdata[vi] = minetest.get_content_id(active)
			-- Set new pos and dir
			local pos_straight = vector.add(pos, dir)
			local dir_straight = dir
			local pos_mirrored = vector.add(pos, mirror_dir)
			local dir_mirrored = mirror_dir
			return {{pos_straight, dir_straight}, {pos_mirrored, dir_mirrored}}
		else
			return false
		end
	-- Spread laser to all directions
	elseif minetest.get_item_group(nodename, "crystal") > 0 then
		local def = minetest.registered_nodes[nodename]
		local active = def._lzr_active
		if not active then
			return false
		end
		-- Activate node
		vdata[vi] = minetest.get_content_id(active)
		-- Set dirs
		local dirs = {
			vector.new(0, -1, 0),
			vector.new(0, 1, 0),
			vector.new(-1, 0, 0),
			vector.new(1, 0, 0),
			vector.new(0, 0, -1),
			vector.new(0, 0, 1),
		}
		for d=1, #dirs do
			if vector.equals(dirs[d], dirs) then
				table.remove(dirs, d)
				break
			end
		end
		local output = {}
		for d=1, #dirs do
			table.insert(output, { vector.add(pos, dirs[d]), dirs[d] })
		end
		return output
	-- Detector
	elseif minetest.get_item_group(nodename, "detector") > 0 then
		local detected = lzr_laser.check_detector(pos, dir)
		if detected then
			local def = minetest.registered_nodes[nodename]
			local active = def._lzr_active
			if active then
				-- Activate node
				vdata[vi] = minetest.get_content_id(active)
			end
		end
		-- Laser ends here
		return false
	-- Anything else: fail
	else
		return false
	end
end

function lzr_laser.emit_laser(pos, varea, vdata, vdata_p2)
	local vi = varea:indexp(pos)
	local content_id = vdata[vi]
	local nodename = minetest.get_name_from_content_id(content_id)
	if minetest.get_item_group(nodename, "emitter") == 0 then
		minetest.log("error", "[lzr_laser] lzr_laser.emit_laser was called at invalid pos!")
		return false
	end
	local param2 = vdata_p2[vi]

	local dir = minetest.facedir_to_dir(param2)
	dir = vector.multiply(dir, -1)
	local i_pos = vector.add(pos, dir)
	lzr_laser.travel_laser(i_pos, dir, varea, vdata)

end

function lzr_laser.travel_laser(pos, dir, varea, vdata)
	local i_pos = table.copy(pos)
	local cond = true
	local next_lasers = {{i_pos, dir}}
	local i = 0
	while cond do
		i = i + 1
		-- Halt execution for very long loops to prevent freezing the game
		if i > MAX_LASER_ITERATIONS then
			minetest.log("error", "[lzr_laser] lzr_laser.travel_laser aborted (too many iterations!)")
			break
		end
		local next_laser = next_lasers[1]
		local add_laser_result = lzr_laser.add_laser(next_laser[1], next_laser[2], varea, vdata)
		table.remove(next_lasers, 1)
		if add_laser_result ~= false then
			for a=1, #add_laser_result do
				table.insert(next_lasers, add_laser_result[a])
			end
		end
		if #next_lasers == 0 then
			break
		end
	end
end

-- Remove all lasers in area and disable all laser blocks
function lzr_laser.clear_lasers_in_area(pos1, pos2, ignore_emitters, varea, vdata)
	for vi=1, #vdata do
		local cid = vdata[vi]
		local nodename = minetest.get_name_from_content_id(cid)
		if minetest.get_item_group(nodename, "laser") ~= 0 then
			vdata[vi] = minetest.CONTENT_AIR
		elseif minetest.get_item_group(nodename, "laser_block") ~= 0 then
			local def = minetest.registered_nodes[nodename]
			local is_ignored_emitter = false
			if ignore_emitters ~= true then
				is_ignored_emitter = minetest.get_item_group(nodename, "emitter") > 0
			end
			if def and not is_ignored_emitter then
				local inactive = def._lzr_inactive
				if inactive then
					vdata[vi] = minetest.get_content_id(inactive)
				end
			end
		end
	end
end

-- Emit lasers from all active emitters
function lzr_laser.emit_lasers_in_area(pos1, pos2, varea, vdata, vdata_p2)
	local emitters = minetest.find_nodes_in_area(pos1, pos2, {"group:emitter"})
	for e=1, #emitters do
		local epos = emitters[e]
		local vi = varea:indexp(epos)
		local emitter_cid = vdata[vi]
		local emittername = minetest.get_name_from_content_id(emitter_cid)
		local is_active = minetest.get_item_group(emittername, "emitter") == 2
		if is_active then
			lzr_laser.emit_laser(emitters[e], varea, vdata, vdata_p2)
		end
	end
end

-- Return true if all detectors in area are on (including if no detector)
function lzr_laser.check_detectors_in_area(pos1, pos2)
	local detectors = minetest.find_nodes_in_area(pos1, pos2, {"group:detector"})
	for d=1, #detectors do
		local dpos = detectors[d]
		local detector = minetest.get_node(dpos)
		local is_active = minetest.get_item_group(detector.name, "detector") == 2
		if not is_active then
			return false
		end
	end
	return true
end

-- Returns true if there are no unclaimed treasures remaining
function lzr_laser.check_treasures_in_area(pos1, pos2)
	local closed_chests = minetest.find_nodes_in_area(pos1, pos2, {"group:chest_closed"})
	return #closed_chests > 0
end

-- Returns the number of treasures found in current level
function lzr_laser.count_found_treasures(pos1, pos2)
	return #minetest.find_nodes_in_area(pos1, pos2, {"group:chest_open_treasure"})
end

-- Returns true if player has no detectors in inventory
function lzr_laser.check_inventory_detectors(player, detector_placed)
	local inv = player:get_inventory()
	local count = 0
	for i=1, inv:get_size("main") do
		local item = inv:get_stack("main", i)
		if minetest.get_item_group(item:get_name(), "detector") ~= 0 then
			count = count + item:get_count()
			if (detector_placed and count > 1) or (not detector_placed) then
				return false
			end
		end
	end
	return true
end

-- Returns true if ALL detectors (both in level and in inventory) are active
function lzr_laser.check_all_detectors(detector_placed)
	local cond_area = lzr_laser.check_detectors_in_area(lzr_globals.PLAYFIELD_START, lzr_globals.PLAYFIELD_END)
	local cond_inventory = true
	local player = minetest.get_player_by_name("singleplayer")
	if player then
		cond_inventory = lzr_laser.check_inventory_detectors(player, detector_placed)
	end
	return cond_area and cond_inventory
end

-- Returns true if current level is won
function lzr_laser.check_level_won()
	return not lzr_laser.check_treasures_in_area(lzr_globals.PLAYFIELD_START, lzr_globals.PLAYFIELD_END)
end

-- Completely recalculate all lasers
function lzr_laser.full_laser_update(pos1, pos2)
	local benchmark_time_1 = minetest.get_us_time()

	burning_cache = {}
	destroy_cache = {}

	local vmanip = minetest.get_voxel_manip(pos1, pos2)
	local vpos1, vpos2 = vmanip:get_emerged_area()
	local varea = VoxelArea:new({MinEdge = vpos1, MaxEdge = vpos2})
	local vdata = vmanip:get_data()
	local vdata_p2 = vmanip:get_param2_data()

	lzr_laser.clear_lasers_in_area(pos1, pos2, nil, varea, vdata)
	lzr_laser.emit_lasers_in_area(pos1, pos2, varea, vdata, vdata_p2)

	vmanip:set_data(vdata)
	vmanip:set_param2_data(vdata_p2)
	vmanip:write_to_map()

	-- Trigger node burning for nodes burned by laser
	for b=1, #burning_cache do
		local node = minetest.get_node(burning_cache[b])
		local def = minetest.registered_nodes[node.name]
		if def and def.on_construct then
			def.on_construct(burning_cache[b])
		end
	end
	burning_cache = {}
	-- Trigger animation and sound effect for nodes destroyed by laser
	for d=1, #destroy_cache do
		local dpos = destroy_cache[d].pos
		local nodename = destroy_cache[d].nodename
		local def = minetest.registered_nodes[nodename]
		if def and def.sounds and def.sounds.dug then
			minetest.sound_play(def.sounds.dug, {pos=dpos}, true)
		end
		minetest.add_particlespawner({
			amount = 12,
			time = 0.001,
			minpos = vector.subtract(dpos, vector.new(0.5, 0.5, 0.5)),
			maxpos = vector.add(dpos, vector.new(0.5, 0.5, 0.5)),
			minvel = vector.new(-0.5, -0.2, -0.5),
			maxvel = vector.new(0.5, 0.2, 0.5),
			minacc = vector.new(0, -lzr_globals.GRAVITY, 0),
			maxacc = vector.new(0, -lzr_globals.GRAVITY, 0),
			minsize = 0.8,
			maxsize = 0.8,
			minexptime = 0.5,
			maxexptime = 0.55,
			node = {name=nodename},
		})
	end
	destroy_cache = {}

	-- Print benchmark time
	local benchmark_time_2 = minetest.get_us_time()
	local diff = benchmark_time_2 - benchmark_time_1
	minetest.log("info", "[lzr_laser] lzr_laser.full_laser_update took "..diff.." µs.")
end
