local S = minetest.get_translator("lzr_menu")
local FS = function(...) return minetest.formspec_escape(S(...)) end

local SPEAKER_POS = vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_SPEAKER_OFFSET)

local COLOR_PICKER_MODIFIERS = { -32, -8, -1, 1, 8, 32 }

local MENU_LASER_ALPHA = "BF"

-- Time it takes (in seconds) to update the shown menu markers
local MENU_MARKER_UPDATE_INTERVAL = 0.5

lzr_menu = {}

lzr_menu.SHIP_SIZE = nil

local registered_on_ship_builts = {}
-- Register callback function that is called when the ship has been built
-- and is ready for play. Will be called only once for the current game session.
function lzr_menu.register_on_ship_built(func)
	table.insert(registered_on_ship_builts, func)
end

local registered_on_ship_rebuilts = {}
-- Register callback function that is called when the ship has been built
-- OR rebuilt (after an update).
function lzr_menu.register_on_ship_rebuilt(func)
	table.insert(registered_on_ship_rebuilts, func)
end

local registered_on_player_ship_enters = {}
-- Register callback function that is called when the player enters the ship.
function lzr_menu.register_on_player_ship_enter(func)
	table.insert(registered_on_player_ship_enters, func)
end

local place_gold_stash = function(gold_blocks)
	if gold_blocks <= 0 then
		return
	end
	local min_pos_first = vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_STASH_OFFSET_FIRST)
	local max_pos_first = vector.add(min_pos_first, lzr_globals.MENU_SHIP_STASH_SIZE_FIRST)
	local min_pos_second = vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_STASH_OFFSET_SECOND)
	local max_pos_second = vector.add(min_pos_second, lzr_globals.MENU_SHIP_STASH_SIZE_SECOND)
	local min_pos = min_pos_first
	local max_pos = max_pos_first
	-- The gold blocks are placed in two "passes": In the first pass, blocks are placed in a smaller
	-- cuboid first, and when that one is full, we move to the second pass.
	local first_pass = true
	local pos = table.copy(min_pos)
	local posses = {}
	for g=0, gold_blocks-1 do
		if pos.z > max_pos.z then
			if first_pass then
				min_pos = min_pos_second
				max_pos = max_pos_second
				first_pass = false
				pos = table.copy(min_pos)
			else
				break
			end
		end
		local gpos = table.copy(pos)
		if g % 4 == 1 then
			gpos.x = gpos.x + 1
		elseif g % 4 == 2 then
			gpos.z = gpos.z + 1
		elseif g % 4 == 3 then
			gpos.x = gpos.x + 1
			gpos.z = gpos.z + 1
			pos.y = pos.y + 1
			if pos.y >= max_pos.y then
				pos.y = min_pos.y
				pos.x = pos.x + 3
				if pos.x > max_pos.x then
					pos.x = min_pos.x
					pos.z = pos.z + 3
				end
			end
		end
		table.insert(posses, gpos)
	end
	minetest.bulk_set_node(posses, {name="lzr_treasure:gold_block"})
	minetest.log("action", "[lzr_menu] Placed "..gold_blocks.." gold block(s) in ship treasure stash")
end

local ship_emerged = false
local gold_stashed = 0

local build_ship = function()
	minetest.place_schematic(lzr_globals.MENU_SHIP_POS, minetest.get_modpath("lzr_menu").."/schematics/lzr_menu_ship.mts", "0", {}, true, "")
	minetest.set_node(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_STARTBOOK_OFFSET), {name="lzr_menu:level_starter", param2=0})
	minetest.set_node(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_CUSTOMBOOK_OFFSET), {name="lzr_menu:custom_level_starter", param2=0})
	minetest.set_node(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_EDITOR_OFFSET), {name="lzr_menu:editor_starter", param2=1})
	minetest.set_node(SPEAKER_POS, {name="lzr_menu:speaker", param2=2})
	minetest.set_node(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_TELEVISION_OFFSET), {name="lzr_menu:television", param2=2})
	minetest.set_node(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_HOWTO_BOOKSHELF_OFFSET), {name="lzr_menu:bookshelf_info", param2=1})
	minetest.set_node(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_HOWTO_EDITOR_BOOKSHELF_OFFSET), {name="lzr_menu:bookshelf_info_editor", param2=1})
	minetest.set_node(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_HOWTO_BLOCKS_BOOKSHELF_OFFSET), {name="lzr_menu:bookshelf_info_blocks", param2=1})
	minetest.set_node(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_CREDITS_BOOKSHELF_OFFSET), {name="lzr_menu:bookshelf_credits", param2=1})
	minetest.set_node(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_LEVEL_PACKS_BOOKSHELF_OFFSET), {name="lzr_menu:bookshelf_level_packs", param2=1})
	minetest.set_node(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_RESET_BOMB_OFFSET), {name="lzr_menu:reset_bomb", param2=0})

	local core_level_data = lzr_levels.get_level_pack("__core")
	if lzr_levels.are_all_levels_completed(core_level_data) then
		lzr_menu.place_painting("perfect_plunderer")
	end

	-- Call 'rebuilt' callbacks
	for f=1, #registered_on_ship_rebuilts do
		registered_on_ship_rebuilts[f]()
	end
end

lzr_menu.update_treasure_stash = function(gold_blocks)
	if gold_stashed == gold_blocks then
		return
	end
	if ship_emerged then
		if gold_stashed > gold_blocks then
			build_ship()
		end
		place_gold_stash(gold_blocks)
		gold_stashed = gold_blocks
	else
		gold_stashed = gold_blocks
	end
end

lzr_levels.register_on_collected_treasure(function(treasures)
	lzr_menu.update_treasure_stash(treasures)
end)

