local S, PS = minetest.get_translator("lzr_parrot_npc")
local FS = function(...) return minetest.formspec_escape(S(...)) end
local NS = function(s) return s end

local F = minetest.formspec_escape

local mod_storage = minetest.get_mod_storage()

lzr_parrot_npc = {}

--~ Parrot name. The player’s main companion and hint-giver. Named after gold
local GOLDIE_NAME = NS("Goldie the Parrot")
--~ Parrot name. The player’s main companion and hint-giver. Named after gold
local GOLDIE_NAME_SHORT = NS("Goldie")

local HIDDEN_PARROT_NOT_FOUND = 0
local HIDDEN_PARROT_FOUND = 1

local HIDDEN_PARROT_FLY_HEIGHT = 20
local HIDDEN_PARROT_FLY_SPEED = 3
local HIDDEN_PARROT_FLY_DESPAWN_DISTANCE = 200
local HIDDEN_PARROT_FLY_DELAY = 2.0

-- Mininum and maximum delay in second between idle parrot animations
local PARROT_ANIMATION_DELAY_MIN = 2.0
local PARROT_ANIMATION_DELAY_MAX = 8.0

-- Check if parrot is stuck in solid block every this many seconds
local PARROT_STUCK_CHECK_INTERVAL = 1.0
-- Vertical fly speed of parrot to try to get unstuck
local PARROT_UNSTUCK_FLY_SPEED = 2.0
-- Vertical fly speed of parrot if falling
local PARROT_FALL_FLY_SPEED = -1.0

-- Pitch of parrot call sound when scorched
local PARROT_SCORCHED_CALL_PITCH = 0.8

-- Radius around parrot to look for players to
-- display the "talk" HUD indivator
-- (only if _talk_mark in parrot entity is true)
local PARROT_TALK_MARKER_RADIUS = 6

local speaker_portraits = {
	goldie = "lzr_parrot_npc_goldie_portrait.png",
	ruby = "lzr_parrot_npc_ruby_portrait.png",
	emmy = "lzr_parrot_npc_emmy_portrait.png",
	saphie = "lzr_parrot_npc_saphie_portrait.png",
	garnie = "lzr_parrot_npc_garnie_portrait.png",
	tuckie = "lzr_parrot_npc_tuckie_portrait.png",
	dimey = "lzr_parrot_npc_dimey_portrait.png",
}

-- Holds name and object of hidden parrot speaker who last spawned a speak dialog
local last_hidden_speaker = nil
local last_hidden_speaker_object = nil

local PARROT_ANIMS = {
	idle = { frame_range = { x=0, y=0 }, frame_speed = 2 },
	flap_small = { frame_range = { x=0, y=2 }, frame_speed = 6 },
	flap_big = { frame_range = { x=2, y=4 }, frame_speed = 20 },
	head_bounce = { frame_range = { x=4, y=6 }, frame_speed = 10 },
	head_roll_right = { frame_range = { x=6, y=8 }, frame_speed = 2 },
	head_roll_left = { frame_range = { x=8, y=10 }, frame_speed = 2 },
	head_roll_leftright = { frame_range = { x=6, y=10 }, frame_speed = 2 },
}

local function make_call(to_player, pitch)
	minetest.sound_play({name="lzr_parrot_npc_curr"}, {to_player=to_player:get_player_name(), pitch=pitch})
end

-- List of hidden parrots. These parrots, together with Goldie,
-- replicate the 7 laser colors.
local hidden_parrots = {
	"ruby",
	"emmy",
	"saphie",
	"garnie",
	"tuckie",
	"dimey",
}
local hidden_parrot_names = {
	--~ Parrot name; named after ruby, a red gemstone
	ruby = NS("Ruby the Hidden Parrot"),
	--~ Parrot name; named after emerald, a green gemstone
	emmy = NS("Emmy the Hidden Parrot"),
	--~ Parrot name; named after saphire, a blue gemstone
	saphie = NS("Saphie the Hidden Parrot"),
	--~ Parrot name; named after garnet, a magenta gemstone
	garnie = NS("Garnie the Hidden Parrot"),
	--~ Parrot name; named after turquoise, a gemstone
	tuckie = NS("Tuckie the Hidden Parrot"),
	--~ Parrot name; named after diamond
	dimey = NS("Dimey the Hidden Parrot"),
}
-- same as hidden_parrot_names, but after the parrot was found ;-)
local found_parrot_names = {
	--~ Parrot name; named after ruby, a red gemstone
	ruby = NS("Ruby the Found Parrot"),
	--~ Parrot name; named after emerald, a green gemstone
	emmy = NS("Emmy the Found Parrot"),
	--~ Parrot name; named after saphire, a blue gemstone
	saphie = NS("Saphie the Found Parrot"),
	--~ Parrot name; named after garnet, a magenta gemstone
	garnie = NS("Garnie the Found Parrot"),
	--~ Parrot name; named after turquoise, a gemstone
	tuckie = NS("Tuckie the Found Parrot"),
	--~ Parrot name; named after diamond
	dimey = NS("Dimey the Found Parrot"),
}
local hidden_parrot_names_short = {
	--~ Parrot name; named after ruby, a red gemstone
	ruby = NS("Ruby"),
	--~ Parrot name; named after emerald, a green gemstone
	emmy = NS("Emmy"),
	--~ Parrot name; named after saphire, a blue gemstone
	saphie = NS("Saphie"),
	--~ Parrot name; named after garnet, a magenta gemstone
	garnie = NS("Garnie"),
	--~ Parrot name; named after turquoise, a gemstone
	tuckie = NS("Tuckie"),
	--~ Parrot name; named after diamond
	dimey = NS("Dimey"),
}