local emerge_callback = function(blockpos, action, calls_remaining, param)
	minetest.log("verbose", "[lzr_menu] emerge_callback() ...")
	if action == minetest.EMERGE_ERRORED then
		minetest.log("error", "[lzr_menu] Emerging error.")
	elseif action == minetest.EMERGE_CANCELLED then
		minetest.log("error", "[lzr_menu] Emerging cancelled.")
	elseif calls_remaining == 0 and (action == minetest.EMERGE_GENERATED or action == minetest.EMERGE_FROM_DISK or action == minetest.EMERGE_FROM_MEMORY) then
		ship_emerged = true
		build_ship()
		minetest.log("action", "[lzr_menu] Ship emerged and built.")
		place_gold_stash(gold_stashed)

		if lzr_levels.is_intro_completed() then
			-- Now that the ship has *actually* been built,
			-- setting the player pos should guarantee the player
			-- stands on solid ground
			local player = minetest.get_player_by_name("singleplayer")
			lzr_menu.teleport_player_to_ship(player, "captain")
		end

		-- Call 'built' callbacks
		for f=1, #registered_on_ship_builts do
			registered_on_ship_builts[f]()
		end

		-- Call 'built' callbacks
		for f=1, #registered_on_ship_builts do
			registered_on_ship_builts[f]()
		end
	end
end

local SPEAKER_NOTE_INTERVAL = 1.0

local emerge_ship = function(pos)
	local ship_schem = minetest.read_schematic(minetest.get_modpath("lzr_menu").."/schematics/lzr_menu_ship.mts", {write_yslice_prob="none"})
	if not ship_schem then
		minetest.log("error", "[lzr_menu] Could not read ship schematic!")
		return
	end

	lzr_menu.SHIP_SIZE = ship_schem.size

	minetest.emerge_area(pos, vector.add(pos, lzr_menu.SHIP_SIZE), emerge_callback)
end

minetest.register_on_joinplayer(function(player)
	emerge_ship(lzr_globals.MENU_SHIP_POS)

	if lzr_levels.is_intro_completed() then
		lzr_gamestate.set_state(lzr_gamestate.MENU)
		-- NOTE: This teleport happens BEFORE the ship was actually built, so the
		-- player may fall for a moment. This is thus not reliable
		-- and is only made in prepatation for the future ship spawn.
		lzr_menu.teleport_player_to_ship(player, "captain")
		player:set_look_horizontal(0)
		player:set_look_vertical(0)
		local inv = player:get_inventory()
		for i=1,inv:get_size("main") do
			inv:set_stack("main", i, "")
		end
	end
end)

local on_punch_start_core = function(self, node, puncher)
	if lzr_gamestate.get_state() ~= lzr_gamestate.MENU then
		return
	end
	lzr_level_select.open_dialog(puncher, "core")
end
local on_rightclick_start_core = function(self, node, clicker)
	if lzr_gamestate.get_state() ~= lzr_gamestate.MENU then
		return
	end
	lzr_level_select.open_dialog(clicker, "core")
end
local on_punch_start_custom = function(self, node, puncher)
	if lzr_gamestate.get_state() ~= lzr_gamestate.MENU then
		return
	end
	lzr_level_select.open_dialog(puncher, "custom")
end
local on_rightclick_start_custom = function(self, node, clicker)
	if lzr_gamestate.get_state() ~= lzr_gamestate.MENU then
		return
	end
	lzr_level_select.open_dialog(clicker, "custom")
end

-- A node that starts level selection
minetest.register_node("lzr_menu:level_starter", {
	--~ A node that starts level selection
	description = S("Level Starter"),
	-- symbolized by a map
	tiles = { "lzr_menu_map.png", "blank.png" },
	paramtype = "light",
	paramtype2 = "4dir",
	drawtype = "nodebox",
	node_box = {
		type = "fixed",
		fixed = {
			{ -0.5, -0.5, -0.5, 0.5, -63/128, 0.5 },
		}
	},
	use_texture_alpha = "clip",
	walkable = false,
	sunlight_propagates = true,
	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		meta:set_string("infotext", S("Start playing"))
	end,
	on_punch = on_punch_start_core,
	on_rightclick = on_rightclick_start_core,
	groups = { snappy = 3, not_in_creative_inventory = 1, },
})

-- A node that starts custom level selection
minetest.register_node("lzr_menu:custom_level_starter", {
	--~ A node that starts custom level selection
	description = S("Custom Level Starter"),
	-- symbolized by a map
	tiles = { "lzr_menu_map_custom.png", "blank.png" },
	paramtype = "light",
	paramtype2 = "4dir",
	drawtype = "nodebox",
	node_box = {
		type = "fixed",
		fixed = {
			{ -0.5, -0.5, -0.5, 0.5, -63/128, 0.5 },
		}
	},
	use_texture_alpha = "clip",
	walkable = false,
	sunlight_propagates = true,
	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		meta:set_string("infotext", S("Play custom levels"))
	end,
	on_punch = on_punch_start_custom,
	on_rightclick = on_rightclick_start_custom,
	groups = { snappy = 3, not_in_creative_inventory = 1, },
})

local on_punch_editor = function(self, node, puncher)
	if lzr_gamestate.get_state() ~= lzr_gamestate.MENU then
		return
	end
	lzr_editor.enter_editor(puncher)
end
local on_rightclick_editor = function(self, node, clicker)
	if lzr_gamestate.get_state() ~= lzr_gamestate.MENU then
		return
	end
	lzr_editor.enter_editor(clicker)
end

minetest.register_node("lzr_menu:editor_starter", {
	description = S("Level Editor Starter"),
	-- The level editor starter is represented by a saw
	tiles = { "lzr_menu_saw.png", "blank.png" },
	paramtype = "light",
	paramtype2 = "4dir",
	drawtype = "nodebox",
	node_box = {
		type = "fixed",
		fixed = {
			{ -0.5, -0.5, -0.5, 0.5, -63/128, 0.5 },
		}
	},
	use_texture_alpha = "clip",
	walkable = false,
	sunlight_propagates = true,
	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		meta:set_string("infotext", S("Enter the level editor"))
	end,
	on_punch = on_punch_editor,
	on_rightclick = on_rightclick_editor,
	groups = { breakable = 1, rotatable = 1, not_in_creative_inventory = 1, },
})

local use_speaker = function(speaker_pos, user)
	if lzr_gamestate.get_state() ~= lzr_gamestate.MENU then
		return
	end
	local state = lzr_ambience.toggle_ambience_by_player(user, true)
	if state then
		minetest.sound_play({name="lzr_menu_speaker_turn_on", gain=0.4}, {pos=speaker_pos}, true)
	else
		minetest.sound_play({name="lzr_menu_speaker_turn_off", gain=0.4}, {pos=speaker_pos}, true)
	end
end

local update_speaker_infotext = function(pos)
	if minetest.get_node(pos).name ~= "lzr_menu:speaker" then
		return
	end
	local meta = minetest.get_meta(pos)
	local infotext = S("Toggle music") .. "\n\n"
	local active = lzr_ambience.is_active()
	local ambience_state = meta:get_int("ambience_state")
	-- Update infotext only if neccessary
	if active and ambience_state ~= 1 then
		infotext = infotext .. S("(Music is enabled)")
		meta:set_int("ambience_state", 1)
		meta:set_string("infotext", infotext)
	elseif not active and ambience_state ~= 2 then
		infotext = infotext .. S("(Music is disabled)")
		meta:set_int("ambience_state", 2)
		meta:set_string("infotext", infotext)
	end
end

local on_punch_speaker = function(pos, node, puncher)
	use_speaker(pos, puncher)
	update_speaker_infotext(pos)
end
local on_rightclick_speaker = function(pos, node, clicker)
	use_speaker(pos, clicker)
	update_speaker_infotext(pos)
end