lzr_parrot_npc.get_hidden_parrot_num_by_param2 = function(param2)
	return math.floor(param2 / 4) + 1
end

lzr_parrot_npc.get_hidden_parrot_name = function(num)
	return hidden_parrots[num]
end

lzr_parrot_npc.get_hidden_parrot_names = function()
	return table.copy(hidden_parrots)
end

lzr_parrot_npc.get_hidden_parrot_num_by_name = function(parrot_name)
	for i=1, #hidden_parrots do
		if hidden_parrots[i] == parrot_name then
			return i
		end
	end
end

-- Returns a list of all possible param2 values the hidden parrot spawner
-- for the parrot of the given name might have
local get_hidden_parrot_param2_values = function(parrot_name)
	local num = lzr_parrot_npc.get_hidden_parrot_num_by_name(parrot_name)
	local p2 = (num-1) * 4
	return { p2, p2+1, p2+2, p2+3 }
end

lzr_parrot_npc.was_hidden_parrot_found = function(parrot_name)
	local found = mod_storage:get_int("lzr_parrot_npc:hidden_parrot_"..parrot_name)
	if found == HIDDEN_PARROT_FOUND then
		return true
	else
		return false
	end
end

lzr_parrot_npc.were_all_hidden_parrots_found = function()
	for p=1, #hidden_parrots do
		if not lzr_parrot_npc.was_hidden_parrot_found(hidden_parrots[p]) then
			return false
		end
	end
	return true
end

lzr_parrot_npc.count_found_hidden_parrots = function()
	local found = 0
	for p=1, #hidden_parrots do
		if lzr_parrot_npc.was_hidden_parrot_found(hidden_parrots[p]) then
			found = found + 1
		end
	end
	return found
end

-- Make parrot face player
local face_player = function(parrot, player)
	local papos = parrot.object:get_pos()
	local plpos = player:get_pos()
	papos.y = 0
	plpos.y = 0
	local dir = vector.direction(papos, plpos)
	local yaw = minetest.dir_to_yaw(dir)
	parrot.object:set_yaw(yaw)
end

local texture_repeat = function(texture)
	local textures = {}
	for t=1, 10 do
		table.insert(textures, texture)
	end
	return textures
end

-- Make the given parrot object react to player.
-- Shows associated text of current level (if present),
-- show
local react = function(parrot, player)
	if lzr_gamestate.is_loading() then
		return
	end
	if player and player:is_player() then
		local state = lzr_gamestate.get_state()
		local spoken = false
		if parrot._scorched then
			--~ Parrot speech indicating a parrot is coughing after an explosion
			lzr_speech.speak(player, S("*cough* *cough*"), "goldie", "lzr_parrot_npc_scorched_portrait.png", PARROT_SCORCHED_CALL_PITCH)
			spoken = true
		elseif state == lzr_gamestate.LEVEL then
			local speeches = lzr_levels.get_npc_texts()
			if speeches and speeches.goldie and speeches.goldie ~= "" then
				local flags = lzr_levels.get_flags()
				if flags.parrot_hint then
					lzr_speech.ask(player, S("Do you want me to give you a hint?"), speeches.goldie, "goldie")
				else
					lzr_speech.speak(player, speeches.goldie, "goldie")
				end
				spoken = true
			end
		elseif state == lzr_gamestate.MENU then
			-- Goldie the parrot comments on the game progress when on the pirate ship
			local core_level_data = lzr_levels.get_level_pack("__core")
			local treasures = lzr_levels.count_total_collected_treasures(core_level_data)
			local total_treasures = lzr_levels.count_total_treasures(core_level_data)
			local text
			-- First, always show the number of gold blocks on the pirate ship.
			-- (Only gold blocks from the core level pack count.)
			if treasures == 0 then
				text = S("I’m sad because we don’t have any treasures.")
			else
				text = PS("We have @1 gold block in our possession.", "We have @1 gold blocks in our possession.", treasures, treasures)
				local ratio = treasures / total_treasures
				local comment

				-- Short opinion on the number of gold blocks
				if treasures == total_treasures then
					--~ Goldie the Parrot says this about the collectable gold blocks
					comment = S("Yarr! We’ve plundered everything!")
				elseif ratio < 0.25 then
					--~ Goldie the Parrot says we need more gold blocks
					comment = S("We need more, much more!")
				elseif ratio < 0.5 then
					--~ Goldie the Parrot says we need more gold blocks
					comment = S("We need more.")
				elseif ratio < 0.75 then
					--~ Goldie the Parrot says this
					comment = S("You can never have too much gold.")
				elseif ratio < 1 then
					--~ Goldie the Parrot talks about the gold blocks on the pirate ship
					comment = S("It’s a miracle this ship can carry it.")
				end
				if comment then
					text = text .. "\n" .. comment
				end

				-- Goldie hints at the existence of hidden parrots if none of them
				-- have been found yet and the player has collected enough
				-- gold blocks.
				if not lzr_parrot_npc.were_all_hidden_parrots_found() then
					local found = lzr_parrot_npc.count_found_hidden_parrots()
					if found == 0 and ratio > 0.333 then
						--~ Goldie the Parrot says this
						text = text .. "\n\n" .. S("By the way, I haven’t seen other parrots for a long time. I wonder if they’re hiding somewhere.")
					end
				end
			end
			lzr_speech.speak(player, text, "goldie")
			spoken = true
		end
		-- Remove "Talk to me" nametag after talking
		if spoken and parrot._talk_mark then
			parrot.object:set_nametag_attributes({text=""})
			parrot._talk_mark = nil
		end
		if not spoken then
			make_call(player)
		end

		face_player(parrot, player)

		-- Bounce head
		if parrot._animation_timer > 0.5 and not parrot._falling and not parrot._unstucking then
			local anim = PARROT_ANIMS.head_bounce
			parrot.object:set_animation(anim.frame_range, anim.frame_speed, 0, false)
			parrot._animation_timer = 0
			parrot._next_animation_at = math.random(PARROT_ANIMATION_DELAY_MIN, PARROT_ANIMATION_DELAY_MAX)
		end
	end