minetest.register_node("lzr_menu:speaker", {
	--~ Loudspeaker
	description = S("Speaker"),
	tiles = {
		"lzr_menu_speaker_top.png",
		"lzr_menu_speaker_side.png",
		"lzr_menu_speaker_side.png",
		"lzr_menu_speaker_side.png",
		"lzr_menu_speaker_back.png",
		"lzr_menu_speaker_front.png",
	},
	paramtype2 = "4dir",
	groups = { breakable = 1, rotatable = 1, not_in_creative_inventory = 1},
	sounds = lzr_sounds.node_sound_wood_defaults(),
	is_ground_content = false,
	on_punch = on_punch_speaker,
	on_rightclick = on_rightclick_speaker,
	on_construct = function(pos)
		update_speaker_infotext(pos)
		local timer = minetest.get_node_timer(pos)
		timer:start(SPEAKER_NOTE_INTERVAL)
	end,
	on_timer = function(pos)
		if lzr_ambience.is_active() then
			local node = minetest.get_node(pos)
			local dir = minetest.fourdir_to_dir(node.param2)
			local notes = {
				"lzr_menu_speaker_note_quarter.png",
				"lzr_menu_speaker_note_eighth.png",
				"lzr_menu_speaker_note_eighth_2.png",
			}
			local note = notes[math.random(1, #notes)]
			local hue = math.random(-12, 12) * 15
			note = note .. "^[hsl:"..hue..":0:0"
			local notepos = vector.add(pos, vector.multiply(dir, -0.65))
			notepos = vector.offset(notepos, 0, -2/16, 0)
			minetest.add_particlespawner({
				amount = 1,
				time = SPEAKER_NOTE_INTERVAL / 2,
				pos = notepos,
				texture = {
					name = note,
					alpha_tween = { start = 0.6, 1, 0 },
				},
				size = 3,
				exptime = 1,
				vel = {
					min = vector.new(0, 0.2, 0),
					max = vector.new(0, 0.6, 0),
				},
			})
		end
		update_speaker_infotext(pos)
		local timer = minetest.get_node_timer(pos)
		timer:start(SPEAKER_NOTE_INTERVAL)
	end,
})

-- Instantly update the ship speaker infotext when ambience status
-- was changed (could be by chat command)
lzr_ambience.register_on_ambience_change(function(state)
	update_speaker_infotext(SPEAKER_POS)
end)

local graphics_settings_initial = {
	opaque_lasers = minetest.settings:get_bool("lzr_opaque_lasers", false),
	patterned_lasers = minetest.settings:get_bool("lzr_patterned_lasers", false),
}
for c=1, #lzr_globals.COLOR_NAMES do
	local colorname = lzr_globals.COLOR_NAMES[c]
	local setting = minetest.settings:get("lzr_laser_color_"..colorname)
	if not setting or setting == "" then
		setting = lzr_laser.DEFAULT_LASER_COLORS[c]
	end
	graphics_settings_initial["color_"..colorname] = setting
end

local graphics_settings_state = table.copy(graphics_settings_initial)

local color_descriptions = {
	-- [laser color] = { <color adjective>, <laser color button tooltip>, <long description> }
	[lzr_globals.COLOR_RED] = { S("red"), S("“red” lasers"), S("Adjust the replacement color for “red” lasers") },
	[lzr_globals.COLOR_GREEN] = { S("green"), S("“green” lasers"), S("Adjust the replacement color for “green” lasers") },
	[lzr_globals.COLOR_BLUE] = { S("blue"), S("“blue” lasers"), S("Adjust the replacement color for “blue” lasers") },
	[lzr_globals.COLOR_YELLOW] = { S("yellow"), S("“yellow” lasers"), S("Adjust the replacement color for “yellow” lasers") },
	[lzr_globals.COLOR_CYAN] = { S("cyan"), S("“cyan” lasers"), S("Adjust the replacement color for “cyan” lasers") },
	[lzr_globals.COLOR_MAGENTA] = { S("magenta"), S("“magenta” lasers"), S("Adjust the replacement color for “magenta” lasers") },
	[lzr_globals.COLOR_WHITE] = { S("white"), S("“white” lasers"), S("Adjust the replacement color for “white” lasers") },
}

local open_color_picker = function(player, colorname)
	local r = graphics_settings_state["picked_color_red"]
	local g = graphics_settings_state["picked_color_green"]
	local b = graphics_settings_state["picked_color_blue"]
	local colorcode = lzr_globals.COLOR_NAMES_SWAPPED[colorname]
	-- If missing or invalid setting, fallback to default
	if not r then
		if colorcode then
			local lhexcode = lzr_laser.DEFAULT_LASER_COLORS[colorcode]
			if lhexcode then
				r, g, b = lzr_util.hexcode_to_rgb(lhexcode)
			end
		end
		if not r then
			minetest.log("error", "[lzr_menu] open_color_picker could not pick default color for: "..colorname.."!")
			return
		end
	end
	local hexcode = lzr_util.rgb_to_hexcode(r,g,b)
	local channels = {
		{ "red", S("Red"), r },
		{ "green", S("Green"), g },
		{ "blue", S("Blue"), b },
	}

	local lalpha
	if graphics_settings_state.opaque_lasers then
		lalpha = "FF"
	else
		lalpha = MENU_LASER_ALPHA
	end
	local color_adj = color_descriptions[colorcode][1]

	local form = "" ..
	"formspec_version[7]size[9,7.1]"..

	"image[8.3,0.3;0.4,0.4;lzr_menu_tooltip_icon.png]"..
	"tooltip[8.3,0.3;0.4,0.4;"..
	minetest.formspec_escape(
		S("Here you can redefine the color @1 for lasers.", color_adj).."\n"..
		S("The game will pretend this is @1, even if the actual color is different.", color_adj)
	).."]"..

	"label[0.5,0.5;"..minetest.formspec_escape(color_descriptions[colorcode][3]).."]"..
	"box[1.0,1.2;7,0.2;#"..hexcode..lalpha.."]"..
	"container[0.5,2.0]"
	for c=1, #channels do
		local id = channels[c][1]
		local desc = channels[c][2]
		local defval = channels[c][3]
		local y = c - 1
		-- get modifier
		local function gm(index)
			local txt
			if COLOR_PICKER_MODIFIERS[index] < 0 then
				--~ Subtract color value in custom laser color menu
				return FS("−@1", math.abs(COLOR_PICKER_MODIFIERS[index]))
			else
				--~ Add color value in custom laser color menu
				return FS("+@1", COLOR_PICKER_MODIFIERS[index])
			end
		end
		form = form .. "label[0.39,"..(y+0.3)..";"..minetest.formspec_escape(desc).."]" ..
			"button[2,"..y..";0.7,0.5;channel_"..id.."_mmm;"..gm(1).."]" ..
			"button[2.7,"..y..";0.7,0.5;channel_"..id.."_mm;"..gm(2).."]"..
			"button[3.4,"..y..";0.7,0.5;channel_"..id.."_m;"..gm(3).."]"..
			--~ Color value number in custom laser color menu. @1 current value, @2 maximum possible value
			"label[4.4,"..(y+0.3)..";"..FS("@1/@2", defval, 255).."]"..
			"button[5.6,"..y..";0.7,0.5;channel_"..id.."_p;"..gm(4).."]"..
			"button[6.3,"..y..";0.7,0.5;channel_"..id.."_pp;"..gm(5).."]"..
			"button[7.0,"..y..";0.7,0.5;channel_"..id.."_ppp;"..gm(6).."]"
		local chancode
		if id == "red" then
			chancode = string.format("#%02x0000", defval)
		elseif id == "green" then
			chancode = string.format("#00%02x00", defval)
		elseif id == "blue" then
			chancode = string.format("#0000%02x", defval)
		end
		form = form .. "box[0.0,"..(y+0.15)..";0.3,0.3;"..chancode..lalpha.."]"
	end
	form = form .. "container_end[]"

	form = form .. "button[0.5,4.9;2,0.5;default;"..FS("Default").."]"..
	"tooltip[default;"..FS("Pick the default color").."]"

	form = form .. "button[0.7,6;3.5,0.8;ok;"..FS("OK").."]"..
	"button[4.75,6;3.5,0.8;cancel;"..FS("Cancel").."]"

	minetest.show_formspec(player:get_player_name(), "lzr_menu:color_picker", form)
end

local open_graphics_settings = function(player)
	graphics_settings_state.picking_color = nil
	local put_asterisk = function(x, y)
		return "label["..x..","..y..";"..minetest.formspec_escape("(*)").."]" ..
		"tooltip["..(x-0.08)..","..(y-0.2)..";0.4,0.4;"..S("This setting will take effect after a restart.").."]"
	end

	local form = "" ..
	-- Header
	"formspec_version[7]size[12,6.1]"..

	-- Opaque lasers
		"container[-0.6,0]" ..
		"checkbox[2.3,1;opaque_lasers;"..FS("Opaque lasers")..";"..tostring(graphics_settings_state.opaque_lasers).."]"

	form = form .. "box[1,0.5;1,1;#0000003f]"
	if graphics_settings_state.opaque_lasers then
		form = form .. "image[1.1,0.6;0.8,0.8;lzr_menu_settings_opaque_lasers_on.png]"
	else
		form = form .. "image[1.1,0.6;0.8,0.8;lzr_menu_settings_opaque_lasers_off.png]"
	end
	form = form .. "tooltip[opaque_lasers;"..FS("If enabled, lasers aren’t translucent. Can improve performance.").."]"
	if graphics_settings_state.opaque_lasers ~= graphics_settings_initial.opaque_lasers then
		form = form .. put_asterisk(6, 1)
	end

	-- Patterned lasers
	form = form .. "box[1,1.6;1,1;#0000003f]"
	if graphics_settings_state.patterned_lasers then
		form = form .. "image[1.1,1.7.1;0.8,0.8;lzr_menu_settings_patterned_lasers_on.png]"
	else
		form = form .. "image[1.1,1.7.1;0.8,0.8;lzr_menu_settings_patterned_lasers_off.png]"
	end
	form = form .. "checkbox[2.3,2.1;patterned_lasers;"..FS("Draw patterns on lasers")..";"..tostring(graphics_settings_state.patterned_lasers).."]" ..
		"tooltip[patterned_lasers;"..FS("Special patterns will appear on the lasers, one for each color. Helps to distinguish lasers without relying on color alone.").."]"

	if graphics_settings_state.patterned_lasers ~= graphics_settings_initial.patterned_lasers then
		form = form .. put_asterisk(6, 2.1)
	end
	form = form .. "container_end[]"

	local get_color_setting_texture = function(colorname)
		local hexcode = graphics_settings_state["color_"..colorname]
		local lalpha
		if graphics_settings_state.opaque_lasers then
			lalpha = "FF"
		else
			lalpha = MENU_LASER_ALPHA
		end
		if type(hexcode) == "string" and string.len(hexcode) == 6 then
			return "[fill:16x16:#"..hexcode..lalpha.."^[mask:lzr_menu_color_select_button_mask.png"
		elseif hexcode == "" or hexcode == nil then
			local colorcode = lzr_globals.COLOR_NAMES_SWAPPED[colorname]
			hexcode = lzr_laser.DEFAULT_LASER_COLORS[colorcode]
			-- This is an error
			if not hexcode then
				minetest.log("error", "[lzr_menu] get_color_setting_texture: Could not get default color for: "..colorname)
				return "blank.png"
			end
			return "[fill:16x16:#"..hexcode..lalpha.."^[mask:lzr_menu_color_select_button_mask.png"
		else
			-- Invalid color in settings
			return "blank.png"
		end
	end

	local are_colors_modified = function(colornames)
		for c=1, #colornames do
			local colorname = colornames[c]
			if graphics_settings_initial["color_"..colorname] ~= graphics_settings_state["color_"..colorname] then
				return true
			end
		end
	end

	-- Laser colors
	form = form .. "container[6.5,0.6]"
	form = form .. "box[-0.2,-0.2;5.4,3;#00000040]"
	form = form .. "label[0,0;"..FS("Laser colors:").."]"


	local ui_colors = { "red", "green", "blue", "yellow", "magenta", "cyan", "white" }
	if are_colors_modified(ui_colors) then
		form = form .. put_asterisk(4.5, 0)
	end


	local ibx = 0
	local iby = 0
	local max_x = 0
	local max_y = 0
	for c=1, #ui_colors do
		local col = ui_colors[c]
		local x = 0 + ibx*1.7
		local y = 0.3 + iby
		max_x = math.max(x, max_x)
		max_y = math.max(y, max_y)

		form = form .. "image_button["..x..","..y..";1.6,0.5;"..minetest.formspec_escape(get_color_setting_texture(col))..";color_"..col..";]" ..
			"tooltip[color_"..col..";"..minetest.formspec_escape(color_descriptions[lzr_globals.COLOR_NAMES_SWAPPED[col]][2]).."]"

		ibx = ibx + 1
		if ibx >= 3 then
			ibx = 0
			iby = iby + 0.6
		end
	end
	form = form .. "button[0,"..(max_y+0.7)..";3.2,0.5;color_reset;"..FS("Reset").."]" ..
		"tooltip[color_reset;"..FS("Reset all laser colors").."]" ..
		"container_end[]"

	-- Buttons
	form = form .. "button[2.5,4.7;3.1,0.8;apply;"..FS("Apply").."]"..
		"button_exit[6.5,4.7;3.1,0.8;cancel;"..FS("Cancel").."]"

	minetest.show_formspec(player:get_player_name(), "lzr_menu:graphics_settings", form)
end


minetest.register_on_player_receive_fields(function(player, formname, fields)
	if lzr_gamestate.get_state() ~= lzr_gamestate.MENU then
		return
	end
	local pname = player:get_player_name()
	if formname == "lzr_menu:graphics_settings" then
		if fields.opaque_lasers == "true" then
			graphics_settings_state.opaque_lasers = true
			open_graphics_settings(player)
		elseif fields.opaque_lasers == "false" then
			graphics_settings_state.opaque_lasers = false
			open_graphics_settings(player)
		end
		if fields.patterned_lasers == "true" then
			graphics_settings_state.patterned_lasers = true
			open_graphics_settings(player)
		elseif fields.patterned_lasers == "false" then
			graphics_settings_state.patterned_lasers = false
			open_graphics_settings(player)
		end
		for c=1, #lzr_globals.COLOR_NAMES do
			local color = lzr_globals.COLOR_NAMES[c]
			if fields["color_"..color] then
				graphics_settings_state.picking_color = color
				local r, g, b = lzr_util.hexcode_to_rgb(graphics_settings_state["color_"..color])
				graphics_settings_state["picked_color_red"] = r
				graphics_settings_state["picked_color_green"] = g
				graphics_settings_state["picked_color_blue"] = b
				open_color_picker(player, color)
				return
			end
		end
		if fields.color_reset then
			for c=1, #lzr_globals.COLOR_NAMES do
				local colorname = lzr_globals.COLOR_NAMES[c]
				graphics_settings_state["color_"..colorname] = lzr_laser.DEFAULT_LASER_COLORS[c]
			end
			open_graphics_settings(player)
			return
		end
		if fields.apply then
			-- Check if any setting has changed
			local changed = false
			for k, v in pairs(graphics_settings_initial) do
				if v ~= graphics_settings_state[k] then
					changed = true
					break
				end
			end
			if not changed then
				-- Nothing has changed: Nothing to do.
				minetest.close_formspec(pname, "lzr_menu:graphics_settings")
				return
			end

			minetest.settings:set_bool("lzr_opaque_lasers", graphics_settings_state.opaque_lasers)
			minetest.settings:set_bool("lzr_patterned_lasers", graphics_settings_state.patterned_lasers)

			for c=1, #lzr_globals.COLOR_NAMES do
				local colorname = lzr_globals.COLOR_NAMES[c]
				local cvalue = graphics_settings_state["color_"..colorname]
				if not cvalue then
					cvalue = ""
				end
				minetest.settings:set("lzr_laser_color_"..colorname, cvalue)
			end

			local form = "formspec_version[7]size[10,4]"..
				"textarea[1,0.8;8,1.5;;;"..FS("The game needs to be restarted for the new graphics settings to take effect.").."]"..
				"button_exit[1,2.5;3,0.8;restart;"..FS("Restart").."]"..
				"button_exit[5.5,2.5;3,0.8;cancel;"..FS("Keep playing").."]"
			minetest.show_formspec(pname, "lzr_menu:confirm_restart", form)
		elseif fields.quit or fields.cancel then
			graphics_settings_state = {
				opaque_lasers = minetest.settings:get_bool("lzr_opaque_lasers", false),
				patterned_lasers = minetest.settings:get_bool("lzr_patterned_lasers", false),
			}
			for c=1, #lzr_globals.COLOR_NAMES do
				local colorname = lzr_globals.COLOR_NAMES[c]
				graphics_settings_state["color_"..colorname] = minetest.settings:get("lzr_laser_color_"..colorname)
			end
		end
	elseif formname == "lzr_menu:confirm_restart" then
		if fields.restart then
			minetest.disconnect_player(pname, S("You’ve quit the game. Start the game again for the settings to take effect."))
		end
	elseif formname == "lzr_menu:color_picker" then
		local channels = { "red", "green", "blue" }
		local modifiers = { "mmm", "mm", "m", "p", "pp", "ppp" }
		for c=1, #channels do
		for m=1, #modifiers do
			local channel = channels[c]
			local modifier = modifiers[m]
			if fields["channel_"..channel.."_"..modifier] then
				local val = graphics_settings_state["picked_color_"..channel]
				if val then
					val = val + COLOR_PICKER_MODIFIERS[m]
					val = math.floor(math.min(255, math.max(0, val)))
				else
					val = 255
				end

				graphics_settings_state["picked_color_"..channel] = val
				open_color_picker(player, graphics_settings_state.picking_color)
			end
		end
		end
		if fields.default then
			local colorname = graphics_settings_state.picking_color
			if colorname then
				local colorcode = lzr_globals.COLOR_NAMES_SWAPPED[colorname]
				local default_color_hex = lzr_laser.DEFAULT_LASER_COLORS[colorcode]
				local r,g,b = lzr_util.hexcode_to_rgb(default_color_hex)
				graphics_settings_state["picked_color_red"] = r
				graphics_settings_state["picked_color_green"] = g
				graphics_settings_state["picked_color_blue"] = b
				open_color_picker(player, colorname)
			end
		end
		if fields.ok then
			local r = graphics_settings_state["picked_color_red"]
			local g = graphics_settings_state["picked_color_green"]
			local b = graphics_settings_state["picked_color_blue"]
			if r and g and b then
				r = math.floor(math.min(255, math.max(0, r)))
				g = math.floor(math.min(255, math.max(0, g)))
				b = math.floor(math.min(255, math.max(0, b)))
				local hex = lzr_util.rgb_to_hexcode(r, g, b)
				if graphics_settings_state.picking_color and hex then
					graphics_settings_state["color_"..graphics_settings_state.picking_color] = hex
				end
			end
			open_graphics_settings(player)
		elseif fields.cancel or fields.quit then
			open_graphics_settings(player)
		end
	elseif formname == "lzr_menu:reset_confirm" then
		if not minetest.global_exists("lzr_reset") then
			return
		end
		if fields.reset then
			lzr_reset.reset_progress()
			local pos = vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_RESET_BOMB_OFFSET)
			pos = vector.offset(pos, 0, 0.6, 0)
			lzr_laser.bomb_explosion_audiovisuals(pos, {name="lzr_menu:reset_bomb", param2=0})
		end
	end
end)

local function on_rightclick_television(_, _, player)
	if lzr_gamestate.get_state() ~= lzr_gamestate.MENU then
		return
	end

	open_graphics_settings(player)
end
local function on_punch_television(_, _, player)
	if lzr_gamestate.get_state() ~= lzr_gamestate.MENU then
		return
	end
	open_graphics_settings(player)
end

-- Television to update graphics settings
minetest.register_node("lzr_menu:television", {
	description = S("Television"),
	tiles = {
		"xdecor_television_left.png^[transformR90",
		"xdecor_television_left.png^[transformR270",
		"xdecor_television_left.png",
		"xdecor_television_left.png^[transformR180",
		"xdecor_television_back.png",
		{ name = "xdecor_television_front_animated.png", animation = { type = "vertical_frames", aspect_w = 16, aspect_h = 16, length = 1 } },
	},
	paramtype2 = "4dir",
	groups = { breakable = 1, rotatable = 1, not_in_creative_inventory = 1},
	sounds = lzr_sounds.node_sound_metal_defaults(),
	is_ground_content = false,
	on_punch = on_punch_television,
	on_rightclick = on_rightclick_television,
	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		meta:set_string("infotext", S("Change graphics settings"))
	end,
})

local register_bookshelf = function(id, def)
	minetest.register_node("lzr_menu:bookshelf_"..id, {
		description = def.description,
		paramtype2 = "4dir",
		tiles = {
			{name="default_wood.png", align_style="world"},
			{name="default_wood.png", align_style="world"},
			{name="default_wood.png", align_style="world"},
			{name="default_wood.png", align_style="world"},
			"default_wood.png^lzr_decor_bookshelf.png",
		},
		sounds = lzr_sounds.node_sound_wood_defaults(),
		on_construct = function(pos)
			local meta = minetest.get_meta(pos)
			meta:set_string("infotext", def.infotext)
		end,
		on_punch = function(self, node, puncher)
			if lzr_gamestate.get_state() ~= lzr_gamestate.MENU then
				return
			end
			def.action(puncher)
		end,
		on_rightclick = function(self, node, clicker)
			if lzr_gamestate.get_state() ~= lzr_gamestate.MENU then
				return
			end
			def.action(clicker)
		end,
		groups = { not_in_creative_inventory = 1 },
	})
end

register_bookshelf("info", {
	description = S("Gameplay Info Book Bookshelf"),
	infotext = S("Read about how to play the game"),
	action = function(player)
		lzr_infobooks.open_bookshelf(player:get_player_name(), "how_to_play")
	end,
})

register_bookshelf("info_editor", {
	description = S("Editor Info Book Bookshelf"),
	infotext = S("Read about how to use the level editor"),
	action = function(player)
		lzr_infobooks.open_bookshelf(player:get_player_name(), "editor")
	end,
})

register_bookshelf("info_blocks", {
	description = S("Blocks Info Book Bookshelf"),
	infotext = S("Learn about the blocks in the game"),
	action = function(player)
		lzr_infobooks.open_bookshelf(player:get_player_name(), "blocks")
	end,
})

register_bookshelf("credits", {
	description = S("Credits Bookshelf"),
	infotext = S("Display the list of people who worked on this game"),
	action = function(player)
		lzr_credits.show_credits(player)
	end,
})

register_bookshelf("level_packs", {
	description = S("Level Packs Bookshelf"),
	infotext = S("Display information and stats about the installed level packs"),
	action = function(player)
		lzr_level_select.open_stats_dialog(player)
	end,
})

-- This painting symbolizes completion of the game
minetest.register_node("lzr_menu:painting_perfect_plunderer", {
	description = S("Painting: Perfect Plunderer"),
	drawtype = "nodebox",
	paramtype2 = "wallmounted",
	paramtype = "light",
	sunlight_propagates = true,
	walkable = false,
	node_box = {
		type = "wallmounted",
		wall_top = {-0.4375, 0.4375, -0.3725, 0.4375, 0.5, 0.3725},
		wall_bottom = {-0.4375, -0.5, -0.3725, 0.4375, -0.4375, 0.3725},
		wall_side = {-0.5, -0.3725, -0.4375, -0.4375, 0.3725, 0.4375},
	},
	tiles = {"lzr_menu_painting_perfect_plunderer.png", "lzr_menu_painting_back.png"},
	use_texture_alpha = "clip",
	groups = { breakable = 1, rotatable = 3, not_in_creative_inventory = 1 },
	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		meta:set_string("infotext", S("Found every treasure of the known world"))
	end,
})

local on_punch_reset_bomb = function(self, node, puncher)
	if lzr_gamestate.get_state() ~= lzr_gamestate.MENU then
		return
	end
	local form = "formspec_version[7]size[6,4]"..
		"textarea[0.5,0.5;5,2;;;"..minetest.formspec_escape(S("Do you want to reset the entire game progress? This cannot be undone.")).."]"..
		"button_exit[0.5,3;2,0.6;reset;"..minetest.formspec_escape(S("Yes")).."]"..
		"set_focus[cancel]"..
		"button_exit[3.5,3;2,0.6;cancel;"..minetest.formspec_escape(S("No")).."]"
	minetest.show_formspec(puncher:get_player_name(), "lzr_menu:reset_confirm", form)
end
local on_rightclick_reset_bomb = on_punch_reset_bomb

-- This allows the player to reset the game progress
minetest.register_node("lzr_menu:reset_bomb", {
	--~ A special "bomb" that resets the game progress
	description = S("Reset Bomb"),
	paramtype2 = "facedir",
	tiles = {"lzr_menu_reset_bomb_top.png^lzr_laser_fixed.png", "lzr_menu_reset_bomb_bottom.png^lzr_laser_fixed.png", "lzr_menu_reset_bomb_side.png^lzr_laser_fixed.png"},
	groups = { breakable = 1, not_in_creative_inventory = 1 },
	sounds = lzr_sounds.node_sound_metal_defaults(),
	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		meta:set_string("infotext", S("Reset game progress"))
	end,
	on_punch = on_punch_reset_bomb,
	on_rightclick = on_rightclick_reset_bomb,
})

function lzr_menu.spawn_hidden_parrot(parrot_name)
	local offset = lzr_globals.MENU_SHIP_HIDDEN_PARROT_OFFSETS[parrot_name]
	if not offset then
		minetest.log("error", "[lzr_menu] Unknown hidden parrot ship spawn offset for parrot '"..tostring(parrot_name).."'!")
		return
	end
	local pos = vector.add(lzr_globals.MENU_SHIP_POS, offset)
	-- Check if this parrot is already spawned at this pos
	local objs = minetest.get_objects_inside_radius(pos, 0.5)
	for o=1, #objs do
		local obj = objs[o]
		local ent = obj:get_luaentity()
		if ent and ent.name == "lzr_parrot_npc:hidden_parrot" and ent._hidden_id == parrot_name then
			-- No spawn if parrot is already there
			return
		end
	end

	-- Spawn parrot, then check for success
	local obj = minetest.add_entity(pos, "lzr_parrot_npc:hidden_parrot")
	if not obj then
		minetest.log("error", "[lzr_menu] Failed to spawn in-ship hidden parrot '"..tostring(parrot_name).."'!")
		return
	end
	local ent = obj:get_luaentity()
	if not ent then
		minetest.log("error", "[lzr_menu] Failed to get luaentity of in-ship hidden parrot '"..tostring(parrot_name).."'!")
		return
	end
	-- Ship parrots look randomly
	local yaw = (math.random(0,359) / 360) * (math.pi*2)
	obj:set_yaw(yaw)

	-- Assign parrot name
	ent:_init(parrot_name)
end

function lzr_menu.teleport_player_to_ship(player, location)
	if location == "captain" then
		player:set_pos(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_PLAYER_SPAWN_OFFSET))
		player:set_look_horizontal(0)
		player:set_look_vertical(0)
	elseif location == "skulls" then
		player:set_pos(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_PLAYER_RESPAWN_OFFSET))
		player:set_look_horizontal(0)
		player:set_look_vertical(0)
	elseif location == "victory" then
		player:set_pos(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_PLAYER_WINSPAWN_OFFSET))
		player:set_look_horizontal(math.pi)
		player:set_look_vertical(0)
	else
		minetest.log("error", "[lzr_menu] Tried to teleport player to invalid ship location: "..tostring(location))
		return
	end
	local zone = lzr_menu.get_player_zone(player)
	lzr_gui.set_menu_gui(player, zone)
	for f=1, #registered_on_player_ship_enters do
		registered_on_player_ship_enters[f](player, location)
	end
end

function lzr_menu.remove_hidden_parrots()
	if not lzr_menu.SHIP_SIZE then
		return
	end
	local max_pos = vector.add(lzr_globals.MENU_SHIP_POS, lzr_menu.SHIP_SIZE)
	local objs = minetest.get_objects_in_area(lzr_globals.MENU_SHIP_POS, max_pos)
	for o=1, #objs do
		local obj = objs[o]
		local ent = obj:get_luaentity()
		if ent and ent.name == "lzr_parrot_npc:hidden_parrot" then
			obj:remove()
		end
	end
end

function lzr_menu.place_painting(painting)
	if painting == "perfect_plunderer" then
		minetest.set_node(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_PAINTING_PERFECT_PLUNDERER_OFFSET), {name="lzr_menu:painting_perfect_plunderer", param2=4})
		minetest.log("action", "[lzr_menu] Added painting 'perfect_plunderer'")
	elseif painting == "parrot_finder" then
		minetest.set_node(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_PAINTING_PARROT_FINDER_OFFSET), {name="lzr_menu:painting_parrot_finder", param2=4})
		minetest.log("action", "[lzr_menu] Added painting 'parrot_finder'")
	end
end
function lzr_menu.remove_painting(painting)
	if painting == "perfect_plunderer" then
		minetest.remove_node(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_PAINTING_PERFECT_PLUNDERER_OFFSET))
		minetest.log("action", "[lzr_menu] Removed painting 'perfect_plunderer'")
	elseif painting == "parrot_finder" then
		minetest.remove_node(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_PAINTING_PARROT_FINDER_OFFSET))
		minetest.log("action", "[lzr_menu] Removed painting 'perfect_plunderer'")
	end