end

local pos_needs_unstuck = function(pos)
	local node = minetest.get_node(pos)
	if node.name == "ignore" then
		return false
	end
	local def = minetest.registered_nodes[node.name]
	if def and def.walkable and (minetest.get_item_group(node.name, "takable") ~= 0 or minetest.get_item_group(node.name, "skull") ~= 0) then
		local apos = vector.offset(pos, 0, 1, 0)
		local anode = minetest.get_node(apos)
		local adef = minetest.registered_nodes[anode.name]
		if adef and adef.walkable then
			if minetest.get_item_group(anode.name, "takable") ~= 0 or minetest.get_item_group(anode.name, "skull") ~= 0 then
				return true
			else
				return false
			end
		else
			return true
		end
	else
		return false
	end
end
local is_on_solid = function(pos)
	local bpos = vector.offset(pos, 0, -0.05, 0)
	local node = minetest.get_node(bpos)
	local def = minetest.registered_nodes[node.name]
	if not def or def.walkable then
		return true
	else
		return false
	end
end

local parrot_check_stuck = function(self)
	if not self._unstucking and not self._falling then
		local pos = self.object:get_pos()
		if pos_needs_unstuck(pos) then
			self._unstucking = true
			self._falling = false
			self.object:set_velocity({x=0, y=PARROT_UNSTUCK_FLY_SPEED, z=0})
			local anim = PARROT_ANIMS.flap_big
			self.object:set_animation(anim.frame_range, anim.frame_speed, 0, true)
		elseif not is_on_solid(pos) then
			self._falling = true
			self._unstucking = false
			self.object:set_velocity({x=0, y=PARROT_FALL_FLY_SPEED, z=0})
			local anim = PARROT_ANIMS.flap_big
			self.object:set_animation(anim.frame_range, anim.frame_speed, 0, true)
		end
		self._stuck_check_timer = 0
	end
end

local parrot_activate = function(self)
	self.object:set_armor_groups({immortal=1})
	self._next_animation_at = math.random(PARROT_ANIMATION_DELAY_MIN, PARROT_ANIMATION_DELAY_MAX)
end
local parrot_activate_goldie = function(self)
	parrot_activate(self)
	self.object:set_properties({
		infotext = S(GOLDIE_NAME_SHORT)
	})
end