end

lzr_gamestate.register_on_enter_state(function(state)
	if state == lzr_gamestate.MENU then
		local player = minetest.get_player_by_name("singleplayer")
		lzr_player.set_menu_inventory(player)

		lzr_ambience.set_ambience("village")
		lzr_sky.set_sky("bright_blue")
		lzr_weather.set_weather("clear")
		lzr_triggers.reset_triggers()

		-- Goldie the Parrot explains the ship the first time the player enters it
		if not lzr_levels.was_menu_seen() then
			local core_level_data = lzr_levels.get_level_pack("__core")
			-- ... but only the player did NOT already finished the core levels, to avoid collision with the "game completed" message.
			-- (This scenario can happen when the level completion status was manipulated.)
			if not (core_level_data and lzr_levels.are_all_levels_completed(core_level_data)) then
				lzr_speech.speak(player,
					S("Welcome to the Ms. Bones, our ship! This is the center of all our pirate operations.").."\n"..
					S("Here you can plan your next travel, change the game settings, check out the loot and do more.").."\n\n"..
					S("Let’s fill this ship with gold!").."\n\n"..
					-- TODO: Remove this message when version 2.0.0 drops
					S("(You’re playing a beta version with fewer levels. Stay tuned for version 2.0.0!)"),
					"goldie")
			end
			lzr_levels.set_menu_seen(true)
		end
	end
end)