local parrot_step_idle = function(self, dtime)
	if self._scorched then
		self._scorched_timer = self._scorched_timer + dtime
		if self._scorched_timer >= self._unscorched_time then
			self.object:set_properties({
				textures = self._base_textures,
			})
			self._scorched = false
		end
	end
	self._stuck_check_timer = self._stuck_check_timer + dtime
	if self._stuck_check_timer >= PARROT_STUCK_CHECK_INTERVAL then
		parrot_check_stuck(self)
	end
	if self._talk_mark then
		local pos = self.object:get_pos()
		local objs = minetest.get_objects_inside_radius(pos, PARROT_TALK_MARKER_RADIUS)
		local player_found = false
		for o=1, #objs do
			if objs[o]:is_player() then
				player_found = true
				break
			end
		end
		if player_found then
			--~ A parrot asks the player to talk to it
			self.object:set_nametag_attributes({
				text = S("Talk to me!"),
				color = "#FFFF00FF",
				bgcolor = "#00000080",
			})
		else
			self.object:set_nametag_attributes({text = ""})
		end
	end
	if self._unstucking then
		local pos = self.object:get_pos()
		if not pos_needs_unstuck(pos) then
			self.object:set_velocity({x=0, y=0, z=0})
			self.object:set_animation(PARROT_ANIMS.idle.frame_range, PARROT_ANIMS.idle.frame_speed, 0, false)
			self._falling = false
			self._unstucking = false
		end
		return
	elseif self._falling then
		local pos = self.object:get_pos()
		if is_on_solid(pos) then
			self.object:set_velocity({x=0, y=0, z=0})
			self.object:set_animation(PARROT_ANIMS.idle.frame_range, PARROT_ANIMS.idle.frame_speed, 0, false)
			self._falling = false
			self._unstucking = false
		end
		return
	end

	-- Play random animation in random intervals
	self._animation_timer = self._animation_timer + dtime
	if self._animation_timer >= self._next_animation_at then
		local idle_anims = {
			"flap_small",
			"flap_small",
			"flap_small",
			"head_bounce",
			"head_roll_right",
			"head_roll_right",
			"head_roll_right",
			"head_roll_left",
			"head_roll_left",
			"head_roll_left",
		}
		local a = math.random(1, #idle_anims)
		local anim = PARROT_ANIMS[idle_anims[a]]
		self.object:set_animation(anim.frame_range, anim.frame_speed, 0, false)

		self._animation_timer = 0
		self._next_animation_at = math.random(PARROT_ANIMATION_DELAY_MIN, PARROT_ANIMATION_DELAY_MAX)
	end
end

local parrot_scorch = function(self, time)
	self.object:set_properties({
		textures=texture_repeat("lzr_parrot_npc_scorched.png"),
	})
	self._scorched = true
	self._scorched_timer = 0
	self._unscorched_time = time
end

-- Parrot selectionbox.
-- Careful with adding rotate=true! That can mess up the
-- hook overlay (rotation arrows) when pointing at a parrot
-- in front of a rotatable block. Probably Luanti's raycasting
-- is not that reliable for objects with rotate=true in it
-- (as of 5.13.0). If you add rotate=true, make sure to carefully
-- test if the hook rotation arrows still interact well with
-- objects. (see https://codeberg.org/Wuzzy/Lazarr/issues/211)
local SELECTIONBOX = { -0.17, 0, -0.17, 0.17, 0.65, 0.17 }

minetest.register_entity("lzr_parrot_npc:parrot", {
	initial_properties = {
		visual = "mesh",
		mesh = "lzr_parrot_npc_parrot.gltf",
		visual_size = { x=1, y=1, z=1 },
		textures = texture_repeat("lzr_parrot_npc_goldie.png"),
		backface_culling = false,
		static_save = false,
		physical = false,
		collide_with_objects = false,
		selectionbox = SELECTIONBOX,
	},
	_animation_timer = 0,
	_next_animation_at = nil,
	_stuck_check_timer = 0,
	_unstucking = false,
	_scorched = false,
	_scorched_timer = nil,
	_unchorched_time = nil,
	_scorch = parrot_scorch,
	_base_textures = texture_repeat("lzr_parrot_npc_goldie.png"),

	on_activate = parrot_activate_goldie,
	on_step = parrot_step_idle,
	on_rightclick = function(self, clicker)
		react(self, clicker)
	end,
	on_punch = function(self, puncher)
		react(self, puncher)
	end,
})

local mark_hidden_parrot_as_found = function(player, parrot_name)
	local found = mod_storage:get_int("lzr_parrot_npc:hidden_parrot_"..parrot_name)
	if found == HIDDEN_PARROT_FOUND then
		if lzr_parrot_npc.were_all_hidden_parrots_found() then
			lzr_menu.place_painting("parrot_finder")
		end
		return false
	else
		mod_storage:set_int("lzr_parrot_npc:hidden_parrot_"..parrot_name, HIDDEN_PARROT_FOUND)
		lzr_menu.spawn_hidden_parrot(get_hidden_parrot_param2_values(parrot_name), parrot_name)
		minetest.log("action", "[lzr_parrot_npc] Player found hidden parrot '"..parrot_name.."'")
		if lzr_parrot_npc.were_all_hidden_parrots_found() then
			lzr_menu.place_painting("parrot_finder")
		end
		return true
	end
end

local react_hidden = function(parrot, player)
	if lzr_gamestate.is_loading() then
		return
	end
	if player and player:is_player() then
		local hidden_parrot_identity = parrot._hidden_id
		local call_pitch
		if hidden_parrot_identity and mark_hidden_parrot_as_found(player, hidden_parrot_identity) then
			local text
			local found = lzr_parrot_npc.count_found_hidden_parrots()

			-- First found parrot
			if found == 1 then
				local friends = {}
				for h=1, #hidden_parrots do
					if hidden_parrots[h] ~= hidden_parrot_identity then
						table.insert(friends, S(hidden_parrot_names_short[hidden_parrots[h]]))
					end
				end
				local f1, f2, f3, f4, f5 = friends[1], friends[2], friends[3], friends[4], friends[5]
				text = S("Hey, you found my secret hideout! Now it’s useless, such a shame …").."\n"..
				S("I’ve got an idea. Let me come with you, okay?").."\n\n"..
				S("I have five friends scattered around the world, I want to meet them again. Their names are @1, @2, @3, @4 and @5.", f1, f2, f3, f4, f5)
			-- Last found parrot
			elseif found == #hidden_parrots then
				text = S("Incredible! It’s you! I’ve heard a lot from your journeys. Now the whole gang is re-united!").."\n"
				if hidden_parrot_identity == "tuckie" then
					text = text .. S("I’ve got a beautiful portrait of myself. You can have it. How nice is that?")
				else
					text = text .. S("I’ve found a beautiful portrait of our friend Tuckie. You can have it. How nice is that?")
				end
			-- Found parrot, neither first or last
			else
				local remaining = #hidden_parrots - found
				text = S("You found my secret hiding spot!").."\n"..
				PS("Only @1 parrot is still in hiding.", "There are still @1 hidden parrots left to go.", remaining, remaining)

			end

			last_hidden_speaker = hidden_parrot_identity
			last_hidden_speaker_object = parrot

			if parrot._scorched then
				lzr_speech.speak(player, text, hidden_parrot_identity, "lzr_parrot_npc_scorched_portrait.png", PARROT_SCORCHED_CALL_PITCH)
			else
				lzr_speech.speak(player, text, hidden_parrot_identity)
			end
			face_player(parrot, player)
			return
		elseif lzr_gamestate.get_state() == lzr_gamestate.MENU then
			local found = lzr_parrot_npc.count_found_hidden_parrots()
			-- Speech of (ex-)hidden parrot at the ship
			local text
			local remaining = #hidden_parrots - found

			-- Put the hidden parrots in a list
			local found_parrot_names = {}
			for p=1, #hidden_parrots do
				if lzr_parrot_npc.was_hidden_parrot_found(hidden_parrots[p]) then
					local pname = lzr_parrot_npc.get_hidden_parrot_name(p)
					table.insert(found_parrot_names, pname)
				end
			end

			-- The parrot speech depends on how many parrots have been found, so
			-- that each of the found parrots says a different line.
			-- Each new found parrot "unlocks" a new text.
			-- The text may change when all hidden parrots were found.
			-- This is mostly for flavor, but the first parrot can give
			-- a vague hint.

			if hidden_parrot_identity == found_parrot_names[1] then
				-- Tells player the hiding spots of found parrots
				if found == 1 then
					--~ Said by a parrot
					text = S("Can you look for my friends, please?").."\n\n"..
						--~ Said by a parrot
						S("My secret hiding spot was here:")
				else
					--~ Said by a parrot
					text = S("In case you forgot, these were our secret hiding spots:")
				end
				local core_level_data = lzr_levels.get_level_pack("__core")
				local parrot_list = {}
				local parrot_levels = lzr_levels.get_hidden_parrot_levels(core_level_data)

				-- Sort list by param2 value of the spawners because the original list
				-- is in level order (which we don't want).
				table.sort(parrot_levels, function(e1, e2)
					return e1.param2 < e2.param2
				end)

				-- Create list of found parrots and the levels they hid in
				for p=1, #parrot_levels do
					local num = lzr_parrot_npc.get_hidden_parrot_num_by_param2(parrot_levels[p].param2)
					local pname = hidden_parrots[num]
					if lzr_parrot_npc.was_hidden_parrot_found(pname) then
						local level = parrot_levels[p].level
						local level_name = lzr_levels.get_level_name(level, core_level_data, true)
						local parrot_name = hidden_parrot_names_short[pname]
						if found == 1 then
							table.insert(parrot_list, level_name)
						else
							--~ List entry. @1 = parrot name, @2 level name of the parrot’s secret hiding spot
							table.insert(parrot_list, S("• @1: @2", S(parrot_name), level_name))
						end
					end
				end
				text = text .. "\n" .. table.concat(parrot_list, "\n")

				-- Give a hint about the hiding spots of parrots if 50% of all treasures were found
				-- and the player hasn't found all hidden parrots yet.
				if found < #hidden_parrots then
					local treasures = lzr_levels.count_total_collected_treasures(core_level_data)
					local total_treasures = lzr_levels.count_total_treasures(core_level_data)
					local ratio = treasures / total_treasures
					if ratio >= 0.5 then
						--~ A parrot gives a somewhat cryptic hint about the hiding spots of parrots. "One rank, one parrot." means: There is one hidden parrot per difficulty. The translation does not need to be literal.
						text = text .. "\n\n" .. S("By the way, our hiding spots follow a rule: One rank, one parrot.")
					end
				end
			elseif hidden_parrot_identity == found_parrot_names[2] then
				if found < #hidden_parrots then
					--~ gang = parrot gang (hidden parrots)
					text = PS("Only @1 parrot of our gang is hiding somewhere in the world. But where?", "There are still @1 parrots of our gang hiding somewhere in the world. But where?", remaining, remaining)
				else
					--~ parrot speech
					text = S("Let’s plunder the world together!")
				end
			elseif hidden_parrot_identity == found_parrot_names[3] then
				--~ parrot speech
				text = S("Each of us is named after a gemstone.")
			elseif hidden_parrot_identity == found_parrot_names[4] then
				if found < #hidden_parrots then
					if found == #hidden_parrots - 1 then
						--~ Said by one of the (now found) hidden parrots
						text = S("Please help us find our last friend.")
					else
						--~ Said by one of the (now found) hidden parrots
						text = PS("Please help us find our @1 friend.", "Please help us find our @1 friends.", remaining, remaining)
					end
				else
					--~ parrot speech
					text = S("Yohoho, buddy!")
				end
			elseif hidden_parrot_identity == found_parrot_names[5] then
				if found < #hidden_parrots then
					--~ Said by a parrot. The Feather Gang is a group of parrots
					text = S("I’m excited! The Feather Gang is almost complete!")
				else
					--~ Said by a parrot. The Feather Gang is a group of parrots
					text = S("Arr! With the mighty Feather Gang at your side, you’re unstoppable!")
				end
			elseif hidden_parrot_identity == found_parrot_names[6] then
				text = S("Goldie seems to be really obsessed with gold. I’m more into crystals.")
			else
				-- Fallback text
				text = S("Yohoho, buddy!")
			end
			-- Since we’re in the ship, the parrot is now a Found Parrot ;-)
			local found_parrot_identity = hidden_parrot_identity .. "_found"
			lzr_speech.speak(player, text, found_parrot_identity)
		else
			make_call(player, call_pitch)
		end
		if parrot._flying_home_phase == 0 then
			face_player(parrot, player)
			-- Bounce head
			if parrot._animation_timer > 0.5 then
				local anim = PARROT_ANIMS.head_bounce
				parrot.object:set_animation(anim.frame_range, anim.frame_speed, 0, false)
				parrot._animation_timer = 0
				parrot._next_animation_at = math.random(PARROT_ANIMATION_DELAY_MIN, PARROT_ANIMATION_DELAY_MAX)
			end
		end
	end
end

local hidden_parrot_fly_home = function(parrot)
	if parrot._flying_home_phase < 1 then
		parrot._flying_home_phase = 1
		parrot._fly_timer = 0
		minetest.log("action", "[lzr_parrot_npc] Hidden parrot will fly home soon: "..tostring(last_hidden_speaker))
	end
end

minetest.register_on_player_receive_fields(function(player, formname, fields)
	if formname == "lzr_speech:speech" and fields.quit and last_hidden_speaker then
		local parrot = last_hidden_speaker_object
		hidden_parrot_fly_home(parrot)
	end
end)

local parrot_step_hidden = function(self, dtime)
	if self._flying_home_phase ~= 0 then
		if self._scorched and self._base_textures then
			self._scorched_timer = self._scorched_timer + dtime
			if self._scorched_timer >= self._unscorched_time then
				self.object:set_properties({
					textures = self._base_textures,
				})
				self._scorched = false
			end
		end

		-- Phase 1: Wait for a short time before lifting off
		if self._flying_home_phase == 1 then
			self._fly_timer = self._fly_timer + dtime
			if self._fly_timer > HIDDEN_PARROT_FLY_DELAY then
				self._flying_home_phase = 2
				self._fly_start_pos = self.object:get_pos()
				self.object:set_velocity({x=0,y=3,z=0})
				local anim = PARROT_ANIMS.flap_big
				self.object:set_animation(anim.frame_range, anim.frame_speed, 0, true)
				minetest.log("action", "[lzr_parrot_npc] Hidden parrot lifts off: "..tostring(self._hidden_id))
			end
		-- Phase 2: Liftoff! Fly upwards until we reach a certain height
		elseif self._flying_home_phase == 2 then
			local pos = self.object:get_pos()
			local fly_height = pos.y - self._fly_start_pos.y
			if fly_height >= HIDDEN_PARROT_FLY_HEIGHT then
				self.object:set_velocity(vector.zero())
				self._flying_home_phase = 3
				local hpos = table.copy(pos)
				hpos.y = 0
				local spos = table.copy(lzr_globals.MENU_SHIP_POS)
				spos.y = 0
				local dir = vector.direction(hpos, spos)
				local vel = vector.multiply(dir, HIDDEN_PARROT_FLY_SPEED)
				self.object:set_velocity(vel)
				local yaw = minetest.dir_to_yaw(dir)
				self.object:set_yaw(yaw)
				minetest.log("action", "[lzr_parrot_npc] Hidden parrot flies home: "..tostring(self._hidden_id))
				return
			end
		-- Phase 3: Fly horizontally towards the main ship (rough direction)
		elseif self._flying_home_phase == 3 then
			local pos = self.object:get_pos()
			if vector.distance(pos, self._fly_start_pos) > HIDDEN_PARROT_FLY_DESPAWN_DISTANCE then
				minetest.log("action", "[lzr_parrot_npc] Hidden parrot has flown enough and despawns: "..tostring(self._hidden_id))
				self.object:remove()
				return
			end
		end
	else
		parrot_step_idle(self, dtime)
	end
end

-- Hidden Parrot NPC. One of the game's hidden parrots.
-- When interacted with, the parrot counts as found.
minetest.register_entity("lzr_parrot_npc:hidden_parrot", {
	initial_properties = {
		visual = "mesh",
		mesh = "lzr_parrot_npc_parrot.gltf",
		visual_size = { x=1, y=1, z=1 },
		textures = {
			texture_repeat("lzr_parrot_npc_ruby.png"),
		},
		static_save = false,
		physical = false,
		collide_with_objects = false,
		selectionbox = SELECTIONBOX,
		backface_culling = false,
	},
	_animation_timer = 0,
	_next_animation_at = nil,
	_stuck_check_timer = 0,
	_unstucking = false,
	_scorched = false,
	_scorched_timer = nil,
	_unchorched_time = nil,
	_scorch = parrot_scorch,
	_base_textures = texture_repeat("lzr_parrot_npc_ruby.png"),

	_fly_start_pos = nil,
	_fly_timer = 0,
	_flying_home_phase = 0,
	-- Hidden parrot ID
	_hidden_id = nil,
	-- Initialize hidden parrot with the given ID (parrot identifier).
	-- MUST be called right after spawning entity.
	_init = function(self, hidden_id)
		self._hidden_id = hidden_id
		local name = S(hidden_parrot_names_short[hidden_id])
		local textures = texture_repeat("lzr_parrot_npc_"..hidden_id..".png")
		self.object:set_properties({
			textures = textures,
			infotext = name,
		})
		self._base_textures = textures
	end,

	on_activate = parrot_activate,
	on_step = parrot_step_hidden,
	on_rightclick = function(self, clicker)
		react_hidden(self, clicker)
	end,
	on_punch = function(self, puncher)
		react_hidden(self, puncher)
	end,
})

minetest.register_node("lzr_parrot_npc:parrot_spawner", {
	--~ A block that spawns a normal parrot
	description = S("Parrot Spawner"),
	_tt_help = S("Goldie the Parrot will spawn here").."\n"..
		S("(only one per level allowed)"),
	tiles = {
		{ name = "lzr_parrot_npc_goldie_spawner_top.png", backface_culling = true },
		{ name = "lzr_parrot_npc_goldie_spawner_bottom.png", backface_culling = true },
		{ name = "lzr_parrot_npc_goldie_spawner_left.png", backface_culling = true },
		{ name = "lzr_parrot_npc_goldie_spawner_right.png", backface_culling = true },
		{ name = "lzr_parrot_npc_goldie_spawner_back.png", backface_culling = true },
		{ name = "lzr_parrot_npc_goldie_spawner_front.png", backface_culling = true },
	},
	paramtype2 = "degrotate",
	drawtype = "mesh",
	mesh = "lzr_parrot_npc_cube.obj",
	selection_box = {
		type = "fixed",
		fixed = { -0.4, -0.4, -0.4, 0.4, 0.4, 0.4 },
	},
	visual_scale = 0.8,
	wield_scale = { x=0.8, y=0.8, z=0.8 },
	paramtype = "light",
	sunlight_propagates = true,
	walkable = false,
	groups = { breakable = 1, rotatable = 3, spawner = 1 },
	use_texture_alpha = "clip",
	sounds = {
		_rotate = "",
	},
})

local _tt_help_hps = S("A hidden parrot will spawn here")
if minetest.get_modpath("lzr_tools") then
	_tt_help_hps = _tt_help_hps .. "\n" .. S("Use the block variant changer to select the parrot")
end
minetest.register_node("lzr_parrot_npc:hidden_parrot_spawner", {
	--~ A block that spawns a hidden parrot
	description = S("Hidden Parrot Spawner"),
	_tt_help = _tt_help_hps,
	tiles = {
		{ name = "lzr_parrot_npc_hidden_parrot_spawner_top.png", backface_culling = true },
		{ name = "lzr_parrot_npc_hidden_parrot_spawner_bottom.png", backface_culling = true },
		{ name = "lzr_parrot_npc_hidden_parrot_spawner_left.png", backface_culling = true },
		{ name = "lzr_parrot_npc_hidden_parrot_spawner_right.png", backface_culling = true },
		{ name = "lzr_parrot_npc_hidden_parrot_spawner_back.png", backface_culling = true },
		{ name = "lzr_parrot_npc_hidden_parrot_spawner_front.png", backface_culling = true },
	},
	paramtype2 = "color4dir",
	drawtype = "mesh",
	palette = "lzr_parrot_npc_hidden_parrot_spawner_palette.png",
	mesh = "lzr_parrot_npc_cube.obj",
	selection_box = {
		type = "fixed",
		fixed = { -0.4, -0.4, -0.4, 0.4, 0.4, 0.4 },
	},
	visual_scale = 0.8,
	wield_scale = { x=0.8, y=0.8, z=0.8 },
	paramtype = "light",
	sunlight_propagates = true,
	walkable = false,
	-- hide the hidden parrot spawner ;)
	groups = { breakable = 1, rotatable = 3, spawner = 1, not_in_creative_inventory = 1 },
	use_texture_alpha = "clip",
	sounds = {
		_rotate = "",
	},
	drop = "lzr_parrot_npc:hidden_parrot_spawner",
	_lzr_next_variant = function(node)
		local id = math.floor(node.param2 / 4)
		local mod = node.param2 % 4
		id = id + 1
		if id > (#hidden_parrots - 1) then
			id = 0
		end
		node.param2 = id*4 + mod
		return node
	end,
	_lzr_prev_variant = function(node)
		local id = math.floor(node.param2 / 4)
		local mod = node.param2 % 4
		id = id - 1
		if id < 0 then
			id = #hidden_parrots - 1
		end
		node.param2 = id*4 + mod
		return node
	end,
})

function lzr_parrot_npc.clear_hidden_parrot_progress()
	for h=1, #hidden_parrots do
		mod_storage:set_int("lzr_parrot_npc:hidden_parrot_"..hidden_parrots[h], HIDDEN_PARROT_NOT_FOUND)
	end
	lzr_menu.remove_painting("parrot_finder")

	minetest.log("action", "[lzr_parrot_npc] Hidden parrot progress was cleared")
end

local function spawn_menu_parrots()
	for p=1, #hidden_parrots do
		local parrot_name = hidden_parrots[p]
		if lzr_parrot_npc.was_hidden_parrot_found(parrot_name) then
			lzr_menu.spawn_hidden_parrot(get_hidden_parrot_param2_values(parrot_name), parrot_name)
		end
	end
end

lzr_menu.register_on_ship_built(function()
	spawn_menu_parrots()
end)

lzr_menu.register_on_ship_rebuilt(function()
	spawn_menu_parrots()
	if lzr_parrot_npc.were_all_hidden_parrots_found() then
		lzr_menu.place_painting("parrot_finder")
	end
end)

lzr_menu.register_on_player_ship_enter(function()
	if lzr_menu.ship_analyzed then
		spawn_menu_parrots()
	end
end)

lzr_menu.register_on_parrots_respawn(function()
	spawn_menu_parrots()
end)

-- Notify parrots to instantly trigger unstuck algorithm when a node was placed
minetest.register_on_placenode(function(pos, node)
	local def = minetest.registered_nodes[node.name]
	if def and def.walkable then
		local objs = minetest.get_objects_inside_radius(pos, 1)
		for o=1, #objs do
			local obj = objs[o]
			local ent = obj:get_luaentity()
			if ent and (ent.name == "lzr_parrot_npc:parrot" or ent.name == "lzr_parrot_npc:hidden_parrot") then
				parrot_check_stuck(ent)
			end
		end
	end
end)
-- Notify parrots to instantly trigger unstuck algorithm when a node was dug
minetest.register_on_dignode(function(pos, node)
	local apos = vector.offset(pos, 0, 1, 0)
	local objs = minetest.get_objects_inside_radius(apos, 1)
	for o=1, #objs do
		local obj = objs[o]
		local ent = obj:get_luaentity()
		if ent and (ent.name == "lzr_parrot_npc:parrot" or ent.name == "lzr_parrot_npc:hidden_parrot") then
			parrot_check_stuck(ent)
		end
	end
end)

-- Register parrot speakers
for id, portrait in pairs(speaker_portraits) do
	if id == "goldie" then
		lzr_speech.register_speaker(id, S(GOLDIE_NAME), portrait, "lzr_parrot_npc_curr")
	else
		lzr_speech.register_speaker(id, S(hidden_parrot_names[id]), portrait, "lzr_parrot_npc_curr")
		lzr_speech.register_speaker(id.."_found", S(found_parrot_names[id]), portrait, "lzr_parrot_npc_curr")
	end
end

-- Hidden command for hidden parrots. ;-)
-- Only available in debug mode.
if minetest.settings:get_bool("lzr_debug", false) then
	minetest.register_chatcommand("find_hidden_parrot", {
		privs = { server = true, debug = true },
		description = S("Mark a hidden parrot as found"),
		params = S("<parrot name>"),
		func = function(name, param)
			if param == "" then
				return false
			elseif not hidden_parrot_names[param] then
				return false, S("Hidden parrot “@1” does not exist.", param)
			end
			local player = minetest.get_player_by_name(name)
			if not player:is_player() then
				return false, S("No player.")
			end
			mark_hidden_parrot_as_found(player, param)
			return true, S("Hidden parrot found.")
		end,
	})

end