lzr_menu.get_player_zone = function(player)
	local pos = player:get_pos()

	for zone_id, zone in pairs(lzr_globals.MENU_SHIP_ZONES) do
		-- Main area
		if vector.in_area(pos,
				vector.add(lzr_globals.MENU_SHIP_POS, zone.min),
				vector.add(lzr_globals.MENU_SHIP_POS, zone.max)) then
			if zone.exclusions then
				local in_exclusion = false
				-- Exclusion zones
				for i=1, #zone.exclusions do
					if vector.in_area(pos,
							vector.add(lzr_globals.MENU_SHIP_POS, zone.exclusions[i].min),
							vector.add(lzr_globals.MENU_SHIP_POS, zone.exclusions[i].max)) then
						in_exclusion = true
						break
					end
				end
				if not in_exclusion then
					return zone_id
				end
			else
				return zone_id
			end
		end
	end
	return nil
end

local timer = 0
minetest.register_globalstep(function(dtime)
	timer = timer + dtime
	if timer < MENU_MARKER_UPDATE_INTERVAL then
		return
	end
	timer = 0
	if lzr_gamestate.get_state ()~= lzr_gamestate.MENU then
		return
	end
	local player = minetest.get_player_by_name("singleplayer")
	if not player then
		return
	end
	local pos = player:get_pos()

	local zone = lzr_menu.get_player_zone(player)
	if zone ~= nil then
		lzr_gui.show_menu_markers(player, zone)
	else
		lzr_gui.hide_menu_markers(player)
	end
end)

-- Force-load the player spawn position
local function forceload()
	local ok = minetest.forceload_block(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_PLAYER_SPAWN_OFFSET), true)
	if not ok then
		minetest.log("warning", "[lzr_menu] Could not forceload menu ship")
	else
		minetest.log("action", "[lzr_menu] Menu ship successfully forceloaded")
	end
end
forceload()

minetest.register_entity("lzr_menu:arrow", {
	initial_properties = {
		visual = "mesh",
		mesh = "lzr_menu_arrow.gltf",
		visual_size = { x=5, y=5, z=5 },
		textures = {
			"lzr_menu_arrow.png",
		},
		use_texture_alpha = true,
		static_save = false,
		physical = false,
		collide_with_objects = false,
		selectionbox = {
			-0.25, -0.25, -0.25, 0.25, 0.5, 0.25,
		},
		pointable = false,
		automatic_rotate = -0.5,
	},
	on_activate = function(self)
		minetest.log("error", "set_anim")
		self.object:set_animation({x=0,y=48}, 0.5, 0, true)
	end,
})

minetest.register_chatcommand("ship", {
	privs = {},
	params = "",
	description = S("Go to the ship"),
	func = function(name, param)
		if lzr_gamestate.is_loading() then
			return false, S("Can’t go to the ship while loading!")
		end
		local state = lzr_gamestate.get_state()
		if state == lzr_gamestate.MENU then
			return false, S("You’re already on the ship!")
		else
			lzr_levels.go_to_menu()
			return true
		end
	end,
})

minetest.register_alias("lzr_menu:editorbook", "lzr_menu:editor_starter")
minetest.register_alias("lzr_menu:startbook", "lzr_menu:level_starter")
minetest.register_alias("lzr_menu:startbook_custom", "lzr_menu:custom_level_starter")
