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

local F = minetest.formspec_escape

local select_item = dofile(minetest.get_modpath("lzr_editor").."/select_item.lua")

-- Text color the special autosave level will be highlighted
-- in the save/load dialog
local AUTOSAVE_HIGHLIGHT = "#FF8060"

local TXT_COLOR_ERROR = "#ff8000"
local TXT_COLOR_WARNING = "#ffff00"

-- List of items to give to player on editor entry.
-- This must be compatible with inventory:set_list().
local INITIAL_EDITOR_ITEMS = {
	-- tools
	"lzr_tools:ultra_pickaxe",
	"lzr_tools:ultra_bucket",
	"lzr_hook:hook",
	"lzr_laser:block_state_toggler",
	"lzr_laser:trigger_tool",
	"lzr_laser:color_changer",
	"lzr_tools:variant_changer",
	"lzr_laser:screw_changer",
	-- important nodes
	"lzr_laser:emitter_red_fixed_on",
	"lzr_laser:detector_fixed",
	"lzr_teleporter:teleporter_off",
	"lzr_treasure:chest_wood_locked",
	"lzr_treasure:chest_dark_locked",
	"lzr_laser:mirror_fixed",
	"lzr_laser:double_mirror_00_fixed",
	"lzr_laser:transmissive_mirror_00_fixed",
}

-- If the player is this far away out of the level bounds,
-- teleport back to level on level resize, level move
-- or level load.
local OUT_OF_BOUNDS_TOLERANCE = 1

lzr_editor = {}
-- Remember if the WorldEdit usage warning has already been shown in this session
-- (a warning is shown when a WorldEdit command is used because some commands
-- might break the triggers)
lzr_editor.worldedit_warning = false

-- The level state holds the metadata for
-- the level that is being currently edited.
-- INITIAL_LEVEL_STATE is the default state
-- and the state when the editor starts.
-- NOTE: level size and position are stored by
-- the lzr_world mod.
local INITIAL_LEVEL_STATE = {
	name = "",
	file_name = "",
	size = lzr_globals.DEFAULT_LEVEL_SIZE,
	wall = lzr_globals.DEFAULT_WALL_NODE,
	ceiling = lzr_globals.DEFAULT_CEILING_NODE,
	floor = lzr_globals.DEFAULT_FLOOR_NODE,
	ambient = lzr_ambience.DEFAULT_AMBIENCE,
	sky = lzr_globals.DEFAULT_SKY,
	npc_texts = lzr_globals.DEFAULT_NPC_TEXTS,
	weather = lzr_globals.DEFAULT_WEATHER,
	backdrop = lzr_globals.DEFAULT_BACKDROP,
	backdrop_pos = lzr_globals.DEFAULT_BACKDROP_POS,
	triggers = "",
	flags = "",
}
local level_state = table.copy(INITIAL_LEVEL_STATE)

local temp_settings_state = {}

local temp_flags_state = {}

-- Used to store the current contents for the
-- textlist contents of the custom level file list.
-- (this assumes singleplayer)
local level_file_textlist_state = {}

-- Error and warning texts for the level checker
local error_warning_texts = {
	no_teleporter = S("No start block"),
	too_many_teleporters = S("More than one start block"),
	barriers = S("Barrier or rain membrane in level area"),
	gold_block = S("Bare gold block in level area"),
	ignited_bomb = S("Ignited bomb in level area"),
	burning_barricade = S("Burning barricade in level area"),
	plant_on_ground = S("Rooted plant below non-water block in level area"),
	too_many_parrot_spawners = S("More than one parrot spawner"),
	too_many_hidden_parrot_spawners = S("More than one hidden parrot spawner"),
	--~ param2 is an internal value used by blocks to store some state
	bad_hidden_parrot_spawner = S("Bad param2 value for hidden parrot spawner"),
	--~ Warning shown when a parrot spawner was not placed on a solid cube-shaped block
	parrot_spawner_on_bad_block = S("Parrot spawner not on flat solid surface"),
	--~ Warning shown when a level contains a speech for a parrot but doesn’t have a parrot spawner
	parrot_speech_without_spawner = S("Parrot speech given, but no parrot spawner in level"),
	trigger_out_of_bounds = S("Trigger is out of bounds"),
	trigger_moved = S("Trigger ID does not match location"),
	laser_incompatible = S("Laser-incompatible node found"),
	no_treasures = S("No treasures to collect"),
	--~ Error when level starts with exposed treasure
	pre_collected = S("Exposed treasure in chest"),
	--~ Warning shown when a level has an incomplete door
	half_door = S("Incomplete door"),
	--~ Warning shown when a level has an invalid door. param2 is an internal value used by blocks to store some state
	incompatible_door_segments_param2 = S("Mismatching param2 value for door segments"),
	--~ Warning shown when a level has an invalid door
	incompatible_door_segments_type = S("Mismatching type for door segments"),
	--~ Error shown when a level has multiple incompatible controls-related advanced settings (level flags) active at the same time
	too_many_show_controls_flags = S("More than one ‘show controls’ level flag"),
	--~ Warning shown when the level settings contain a level flag (i.e. an advanced setting) that the game doesn’t know
	unknown_flag = S("Unknown level flag"),
}
-- Add error messages from lzr_triggers as well
for id, text in pairs(lzr_triggers.CHECK_ERROR_TEXTS) do
	error_warning_texts["lzr_triggers_"..id] = text
end

-- Takes a list of positions and returns them as a string
local positions_to_string = function(positions)
	local posstrings = {}
	for p=1, #positions do
		table.insert(posstrings, minetest.pos_to_string(positions[p]))
	end
	--~ list separator
	return table.concat(posstrings, S(", "))
end

-- Creates a nice-looking string listing all level warnings and errors.
-- Then returns it.
-- If everything is OK, returns the empty string.
local list_warnings_and_errors = function()
	local result_str = ""
	local check1_ok, errors = lzr_editor.check_level_errors()
	if not check1_ok then
		-- Level error found.
		local error_texts = {}
		for e=1, #errors do
			local error_type = errors[e][1]
			local posses = errors[e][2]
			local text = error_warning_texts[error_type] or error_type
			if posses == nil or #posses == 0 then
				table.insert(error_texts, S("• Error: @1", text))
			else
				local posses_str = positions_to_string(posses)
				--~ @1 is an error text, @2 is a position or list of positions
				table.insert(error_texts, PS("• Error: @1 (position: @2)", "• Error: @1 (positions: @2)", #posses, text, posses_str))
			end
		end
		local errors_str = table.concat(error_texts, "\n")
		result_str = result_str .. minetest.colorize(TXT_COLOR_ERROR, errors_str)
	end
	local check2_ok, warnings = lzr_editor.check_level_warnings()
	if not check2_ok then
		local warning_texts = {}
		for w=1, #warnings do
			local warning_type = warnings[w][1]
			local posses = warnings[w][2]
			local text = error_warning_texts[warning_type] or warning_type
			if posses == nil or #posses == 0 then
				table.insert(warning_texts, S("• Warning: @1", text))
			else
				local posses_str = positions_to_string(posses)
				--~ @1 is a warning text, @2 is a position or list of positions
				table.insert(warning_texts, PS("• Warning: @1 (position: @2)", "• Warning: @1 (positions: @2)", #posses, text, posses_str))
			end
		end
		local warnings_str = table.concat(warning_texts, "\n")
		if not check1_ok then
			result_str = result_str .. "\n"
		end
		result_str = result_str .. minetest.colorize(TXT_COLOR_WARNING, warnings_str)
	end
	return result_str
end

-- Move player to the level if they're too far away
local function move_player_to_level_if_neccessary(player)
	local lsoffset = vector.new(OUT_OF_BOUNDS_TOLERANCE, OUT_OF_BOUNDS_TOLERANCE, OUT_OF_BOUNDS_TOLERANCE)
	local lsminpos = vector.subtract(lzr_world.get_level_pos(), lsoffset)
	local lsmaxpos = vector.add(vector.add(lzr_world.get_level_pos(), lzr_world.get_level_size()), lsoffset)
	if not vector.in_area(player:get_pos(), lsminpos, lsmaxpos) then
		player:set_pos(lzr_world.get_level_pos())
	end
end

-- Give useful starter items to player
local function give_initial_items(player)
	local inv = player:get_inventory()
	-- Set all items at once
	inv:set_list("main", INITIAL_EDITOR_ITEMS)

	-- Check if we actually have those items because we're paranoid
	for i=1, #INITIAL_EDITOR_ITEMS do
		local item = inv:get_stack("main", i)
		if item:get_name() ~= INITIAL_EDITOR_ITEMS[i] then
			minetest.log("error", "[lzr_editor] Wrong or missing initial item at position "..i..": expected "..INITIAL_EDITOR_ITEMS[i].." but got "..item:get_name())
			break
		end
	end
end

-- Returns true if pos is within the current bounds
-- of the actively edited level.
function lzr_editor.is_in_level_bounds(pos)
	local offset = table.copy(lzr_world.get_level_size())
	offset = vector.offset(offset, -1, -1, -1)
	return vector.in_area(pos, lzr_world.get_level_pos(), vector.add(lzr_world.get_level_pos(), offset))
end

-- Enter editor state (if not already in it).
-- Returns true if state changed, false if already in editor.
function lzr_editor.enter_editor(player)
	local state = lzr_gamestate.get_state()
	if state == lzr_gamestate.EDITOR then
		return false
	else
		level_state = table.copy(INITIAL_LEVEL_STATE)

		local pos = lzr_world.get_level_pos()
		local size = lzr_world.get_level_size()
		lzr_levels.reset_level_area(false, pos, size)

		lzr_world.set_level_pos(lzr_globals.DEFAULT_LEVEL_POS)
		lzr_world.set_level_size(lzr_globals.DEFAULT_LEVEL_SIZE)
		pos = lzr_world.get_level_pos()
		size = lzr_world.get_level_size()
		lzr_triggers.reset_triggers()

		lzr_gamestate.set_state(lzr_gamestate.EDITOR)

		lzr_ambience.set_ambience(lzr_ambience.DEFAULT_AMBIENCE)
		lzr_sky.set_sky(lzr_globals.DEFAULT_SKY)
		lzr_weather.set_weather(lzr_globals.DEFAULT_WEATHER)


		lzr_gui.set_loading_gui(player)
		lzr_player.set_editor_inventory(player, true)
		lzr_player.set_loading_inventory_formspec(player)
		lzr_gamestate.set_loading(true)

		-- Stuff to do after the editor was loaded
		local done = function()

			lzr_player.set_editor_inventory(player)

			give_initial_items(player)

			lzr_privs.grant_edit_privs(player)

			lzr_gui.set_editor_gui(player)

			lzr_gui.show_level_bounds(player, pos, size)

			lzr_gamestate.set_loading(false)

			minetest.log("action", "[lzr_editor] Entered and initialized level editor")
		end

		lzr_levels.build_room({pos=pos, size=size, spawn_pos=pos, yaw=0, clear=true, callback_done=done})
		minetest.log("action", "[lzr_editor] Entering level editor ...")

		return true
	end
end

local check_for_slash = function(str)
	if string.match(str, "[/\\]") then
		return true
	else
		return false
	end
end

local save_level = function(level_name, is_autosave)
	if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then
		return false
	end
	if check_for_slash(level_name) then
		return false
	end

	-- Show level errors and warnings when saving manually
	if not is_autosave then
		local result_str = list_warnings_and_errors()
		if result_str ~= "" then
			minetest.chat_send_player("singleplayer", S("The following problems were found in this level:") .. "\n" .. result_str)
		end
	end

	minetest.mkdir(minetest.get_worldpath().."/levels")

	local filename = minetest.get_worldpath().."/levels/"..level_name..".mts"
	local size = vector.subtract(lzr_world.get_level_size(), vector.new(1, 1, 1))

	--[[ <<<The file is saved here >>> ]]
	local ok = minetest.create_schematic(lzr_world.get_level_pos(), vector.add(lzr_world.get_level_pos(), size), {}, filename, {})
	--[[ ^ Note: This saves the level exactly as-is, including the current state with
	all active lasers and active laser blocks. This is intended. The game will
	automatically clear lasers, reset laser blocks and recalculate the lasers on each level
	start anyway, so it shouldn't matter in which 'state' the level is stored.
	Saving the level with the active lasers pre-calculated tho has the slight benefit that
	the level instantly loads with the correct lasers. ]]

	-- Save level data into CSV
	local csv_filename = minetest.get_worldpath().."/levels/"..level_name..".csv"
	local npc_texts_csv = ""
	if level_state.npc_texts and level_state.npc_texts.goldie then
		npc_texts_csv = level_state.npc_texts.goldie or ""
		-- Replace Lua newlines with CR LF newlines.
		-- Needed because the field in the CSV file uses CR LF newlines.
		npc_texts_csv = string.gsub(npc_texts_csv, "\n", "\13\10")
	end
	local csv_contents = rcsv.write_csv({{
		level_name..".mts",
		level_state.name,
		-- level boundaries syntax: wall|<unused>|floor|ceiling
		-- (the <unused> used to be a node, but it has been removed from the game)
		level_state.wall .. "||" .. level_state.floor .. "|" .. level_state.ceiling,
		level_state.ambient,
		level_state.sky,
		npc_texts_csv,
		level_state.weather,
		level_state.backdrop,
		minetest.pos_to_string(level_state.backdrop_pos),
		lzr_levels.serialize_triggers(),
		level_state.flags,
	}})
	local ok_csv = minetest.safe_file_write(csv_filename, csv_contents)

	if ok and ok_csv then
		minetest.log("action", "[lzr_editor] Level written to "..filename.." and "..csv_filename)
		if not is_autosave then
			level_state.file_name = level_name
		end
		return true, filename, csv_filename
	elseif ok then
		minetest.log("error", "[lzr_editor] Level written to "..filename..", but failed to write CSV!")
		return false, filename, csv_filename
	else
		minetest.log("error", "[lzr_editor] Failed to write level to "..filename)
		return false, filename, csv_filename
	end
end

local autosave_level = function()
	save_level(lzr_globals.AUTOSAVE_NAME, true)
end

-- Exit editor state (if not already outside of editor).
-- Returns true if state changed, false if could not exit
local function exit_editor(player)
	if lzr_gamestate.is_loading() then
		return false
	end
	level_state.file_name = ""
	local state = lzr_gamestate.get_state()
	if state ~= lzr_gamestate.EDITOR then
		return false
	else
		lzr_levels.go_to_menu()
		local pos = lzr_world.get_level_pos()
		local size = lzr_world.get_level_size()
		lzr_levels.reset_level_area(false, pos, size)
		return true
	end
end

minetest.register_chatcommand("editor_save", {
	description = S("Save current level"),
	params = S("<level name>"),
	func = function(name, param)
		if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then
			return false, S("Not in editor mode!")
		elseif lzr_gamestate.is_loading() then
			return false, S("Can’t do this while loading!")
		end
		if param == "" then
			return false, S("No level name provided.")
		end
		local level_name = param
		if check_for_slash(level_name) then
			return false, S("Level name must not contain slash or backslash!")
		end
		local ok, filename, filename2 = save_level(level_name)
		if ok and filename and filename2 then
			--~ @1 and @2 are file locations
			return true, S("Level saved to @1 and @2.", filename, filename2)
		elseif of and filename then
			--~ @1 and @2 are file locations
			return true, S("Level saved to @1, but could not write metadata to @2.", filename, filename2)
		else
			return false, S("Error writing level file!")
		end
	end,
})

local load_level = function(level_name, player)
	if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then
		return false
	end

	if check_for_slash(level_name) then
		return false
	end
	local filename = level_name..".mts"
	local ok = lzr_util.file_exists(minetest.get_worldpath().."/levels", filename)
	if not ok then
		return false
	end
	local schem = minetest.read_schematic(minetest.get_worldpath().."/levels/"..filename, {write_yslice_prob="none"})

	-- Remember old level pos and size for resetting area
	local old_pos = lzr_world.get_level_pos()
	local old_size = lzr_world.get_level_size()

	if schem then
		local csv_ok = false
		local csv_file = io.open(minetest.get_worldpath().."/levels/"..level_name..".csv", "rb")
		local triggers_str = ""
		local flags_str = ""
		if csv_file then
			local csv_string = csv_file:read("*a")
			csv_file:close()
			local csv_parsed, csv_error = rcsv.parse_csv(csv_string)
			if csv_parsed and #csv_parsed >= 1 then
				csv_ok = true
				level_state.name = csv_parsed[1][2]
				local bounds = csv_parsed[1][3]
				local exploded_bounds = string.split(bounds, "|", true)
				if exploded_bounds then
					level_state.wall = exploded_bounds[1]
					local legacy_window = exploded_bounds[2]
					level_state.floor = exploded_bounds[3]
					-- Force ceiling to be barrier if legacy window
					-- node is used so that the level still gets
					-- sunlight
					if legacy_window == "lzr_decor:woodframed_glass" then
						level_state.ceiling = "lzr_core:barrier"
						--~ The "window boundary" refers to a special block used for the boundaries of the level to create windows in the walls. It has been removed in later versions of the game.
						minetest.chat_send_player(player:get_player_name(), S("Note: This level uses a legacy window boundary, which is no longer supported."))
					else
						level_state.ceiling = exploded_bounds[4]
					end
				end
				level_state.ambient = csv_parsed[1][4]
				level_state.sky = csv_parsed[1][5] or lzr_globals.DEFAULT_SKY
				level_state.npc_texts = csv_parsed[1][6]
				if level_state.npc_texts then
					level_state.npc_texts = { goldie = level_state.npc_texts }
				else
					level_state.npc_texts = lzr_globals.DEFAULT_NPC_TEXTS
				end
				level_state.weather = csv_parsed[1][7] or lzr_globals.DEFAULT_WEATHER
				level_state.backdrop = csv_parsed[1][8] or lzr_globals.DEFAULT_BACKDROP
				local parsed_backdrop_pos = csv_parsed[1][9]
				local backdrop_pos
				if parsed_backdrop_pos then
					backdrop_pos = minetest.string_to_pos(parsed_backdrop_pos)
				end
				if backdrop_pos then
					level_state.backdrop_pos = backdrop_pos
				else
					level_state.backdrop_pos = lzr_globals.DEFAULT_BACKDROP_POS
				end
				triggers_str = csv_parsed[1][10] or ""
				flags_str = csv_parsed[1][11] or ""
				level_state.triggers = triggers_str or ""
				level_state.flags = flags_str or ""
			else
				csv_ok = false
				minetest.chat_send_player(player:get_player_name(), minetest.colorize(TXT_COLOR_ERROR, S("The CSV file for level ‘@1’ couldn't be parsed. Error message: @2", level_name, csv_error)))
				minetest.log("action", "[lzr_editor] Error parsing CSV file for "..level_name..": "..csv_error)
			end
		else
			csv_ok = false
			minetest.chat_send_player(player:get_player_name(), minetest.colorize(TXT_COLOR_WARNING, S("Level ‘@1’ doesn’t have a CSV file.", level_name)))
			minetest.log("action", "[lzr_editor] No CSV file found for "..level_name)
		end
		if not csv_ok then
			level_state.name = ""
			level_state.wall = lzr_globals.DEFAULT_WALL_NODE
			level_state.ceiling = lzr_globals.DEFAULT_CEILING_NODE
			level_state.floor = lzr_globals.DEFAULT_FLOOR_NODE
			level_state.ambient = lzr_globals.DEFAULT_AMBIENCE
			level_state.sky = lzr_globals.DEFAULT_SKY
			level_state.npc_texts = lzr_globals.DEFAULT_NPC_TEXTS
			level_state.weather = lzr_globals.DEFAULT_WEATHER
			level_state.backdrop = lzr_globals.DEFAULT_BACKDROP
			level_state.backdrop_pos = lzr_globals.DEFAULT_BACKDROP_POS
			level_state.triggers = ""
			level_state.flags = ""
			minetest.chat_send_player(player:get_player_name(), minetest.colorize(TXT_COLOR_WARNING, S("The level was initialized with fallback level settings.")))
		end

		lzr_world.set_level_size(table.copy(schem.size))
		level_state.size = lzr_world.get_level_size()
		level_state.name = level_state.name or ""
		level_state.wall = level_state.wall or lzr_globals.DEFAULT_WALL_NODE
		level_state.ceiling = level_state.ceiling or lzr_globals.DEFAULT_CEILING_NODE
		level_state.floor = level_state.floor or lzr_globals.DEFAULT_FLOOR_NODE
		level_state.ambient = level_state.ambient or lzr_ambience.DEFAULT_AMBIENCE
		level_state.sky = level_state.sky or lzr_globals.DEFAULT_SKY
		level_state.npc_texts = level_state.npc_texts or lzr_globals.DEFAULT_NPC_TEXTS
		level_state.weather = level_state.weather or lzr_globals.DEFAULT_WEATHER
		level_state.backdrop = level_state.backdrop or lzr_globals.DEFAULT_BACKDROP
		level_state.backdrop_pos = level_state.backdrop_pos or lzr_globals.DEFAULT_BACKDROP_POS
		level_state.triggers = level_state.triggers or ""
		level_state.flags = level_state.flags or ""

		if level_state.backdrop == "ocean" then
			lzr_world.set_level_pos(table.copy(lzr_globals.BACKDROP_POS_OCEAN))
		elseif level_state.backdrop == "islands" then
			lzr_world.set_level_pos(table.copy(level_state.backdrop_pos))
		elseif level_state.backdrop == "underground" then
			lzr_world.set_level_pos(table.copy(lzr_globals.BACKDROP_POS_UNDERGROUND))
		elseif level_state.backdrop == "sky" then
			lzr_world.set_level_pos(table.copy(lzr_globals.BACKDROP_POS_SKY))
		end

		local bounding_nodes = {
			node_wall = level_state.wall,
			node_ceiling = level_state.ceiling,
			node_floor = level_state.floor,
		}

		lzr_gamestate.set_loading(true)
		lzr_gui.set_loading_gui(player)
		lzr_player.set_loading_inventory_formspec(player)
		lzr_player.set_editor_inventory(player, true)

		minetest.log("action", "[lzr_editor] Resetting level area before loading new level ...")
		lzr_levels.reset_level_area(false, old_pos, old_size)

		local done = function()
			lzr_gui.show_level_bounds(player, lzr_world.get_level_pos(), schem.size)

			if lzr_ambience.ambience_exists(level_state.ambient) then
				lzr_ambience.set_ambience(level_state.ambient)
			else
				level_state.ambient = "none"
				lzr_ambience.set_ambience("none")
			end
			if lzr_sky.sky_exists(level_state.sky) then
				lzr_sky.set_sky(level_state.sky)
			else
				lzr_sky.set_sky(lzr_globals.DEFAULT_SKY)
			end
			if lzr_weather.weather_exists(level_state.weather) then
				lzr_weather.set_weather(level_state.weather)
			else
				lzr_weather.set_weather(lzr_globals.DEFAULT_WEATHER)
			end

			lzr_player.set_editor_inventory(player)
			lzr_gui.set_editor_gui(player)

			if level_state.flags and level_state.flags ~= "" then
				-- Print warning when loading level with an unknown level flag
				local flags_table = string.split(level_state.flags, "|")
				if flags_table then
					local available_flags = {}
					for f=1, #lzr_levels.flags do
						available_flags[lzr_levels.flags[f]] = true
					end
					for f=1, #flags_table do
						if not available_flags[flags_table[f]] then
							minetest.chat_send_player(player:get_player_name(),
								minetest.colorize(TXT_COLOR_WARNING,
								--~ "level flag" refers to an advanced on/off setting for the level
								S("WARNING: This level contains an unknown level flag: @1", flags_table[f])))
						end
					end
				end
			end

			lzr_gamestate.set_loading(false)
			minetest.log("action", "[lzr_editor] Loaded level "..filename)
			minetest.chat_send_player(player:get_player_name(), S("Level loaded: @1", level_state.file_name))
		end
		lzr_levels.prepare_and_build_custom_level(lzr_world.get_level_pos(), schem, "start", nil, bounding_nodes, triggers_str, flags_str, done)

		level_state.file_name = level_name
		minetest.log("action", "[lzr_editor] Started loading level from "..filename.." ...")
		return true
	else
		minetest.log("error", "[lzr_editor] Failed to read level from "..filename)
		return false
	end
end


minetest.register_chatcommand("editor_load", {
	description = S("Load level"),
	params = S("<level name>"),
	func = function(name, param)
		local player = minetest.get_player_by_name(name)
		if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then
			return false, S("Not in editor mode!")
		elseif lzr_gamestate.is_loading() then
			return false, S("The editor is already loading a level!")
		end
		if param == "" then
			return false, S("No level name provided.")
		end
		local level_name = param
		if check_for_slash(level_name) then
			return false, S("Level name must not contain slash or backslash!")
		end
		local ok = lzr_util.file_exists(minetest.get_worldpath().."/levels", level_name..".mts")
		if not ok then
			return false, S("Level file does not exist!")
		end

		local ok = load_level(level_name, player)
		if ok then
			return true
		else
			return false, S("Error reading level file!")
		end
	end,
})

minetest.register_chatcommand("editor", {
	description = S("Start the level editor"),
	params = "",
	func = function(name, param)
		local player = minetest.get_player_by_name(name)
		if lzr_gamestate.get_state() == lzr_gamestate.EDITOR then
			return false, S("Already in level editor!")
		elseif lzr_gamestate.is_loading() then
			return false, S("Can’t do this while loading!")
		end
		local ok = lzr_editor.enter_editor(player)
		if ok then
			return true
		else
			minetest.log("error", "[lzr_editor] Can't enter editor for unknown reason")
			return false
		end
	end,
})

minetest.register_chatcommand("reset_triggers", {
	description = S("Remove all triggers and reset them to their initial state"),
	params = "",
	func = function(name, param)
		local state = lzr_gamestate.get_state()
		if state ~= lzr_gamestate.EDITOR then
			return false, S("Not in editor mode!")
		elseif lzr_gamestate.is_loading() then
			return false, S("Can’t do this while loading!")
		end
		lzr_levels.init_triggers()
		return true, S("Triggers have been reset.")
	end
})

-- Unlimited node placement in editor mode
minetest.register_on_placenode(function(pos, newnode, placer, oldnode, itemstack)
	if placer and placer:is_player() then
		return lzr_gamestate.get_state() == lzr_gamestate.EDITOR
	end
end)

-- Don't pick node up if the item is already in the inventory
local old_handle_node_drops = minetest.handle_node_drops
function minetest.handle_node_drops(pos, drops, digger)
	if not digger or not digger:is_player() or
		lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then
		return old_handle_node_drops(pos, drops, digger)
	end
	local inv = digger:get_inventory()
	if inv then
		for _, item in ipairs(drops) do
			if not inv:contains_item("main", item, true) then
				inv:add_item("main", item)
			end
		end
	end
end

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

		local inv = player:get_inventory()
		inv:set_list("main", {})
	end
end)

lzr_gamestate.register_on_exit_state(function(state)
	if state == lzr_gamestate.EDITOR then
		minetest.log("action", "[lzr_editor] Autosaving level on editor exit")
		autosave_level()
		local new_state = lzr_gamestate.get_state()
		if new_state ~= lzr_gamestate.DEV then
			local player = minetest.get_player_by_name("singleplayer")
			if player then
				lzr_privs.revoke_edit_privs(player)
			end
		end
	end
end)

local get_exit_button_type = function(player)
	if lzr_util.is_show_inventory_formspec_supported(player) then
		return "button"
	else
		return "button_exit"
	end
end

local show_settings_dialog = function(player, settings_state)
	if not settings_state then
		settings_state = level_state
	end

	-- Shorthand function to get the current dropdown index of
	-- a multiple-choice item (ambience, sky, weather) as well
	-- as a string to insert in the dropdown[] formspec element
	local get_current_thing = function(thing_type, thing_getter, description_getter)
		local thing_list = ""
		local current_thing = 1
		local things = thing_getter()
		for t=1, #things do
			local description
			if description_getter then
				description = description_getter(things[t])
			else
				description = things[t]
			end
			-- Construct string for dropdown[]
			thing_list = thing_list .. F(description)
			-- Current thing found!
			if things[t] == settings_state[thing_type] then
				current_thing = t
			end
			-- Append comma except at end
			if t < #things then
				thing_list = thing_list .. ","
			end
		end
		return thing_list, current_thing
	end

	local ambient_list, current_ambient = get_current_thing("ambient", lzr_ambience.get_ambiences, lzr_ambience.get_ambience_description)
	local sky_list, current_sky = get_current_thing("sky", lzr_sky.get_skies, lzr_sky.get_sky_description)
	local weather_list, current_weather = get_current_thing("weather", lzr_weather.get_weathers, lzr_weather.get_weather_description)
	local backdrop_list, current_backdrop = get_current_thing("backdrop", lzr_mapgen.get_backdrops, lzr_mapgen.get_backdrop_description)

	local level_size = lzr_world.get_level_size()

	local boundary_button = function(boundaryname, nodename, y)
		local form = ""

		if minetest.registered_nodes[nodename] then
			form = form .. "item_image_button[7.75,"..y..";0.6,0.6;"..F(nodename)..";level_"..boundaryname.."_select;]"
			return form
		else
			form = form .. "image_button[7.75,"..y..";0.6,0.6;"..F("[inventorycube{unknown_node.png{unknown_node.png{unknown_node.png")..";level_"..boundaryname.."_select;]"
			if type(nodename) == "string" then
				form = form .. "tooltip[level_"..boundaryname.."_select;"..F(nodename).."]"
			end
			return form
		end
	end

	local parrot_hint_button_state
	local parrot_hint_enabled = false
	local parrot_hint_tooltip
	do
		local flags = settings_state.flags
		local flags_list = string.split(flags, "|")
		for f=1, #flags_list do
			if flags_list[f] == "parrot_hint" then
				parrot_hint_enabled = true
				break
			end
		end
		if parrot_hint_enabled then
			parrot_hint_button_state = "on"
			parrot_hint_tooltip = S("Goldie the Parrot will ask the player if they want a hint before saying its speech")
		else
			parrot_hint_button_state = "off"
			parrot_hint_tooltip = S("Goldie the Parrot will talk to the player normally")
		end
		local winfo = minetest.get_player_window_information(player:get_player_name())
		if winfo and winfo.touch_controls then
			--~ Meaning: Tap (touchscreen) on this button to toggle a setting
			parrot_hint_tooltip = parrot_hint_tooltip .. "\n" .. S("(Tap to toggle)")
		else
			parrot_hint_tooltip = parrot_hint_tooltip .. "\n" .. S("(Click to toggle)")
		end
	end

	local exit_button = get_exit_button_type(player)

	-- TODO: Apply color theming to fields, textarea and dropdown (once Luanti allows it)
	local form = "formspec_version[7]"..
		"size[9,11.7]"..
		"style_type[image_button,item_image_button;bgimg=lzr_gui_mini_button.png;bgimg_middle=3]"..
		"style_type[image_button:hovered,item_image_button:hovered;bgimg=lzr_gui_mini_button_hover.png]"..
		"style_type[image_button:pressed,item_image_button:pressed;bgimg=lzr_gui_mini_button_pressed.png]"..
		"box[0,0;9,0.8;#00000080]"..
		"label[0.4,0.4;"..FS("Level settings").."]"..
		"field[0.5,1.3;8,0.6;level_name;"..FS("Name")..";"..F(settings_state.name).."]"..
		"label[0.5,2.3;"..FS("Size").."]"..
		"field[1.6,2.3;1,0.6;level_size_x;"..FS("X")..";"..F(tostring(settings_state.size.x)).."]"..
		"field[2.62,2.3;1,0.6;level_size_y;"..FS("Y")..";"..F(tostring(settings_state.size.y)).."]"..
		"field[3.63,2.3;1,0.6;level_size_z;"..FS("Z")..";"..F(tostring(settings_state.size.z)).."]"..
		"image_button[7.75,2.3;0.6,0.6;lzr_editor_flags_select.png;level_flags_select;]"..
		"field[0.5,3.3;7,0.6;level_wall;"..FS("Wall node")..";"..F(settings_state.wall).."]"..
		"field[0.5,4.3;7,0.6;level_floor;"..FS("Floor node")..";"..F(settings_state.floor).."]"..
		"field[0.5,5.3;7,0.6;level_ceiling;"..FS("Ceiling node")..";"..F(settings_state.ceiling).."]"..
		boundary_button("wall", settings_state.wall, 3.3)..
		boundary_button("floor", settings_state.floor, 4.3)..
		boundary_button("ceiling", settings_state.ceiling, 5.3)..

		-- HACK: Background for the textarea so it matches the gray default color with the other fields
		"box[0.5,6.3;6.3,0.6;#2D2D2DFF]"..
		"textarea[0.5,6.3;6.3,0.6;level_npc_goldie;"..FS("Goldie speech")..";"..F(settings_state.npc_texts.goldie).."]"..

		"image_button[7.05,6.3;0.6,0.6;lzr_editor_edit_npc_speech.png;open_npc_speech_editor;]"..
		"image_button[7.75,6.3;0.6,0.6;lzr_editor_flag_parrot_hint_"..parrot_hint_button_state..".png;level_flag_toggle_parrot_hint;]"..

		"label[0.5,7.1;"..FS("Music").."]"..
		"dropdown[0.5,7.3;8,0.6;level_ambient;"..ambient_list..";"..current_ambient..";true]"..

		"label[0.5,8.2;"..FS("Sky").."]"..
		"dropdown[0.5,8.4;3.5,0.6;level_sky;"..sky_list..";"..current_sky..";true]"..

		"label[5,8.2;"..FS("Weather").."]"..
		"dropdown[5,8.4;3.5,0.6;level_weather;"..weather_list..";"..current_weather..";true]"..

		"label[0.5,9.3;"..FS("Backdrop").."]"..
		"dropdown[0.5,9.5;3.5,0.6;level_backdrop;"..backdrop_list..";"..current_backdrop..";true]"

		-- backdrop_pos is only relevant for islands backdrop
		if settings_state.backdrop == "islands" then
			form = form .. "field[5,9.5;1,0.6;level_backdrop_pos_x;"..FS("X")..";"..F(tostring(settings_state.backdrop_pos.x)).."]"..
			"field[6.02,9.5;1,0.6;level_backdrop_pos_y;"..FS("Y")..";"..F(tostring(settings_state.backdrop_pos.y)).."]"..
			"field[7.04,9.5;1,0.6;level_backdrop_pos_z;"..FS("Z")..";"..F(tostring(settings_state.backdrop_pos.z)).."]"..
			"tooltip[level_backdrop_pos_x;"..FS("X coordinate of backdrop position").."]"..
			"tooltip[level_backdrop_pos_y;"..FS("Y coordinate of backdrop position").."]"..
			"tooltip[level_backdrop_pos_z;"..FS("Z coordinate of backdrop position").."]"
		end

		form = form .. "tooltip[level_name;"..FS("Level name as shown to the player").."]"..
		"tooltip[level_size_x;"..FS("Level size along the X axis").."]"..
		"tooltip[level_size_y;"..FS("Level size along the Y axis").."]"..
		"tooltip[level_size_z;"..FS("Level size along the Z axis").."]"..
		--~ "level flags" refers to advanced on/off settings for the level
		"tooltip[level_flags_select;"..FS("Advanced level settings (level flags)").."]"..
		"tooltip[level_wall;"..FS("Itemstring of node to be placed on the left, front, back and right level borders").."]"..
		"tooltip[level_floor;"..FS("Itemstring of node to be placed at the bottom of the level").."]"..
		"tooltip[level_ceiling;"..FS("Itemstring of node to be placed at the top of the level").."]"..
		"tooltip[level_npc_goldie;"..FS("Text to be shown when player interacts with Goldie the Parrot").."]"..
		--~ Tooltip for button that, when pressed, will increase the size of a text input field
		"tooltip[open_npc_speech_editor;"..FS("Expand text field").."]"..
		"tooltip[level_flag_toggle_parrot_hint;"..F(parrot_hint_tooltip).."]"..
		"tooltip[level_ambient;"..FS("Which audio ambience to play").."]"..
		"tooltip[level_sky;"..FS("How the sky looks like. Affects color, sun, moon, stars, clouds and the time of day").."]"..
		"tooltip[level_weather;"..FS("Visual weather effects (no audio)").."\n"..S("Note: The player may disable weather effects in the settings").."]"..
		"tooltip[level_backdrop;"..FS("The world that surrounds the level").."]"..

		exit_button.."[0.5,10.5;3.5,0.7;level_ok;"..FS("OK").."]"..
		exit_button.."[5,10.5;3.5,0.7;level_cancel;"..FS("Cancel").."]"..
		"field_close_on_enter[level_ok;true]"
	minetest.show_formspec(player:get_player_name(), "lzr_editor:level_settings", form)
end

local show_save_load_dialog = function(player, save, level_name)
	if not level_name then
		level_name = ""
	end
	local txt_caption
	local txt_action
	local action
	if save then
		txt_caption = S("Save level as …")
		txt_action = S("Save")
		action = "save"
	else
		txt_caption = S("Load level …")
		txt_action = S("Load")
		action = "load"
	end

	local levels_files = minetest.get_dir_list(minetest.get_worldpath().."/levels", false)
	local levels = {}
	for l=1, #levels_files do
		if string.lower(string.sub(levels_files[l], -4, -1)) == ".mts" then
			local name = string.sub(levels_files[l], 1, -5)
			table.insert(levels, name)
		end
	end
	table.sort(levels)
	level_file_textlist_state = levels
	local levels_str = ""
	-- For pre-selecting the matching entry in the list
	local selected_list_entry
	for l=1, #levels do
		local entry = levels[l]
		if level_name and level_name ~= "" and level_name == entry then
			selected_list_entry = l
		end
		if entry == lzr_globals.AUTOSAVE_NAME then
			entry = AUTOSAVE_HIGHLIGHT .. entry
		end
		levels_str = levels_str .. entry
		if l < #levels then
			levels_str = levels_str .. ","
		end
	end

	local level_list_elem = ""
	if #levels > 0 then
		level_list_elem = "label[0.5,1.5;"..FS("File list:").."]" ..
			"textlist[0.5,2;8,4;level_list;"..levels_str
		if selected_list_entry then
			level_list_elem = level_list_elem .. ";" .. selected_list_entry
		end
		level_list_elem = level_list_elem .. "]"
	end

	local exit_button = get_exit_button_type(player)

	local form = "formspec_version[7]"..
		"size[9,9]"..
		"box[0,0;9,0.8;#00000080]"..
		"label[0.4,0.4;"..F(txt_caption).."]"..
		level_list_elem..
		"field[0.5,6.6;7,0.6;file_name;"..FS("File name")..";"..minetest.formspec_escape(level_name).."]"..
		"label[7.7,6.9;.mts]"..
		"button_exit[0.5,7.7;3.5,0.7;"..action..";"..F(txt_action).."]"..
		exit_button.."[5,7.7;3.5,0.7;cancel;"..FS("Cancel").."]"..
		"field_close_on_enter["..action..";true]"

	local formname
	if save then
		formname = "lzr_editor:level_save"
	else
		formname = "lzr_editor:level_load"
	end
	minetest.show_formspec(player:get_player_name(), formname, form)
end

-- Remove all triggers that are out of level bounds
local clear_out_of_bounds_triggers = function()
	local cleared = 0
	local minpos, maxpos = lzr_world.get_level_bounds()
	for trigger_id, trigger in pairs(lzr_triggers.get_triggers()) do
		local trigger_pos = minetest.string_to_pos(trigger_id)
		if not vector.in_area(trigger_pos, minpos, maxpos) then
			lzr_triggers.remove_trigger(trigger_id)
			cleared = cleared + 1
		end
	end
	if cleared > 0 then
		minetest.log("info", "[lzr_editor] Removed "..cleared.." triggers that were out of bounds")
	end
end

-- Check the level area for errors
lzr_editor.check_level_errors = function()
	local minpos, maxpos = lzr_world.get_level_bounds()
	local errors = {}

	-- Test: Count start blocks (must have exactly 1)
	local teleporters = minetest.find_nodes_in_area(minpos, maxpos, "lzr_teleporter:teleporter_off")
	if #teleporters == 0 then
		table.insert(errors, {"no_teleporter"})
	elseif #teleporters > 1 then
		table.insert(errors, {"too_many_teleporters", teleporters})
	end

	-- Test: Barriers in level area (none allowed, they're only allowed for the level bounds)
	local barriers = minetest.find_nodes_in_area(minpos, maxpos, "group:barrier")
	if #barriers > 0 then
		table.insert(errors, {"barriers", barriers})
	end

	-- Test: Bare gold block (none allowed)
	local gold_blocks = minetest.find_nodes_in_area(minpos, maxpos, "lzr_treasure:gold_block")
	if #gold_blocks > 0 then
		table.insert(errors, {"gold_block", gold_blocks})
	end

	-- Test: Ignited bomb (none allowed)
	local bombs = minetest.find_nodes_in_area(minpos, maxpos, "group:bomb")
	local ignited_bombs = {}
	for b=1, #bombs do
		local node = minetest.get_node(bombs[b])
		if minetest.get_item_group(node.name, "bomb") == 2 then
			table.insert(ignited_bombs, bombs[b])
		end
	end
	if #ignited_bombs > 0 then
		table.insert(errors, {"ignited_bomb", ignited_bombs})
	end

	-- Test: Burning barricade (none allowed)
	local barricades = minetest.find_nodes_in_area(minpos, maxpos, "group:barricade")
	local burning_barricades = {}
	for b=1, #barricades do
		local node = minetest.get_node(barricades[b])
		if minetest.get_item_group(node.name, "barricade") == 2 then
			table.insert(burning_barricades, barricades[b])
		end
	end
	if #burning_barricades > 0 then
		table.insert(errors, {"burning_barricade", burning_barricades})
	end

	-- Test: No treasures to collect (must have at least 1)
	local no_treasures = true
	-- Test: Open chests with treasure (none allowed)
	local pre_collected = false
	local chests = minetest.find_nodes_in_area(minpos, maxpos, "group:chest")
	local pre_collected_list = {}
	for c=1, #chests do
		local node = minetest.get_node(chests[c])
		local g = minetest.get_item_group(node.name, "chest")
		-- Count locked and unlocked unopened chests
		if g == 1 or g == 2 then
			no_treasures = false
		-- Count open chests with treasure
		elseif g == 4 then
			pre_collected = true
			table.insert(pre_collected_list, chests[c])
		end
	end
	if pre_collected then
		table.insert(errors, {"pre_collected", pre_collected_list})
	elseif no_treasures then
		table.insert(errors, {"no_treasures"})
	end

	-- Test: Rooted plants below non-water node (rooted plant nodes are allowed,
	-- but only directly below water)
	local plants_on_ground = minetest.find_nodes_in_area(minpos, maxpos, "group:plant_on_ground")
	local bad_plants_on_ground = {}
	for p=1, #plants_on_ground do
		local plpos = plants_on_ground[p]
		local papos = vector.offset(plpos, 0, 1, 0)
		local panode = minetest.get_node(papos)
		if minetest.get_item_group(panode.name, "water") == 0 then
			table.insert(bad_plants_on_ground, plpos)
		end
	end
	if #bad_plants_on_ground > 0 then
		table.insert(errors, {"plant_on_ground", bad_plants_on_ground})
	end

	-- Test: More than one regular/hidden parrot spawner (max. 1 allowed)
	local parrot_spawners = minetest.find_nodes_in_area(minpos, maxpos, "lzr_parrot_npc:parrot_spawner")
	if #parrot_spawners > 1 then
		table.insert(errors, {"too_many_parrot_spawners", parrot_spawners})
	end

	local hidden_parrot_spawners = minetest.find_nodes_in_area(minpos, maxpos, "lzr_parrot_npc:hidden_parrot_spawner")
	if #hidden_parrot_spawners > 1 then
		table.insert(errors, {"too_many_hidden_parrot_spawners", hidden_parrot_spawners})
	end
	-- Test: Hidden parrot spawner with bad param2
	local bad_hidden_parrot_spawners = {}
	for h=1, #hidden_parrot_spawners do
		local node = minetest.get_node(hidden_parrot_spawners[h])
		local num = (node.param2 % 4) + 1
		local parrot_name = lzr_parrot_npc.get_hidden_parrot_name(num)
		if not parrot_name then
			table.insert(bad_hidden_parrot_spawners, hidden_parrot_spawners[h])
		end
	end
	if #bad_hidden_parrot_spawners > 0 then
		table.insert(errors, {"bad_hidden_parrot_spawner", bad_hidden_parrot_spawners})
	end

	-- Test: Trigger validity check from lzr_triggers
	local trigger_check_ok, trigger_errors, trigger_ids = lzr_triggers.check_triggers(false)
	if not trigger_check_ok then
		local errors_for_list = {}
		for e=1, #trigger_errors do
			local etype = trigger_errors[e]
			-- The trigger ID is a position in string form, this is why this works
			local pos = minetest.string_to_pos(trigger_ids[e])
			if errors_for_list[etype] == nil then
				errors_for_list[etype] = { pos }
			else
				table.insert(errors_for_list[etype], pos)
			end
		end
		for error_type, positions in pairs(errors_for_list) do
			table.insert(errors, {"lzr_triggers_"..error_type, positions})
		end
	end

	-- Test: Additional editor-specific trigger checks
	local oob_triggers = {}
	local moved_triggers = {}
	for trigger_id, trigger in pairs(lzr_triggers.get_triggers()) do
		local trigger_pos = minetest.string_to_pos(trigger_id)
		local oob_triggers = {}
		-- Out of level bounds
		if not vector.in_area(trigger_pos, minpos, maxpos) then
			table.insert(oob_triggers, trigger_pos)
		end
		-- Trigger is not at its expected location
		-- (in the editor, all triggers must have their ID
		-- match the location)
		if not vector.equals(trigger_pos, trigger.location) then
			table.insert(moved_triggers, trigger_pos)
		end
	end
	if #oob_triggers > 0 then
		table.insert(errors, {"trigger_out_of_bounds", oob_triggers})
	end
	if #moved_triggers > 0 then
		table.insert(errors, {"trigger_moved", moved_triggers})
	end

	-- Test: Errors in level flags
	local flags_table = string.split(level_state.flags, "|")
	local show_controls_flags = 0
	for f=1, #flags_table do
		local flagname = flags_table[f]
		if string.sub(flagname, 1, 14) == "show_controls_" then
			show_controls_flags = show_controls_flags + 1
			if show_controls_flags > 1 then
				table.insert(errors, {"too_many_show_controls_flags"})
				break
			end
		end
	end

	if #errors > 0 then
		return false, errors
	else
		return true
	end
end

-- Check the level area for non-serious problems
lzr_editor.check_level_warnings = function()
	local warnings = {}
	local minpos, maxpos = lzr_world.get_level_bounds()

	-- Test: Laser-incompatible nodes (should have none).
	-- Will look a bit ugly if a laser touches those.
	local incompatible = minetest.find_nodes_in_area(minpos, maxpos, "group:laser_incompatible")
	if #incompatible > 0 then
		table.insert(warnings, {"laser_incompatible", incompatible})
	end

	-- Test: Incompatible door segments
	local doors = minetest.find_nodes_in_area(minpos, maxpos, "group:door")
	if #doors > 0 then
		local half_door, inc_p2, inc_type = {}, {}, {}
		for d=1, #doors do
			local dpos = doors[d]
			local dnode = minetest.get_node(dpos)
			local ddir = minetest.facedir_to_dir(dnode.param2)
			-- Only test upright door segments. Door segments that lie flat
			-- don't trigger warnings.
			if ddir.y == 0 then
				local altpos
				local g = minetest.get_item_group(dnode.name, "door")
				if g == 1 then
					altpos = vector.offset(dpos, 0, 1, 0)
				elseif g == 2 then
					altpos = vector.offset(dpos, 0, -1, 0)
				else
					minetest.log("error", "[lzr_editor] Door node '"..dnode.name.."' has bad door group rating: "..g)
				end
				local altnode = minetest.get_node(altpos)
				local ga = minetest.get_item_group(altnode.name, "door")
				if ga == 0 then
					table.insert(half_door, dpos)
				elseif altnode.param2 ~= dnode.param2 then
					table.insert(inc_p2, dpos)
				else
					local g_exit = minetest.get_item_group(dnode.name, "door_exit")
					local ga_exit = minetest.get_item_group(altnode.name, "door_exit")
					local g_locked = minetest.get_item_group(dnode.name, "door_locked")
					local ga_locked = minetest.get_item_group(altnode.name, "door_locked")
					if g_exit ~= ga_exit or g_locked ~= ga_locked then
						table.insert(inc_type, dpos)
					end
				end
			end
		end
		if #half_door > 0 then
			table.insert(warnings, {"half_door", half_door})
		end
		if #inc_p2 > 0 then
			table.insert(warnings, {"incompatible_door_segments_param2", inc_p2})
		end
		if #inc_type > 0 then
			table.insert(warnings, {"incompatible_door_segments_type", inc_type})
		end
	end

	-- Test: Parrot spawners should be on solid flat surfaces
	-- so the parrot doesn't "float" or "falls"
	-- at the start of the level.
	local parrots = minetest.find_nodes_in_area(minpos, maxpos, {"lzr_parrot_npc:parrot_spawner", "lzr_parrot_npc:hidden_parrot_spawner"})
	local bad_spawners = {}
	for p=1, #parrots do
		local below = vector.offset(parrots[p], 0, -1, 0)
		local bnode = minetest.get_node(below)
		local bnodedef = minetest.registered_nodes[bnode.name]
		local fail = false
		-- Unknown node
		if not bnodedef then
			fail = true
		-- Non-solid node
		elseif not bnodedef.walkable then
			fail = true
		-- Not cube-shaped collisionbox
		elseif bnodedef.collision_box and bnodedef.collision_box.type ~= "regular" or bnodedef.node_box and bnodedef.node_box.type ~= "regular" then
			-- Allowed exceptions: Upside-down slabs/stairs and nodes defined as 'parrot-compatible'
			if not (minetest.get_item_group(bnode.name, "slab") == 2 or minetest.get_item_group(bnode.name, "parrot_compatible") == 1) then
				if minetest.get_item_group(bnode.name, "stair") ~= 0 then
					-- These param2 values are stairs rotated with their flat side up
					if (not (bnode.param2 == 6 or bnode.param2 == 8 or bnode.param2 == 15 or bnode.param2 == 17 or (bnode.param2 >= 20 and bnode.param2 <= 23))) then
						fail = true
					end
				else
					fail = true
				end
			end
		end

		if fail then
			table.insert(bad_spawners, parrots[p])
		end
	end
	if #bad_spawners > 0 then
		table.insert(warnings, {"parrot_spawner_on_bad_block", bad_spawners})
	end

	local normal_parrots = minetest.find_nodes_in_area(minpos, maxpos, {"lzr_parrot_npc:parrot_spawner"})
	-- Test: No (regular) parrot spawner in level, but parrot speech is non-empty
	if #normal_parrots == 0 then
		if level_state.npc_texts and level_state.npc_texts.goldie and level_state.npc_texts.goldie ~= "" then
			table.insert(warnings, {"parrot_speech_without_spawner"})
		end
	end

	-- Test for unknown level flags
	local flags_table = string.split(level_state.flags, "|")
	local known_flags = {}
	for f=1, #lzr_levels.flags do
		known_flags[lzr_levels.flags[f]] = true
	end
	for f=1, #flags_table do
		local flagname = flags_table[f]
		if not known_flags[flagname] then
			table.insert(warnings, {"unknown_flag"})
		end
	end

	if #warnings > 0 then
		return false, warnings
	else
		return true
	end
end

minetest.register_chatcommand("editor_check", {
	description = S("Check current level for problems"),
	params = "",
	func = function(name, param)
		if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then
			return false, S("Not in editor mode!")
		elseif lzr_gamestate.is_loading() then
			return false, S("Can’t do this while loading!")
		end
		local result_str = list_warnings_and_errors()
		if result_str == "" then
			return true, S("No problems found.")
		else
			-- Note: We return true because the *command* itself
			-- succeeded, it's the level that has errors.
			return true, S("The following problems were found:").."\n"..result_str
		end
	end,
})

local show_level_flags_dialog = function(pname, keep_temp_flags_state)
	if not keep_temp_flags_state then
		temp_flags_state = {}
	end
	local form = "formspec_version[7]"..
		"size[9,11.7]"..
		"box[0,0;9,0.8;#00000080]"..
		--~ "level flags" refers to on/off settings for the level
		"label[0.4,0.4;"..FS("Advanced level settings (level flags)").."]"
	local y = 1.5

	local enabled_flags = {}
	if not keep_temp_flags_state then
		local current_flags = temp_settings_state.flags
		local current_flags_table = string.split(current_flags, "|")
		for t=1, #current_flags_table do
			enabled_flags[current_flags_table[t]] = true
			temp_flags_state[current_flags_table[t]] = true
		end
	else
		enabled_flags = temp_flags_state
	end

	for f=1, #lzr_levels.flags do
		local flagname = lzr_levels.flags[f]
		local selected = enabled_flags[flagname]
		local label = lzr_levels.flags_names[flagname]
		local tooltip =
			lzr_levels.flags_descriptions[flagname] .. "\n"..
			--~ @1 is a technical identifier for a level flag (an on/off level setting). This text is used in the tooltip in the level editor for the level flags checkboxes
			minetest.colorize("#ffc9c9", S("[@1]", flagname))

		form = form .. "checkbox[0.5,"..y..";flag_"..flagname..";"..F(label)..";"..tostring(selected).."]"
		form = form .. "tooltip[flag_"..flagname..";"..F(tooltip).."]"

		y = y + 0.75
	end

	form = form .. "button[0.5,10.5;3.5,0.7;ok;"..FS("OK").."]"
	form = form .. "button[5,10.5;3.5,0.7;cancel;"..FS("Cancel").."]"

	minetest.show_formspec(pname, "lzr_editor:level_flags", form)
end

local show_npc_speech_dialog = function(pname)
	local goldie_speech = ""
	if temp_settings_state.npc_texts then
		goldie_speech = temp_settings_state.npc_texts.goldie or ""
	end
	local form = "formspec_version[7]"..
		"size[9,11.7]"..
		"box[0,0;9,0.8;#00000080]"..
		"label[0.4,0.4;"..FS("Goldie speech").."]"..
		-- HACK: Background for the textarea so it matches the gray default color with normal input fields
		"box[0.5,1.4;8,8;#2D2D2DFF]"..
		"textarea[0.5,1.4;8,8;level_npc_goldie;"..FS("Goldie speech")..";"..F(goldie_speech).."]"..
		"tooltip[level_npc_goldie;"..FS("Text to be shown when player interacts with Goldie the Parrot").."]"

	form = form .. "button[0.5,10.5;3.5,0.7;ok;"..FS("OK").."]"
	form = form .. "button[5,10.5;3.5,0.7;cancel;"..FS("Cancel").."]"

	minetest.show_formspec(pname, "lzr_editor:npc_speech", form)
end



minetest.register_on_player_receive_fields(function(player, formname, fields)
	if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then
		return
	elseif lzr_gamestate.is_loading() then
		if not fields.quit then
			minetest.chat_send_player(player:get_player_name(), S("Can’t do this while loading!"))
		end
		return
	end

	if formname ~= "lzr_editor:level_settings" and string.sub(formname, 1, 27) ~= "lzr_editor:select_item_page" and formname ~= "lzr_editor:level_flags" and formname ~= "lzr_editor:npc_speech" then
		temp_settings_state = {}
	end

	local get_current_thing = function(thing_type, selected_thing, thing_getter)
		local thing_list = ""
		local current_thing = 1
		local things = thing_getter()
		selected_thing = tonumber(selected_thing)
		if not selected_thing then
			return nil
		end
		for t=1, #things do
			if t == selected_thing then
				return things[t]
			end
		end
		return nil
	end

	local pname = player:get_player_name()

	-- Read settings fields into target table (without validation)
	local parse_settings_fields = function(target_table, fields)
		if fields.level_name then
			target_table.name = fields.level_name
		end

		if fields.level_flags then
			target_table.flags = fields.level_flags
		end

		if not target_table.size then
			target_table.size = {}
		end
		target_table.size.x = tonumber(fields.level_size_x or target_table.size.x)
		target_table.size.y = tonumber(fields.level_size_y or target_table.size.y)
		target_table.size.z = tonumber(fields.level_size_z or target_table.size.z)

		target_table.floor = fields.level_floor or target_table.floor
		target_table.ceiling = fields.level_ceiling or target_table.ceiling
		target_table.wall = fields.level_wall or target_table.wall
		if fields.level_npc_goldie then
			target_table.npc_texts = {}
			target_table.npc_texts.goldie = fields.level_npc_goldie
		end

		target_table.ambient = get_current_thing("ambience", fields.level_ambient, lzr_ambience.get_ambiences) or target_table.ambient
		target_table.sky = get_current_thing("sky", fields.level_sky, lzr_sky.get_skies) or target_table.sky
		target_table.weather = get_current_thing("weather", fields.level_weather, lzr_weather.get_weathers) or target_table.weather
		target_table.backdrop = get_current_thing("backdrop", fields.level_backdrop, lzr_mapgen.get_backdrops) or target_table.backdrop

		if not target_table.backdrop_pos then
			target_table.backdrop_pos = {}
		end
		target_table.backdrop_pos.x = tonumber(fields.level_backdrop_pos_x or target_table.backdrop_pos.x)
		target_table.backdrop_pos.y = tonumber(fields.level_backdrop_pos_y or target_table.backdrop_pos.y)
		target_table.backdrop_pos.z = tonumber(fields.level_backdrop_pos_z or target_table.backdrop_pos.z)

	end

	local fix_settings_fields = function(target_table)
		-- Enforce size bounds
		for _, axis in pairs({"x","y","z"}) do
			target_table.size[axis] = math.floor(tonumber(target_table.size[axis]) or lzr_globals.DEFAULT_LEVEL_SIZE[axis])
			if target_table.size[axis] > lzr_globals.MAX_LEVEL_SIZE[axis] then
				target_table.size[axis] = lzr_globals.MAX_LEVEL_SIZE[axis]
			end
			if target_table.size[axis] < lzr_globals.MIN_LEVEL_SIZE[axis] then
				target_table.size[axis] = lzr_globals.MIN_LEVEL_SIZE[axis]
			end
		end

		-- Enforce backdrop limits
		if target_table.backdrop == "islands" then
			local mg_edge_min, mg_edge_max = minetest.get_mapgen_edges()
			mg_edge_max = vector.subtract(mg_edge_max, lzr_globals.MAX_LEVEL_SIZE)
			local EDGE_BUFFER = 1000 -- make sure the level is far away from the zone boundaries
			-- enforce we're in the map
			for _, axis in pairs({"x","y","z"}) do
				-- Non-numeric values get converted to 0
				target_table.backdrop_pos[axis] = math.floor(tonumber(target_table.backdrop_pos[axis]) or 0)
				target_table.backdrop_pos[axis] = math.min(mg_edge_max[axis]-EDGE_BUFFER-lzr_globals.MAX_LEVEL_SIZE[axis], math.max(mg_edge_min[axis]+EDGE_BUFFER, target_table.backdrop_pos[axis]))
			end
			-- enforce that we're in the islands zone
			if target_table.backdrop_pos.z < lzr_globals.DEEP_OCEAN_Z + EDGE_BUFFER + lzr_globals.MAX_LEVEL_SIZE.z then
				target_table.backdrop_pos.z = lzr_globals.DEEP_OCEAN_Z + EDGE_BUFFER + lzr_globals.MAX_LEVEL_SIZE.z
			end
		end
	end

	local merge_settings = function(target_table, merge_table)
		for k,v in pairs(merge_table) do
			if type(v) == "table" then
				target_table[k] = table.copy(v)
			else
				target_table[k] = v
			end
		end
	end

	if formname == "lzr_editor:level_settings" then

		if not fields.level_cancel and not fields.quit and not fields.level_ok then
			-- select_item button pushed
			if fields.level_wall_select or fields.level_ceiling_select or fields.level_floor_select then
				parse_settings_fields(temp_settings_state, fields)
				local state = table.copy(level_state)
				merge_settings(state, temp_settings_state)
				fix_settings_fields(state)
				temp_settings_state = state

				local bname
				if fields.level_wall_select then
					bname = "wall"
				elseif fields.level_ceiling_select then
					bname = "ceiling"
				elseif fields.level_floor_select then
					bname = "floor"
				end
				select_item.show_dialog(pname, bname)
				return
			end
			-- flags_select button pushed
			if fields.level_flags_select then
				parse_settings_fields(temp_settings_state, fields)
				local state = table.copy(level_state)
				merge_settings(state, temp_settings_state)
				fix_settings_fields(state)
				temp_settings_state = state

				show_level_flags_dialog(pname)
				return
			end
			if fields.level_flag_toggle_parrot_hint then
				parse_settings_fields(temp_settings_state, fields)
				local state = table.copy(level_state)
				merge_settings(state, temp_settings_state)

				local flags = state.flags
				local flags_list = string.split(flags, "|")
				local parrot_hint_enabled = false
				local new_flags_list = {}
				for f=1, #flags_list do
					if flags_list[f] == "parrot_hint" then
						parrot_hint_enabled = true
					else
						table.insert(new_flags_list, flags_list[f])
					end
				end
				if not parrot_hint_enabled then
					table.insert(new_flags_list, "parrot_hint")
				end
				flags = table.concat(new_flags_list, "|")

				state.flags = flags

				fix_settings_fields(state)
				temp_settings_state = state

				show_settings_dialog(player, temp_settings_state)
				return
			end
			-- Larger Goldie speech edit button pressed
			if fields.open_npc_speech_editor then
				parse_settings_fields(temp_settings_state, fields)
				local state = table.copy(level_state)
				merge_settings(state, temp_settings_state)
				fix_settings_fields(state)
				temp_settings_state = state

				show_npc_speech_dialog(pname)
				return
			end

			-- Dropdown changed
			if (fields.level_sky or fields.level_backdrop or fields.level_ambient or fields.level_weather) then
				parse_settings_fields(temp_settings_state, fields)
				if not temp_settings_state.backdrop_pos.x then
					temp_settings_state.backdrop_pos.x = level_state.backdrop_pos.x or 0
					temp_settings_state.backdrop_pos.y = level_state.backdrop_pos.y or 0
					temp_settings_state.backdrop_pos.z = level_state.backdrop_pos.z or 0
				end
				local new_state = table.copy(level_state)
				merge_settings(new_state, temp_settings_state)
				temp_settings_state = new_state
				fix_settings_fields(temp_settings_state)
				show_settings_dialog(player, temp_settings_state)
				return
			end

		end

		if fields.level_cancel or (fields.quit and not fields.key_enter and not fields.level_ok) then
			temp_settings_state = {}
			if fields.level_cancel then
				lzr_util.show_inventory_formspec_if_supported(player)
			end
			return
		end

		if fields.level_ok or (fields.quit and fields.key_enter) then
			local old_level_pos = table.copy(lzr_world.get_level_pos())
			local old_level_size = table.copy(lzr_world.get_level_size())
			local old_floor = level_state.floor
			local old_ceiling = level_state.ceiling
			local old_wall = level_state.wall

			parse_settings_fields(temp_settings_state, fields)
			fix_settings_fields(temp_settings_state)
			level_state = table.copy(temp_settings_state)

			lzr_world.set_level_size(level_state.size)

			-- Some trigger nodes might have been removed if the level
			-- size was reduced, so we must clear some triggers
			clear_out_of_bounds_triggers()

			if level_state.backdrop == "ocean" then
				lzr_world.set_level_pos(table.copy(lzr_globals.BACKDROP_POS_OCEAN))
			elseif level_state.backdrop == "islands" then
				lzr_world.set_level_pos(table.copy(level_state.backdrop_pos))
			elseif level_state.backdrop == "underground" then
				lzr_world.set_level_pos(table.copy(lzr_globals.BACKDROP_POS_UNDERGROUND))
			elseif level_state.backdrop == "sky" then
				lzr_world.set_level_pos(table.copy(lzr_globals.BACKDROP_POS_SKY))
			end

			local rebuild_room, relocate_room = false, false
			local nodes = {}
			nodes.node_floor = level_state.floor
			nodes.node_ceiling = level_state.ceiling
			nodes.node_wall = level_state.wall
			if old_floor ~= nodes.node_floor or old_ceiling ~= nodes.node_ceiling or old_wall ~= nodes.node_wall then
				for k,v in pairs(nodes) do
					if not minetest.registered_nodes[v] then
						nodes[k] = lzr_globals.DEFAULT_WALL_NODE
						level_state[string.sub(k, 6)] = lzr_globals.DEFAULT_WALL_NODE
					end
				end
				rebuild_room = true
			elseif not vector.equals(lzr_world.get_level_size(), old_level_size) then
				rebuild_room = true
			end
			if not vector.equals(lzr_world.get_level_pos(), old_level_pos) then
				relocate_room = true
				rebuild_room = true
			end

			if relocate_room or rebuild_room then
				minetest.log("action", "[lzr_editor] Autosaving level on level rebuild")
				save_level(lzr_globals.AUTOSAVE_NAME, true)
			end

			local ca_nodes, ca_param2s, relocate_size
			if relocate_room then
				local new_size = lzr_world.get_level_size()
				relocate_size = table.copy(new_size)
				if relocate_size.x > old_level_size.x then relocate_size.x = old_level_size.x end
				if relocate_size.y > old_level_size.y then relocate_size.y = old_level_size.y end
				if relocate_size.z > old_level_size.z then relocate_size.z = old_level_size.z end
				-- Copy the room and reset the old level area
				-- Area will be pasted after the room resize
				ca_nodes, ca_param2s = lzr_util.copy_area(old_level_pos, relocate_size)
				lzr_levels.reset_level_area(false, old_level_pos, old_level_size)
			end

			local after_rebuild = function(emerge_success)
				if emerge_success == false then
					minetest.log("error", "[lzr_editor] Could not rebuild level, the emerging has failed")
					return
				end
				minetest.log("action", "[lzr_editor] after_rebuild() called")
				if relocate_room then
					-- Relocate all triggers
					lzr_triggers.relocate_triggers(old_level_pos, lzr_world.get_level_pos())

					-- Paste the room we copied before
					lzr_util.paste_area(lzr_world.get_level_pos(), relocate_size, ca_nodes, ca_param2s)

					-- Initialize trigger node meta
					local minpos, maxpos = lzr_world.get_level_bounds()
					local trigger_nodes = minetest.find_nodes_in_area(minpos, maxpos, {"group:sender", "group:receiver"})
					for t=1, #trigger_nodes do
						local tpos = trigger_nodes[t]
						local trigger_id = minetest.pos_to_string(tpos)
						local meta = minetest.get_meta(tpos)
						meta:set_string("trigger_id", trigger_id)
					end
					minetest.log("action", "[lzr_editor] Level area relocated")
				end

				-- Move player if too far out of new level area
				move_player_to_level_if_neccessary(player)

				if lzr_ambience.ambience_exists(level_state.ambient) then
					lzr_ambience.set_ambience(level_state.ambient)
				else
					lzr_ambience.set_ambience("none")
				end
				if lzr_sky.sky_exists(level_state.sky) then
					lzr_sky.set_sky(level_state.sky)
				else
					lzr_sky.set_sky(lzr_globals.DEFAULT_SKY)
				end
				if lzr_weather.weather_exists(level_state.weather) then
					lzr_weather.set_weather(level_state.weather)
				else
					lzr_weather.set_weather(lzr_globals.DEFAULT_WEATHER)
				end

				if rebuild_room then
					lzr_gui.set_editor_gui(player)
					lzr_player.set_editor_inventory(player)
					lzr_gamestate.set_loading(false)
				end

				lzr_gui.show_level_bounds(player, lzr_world.get_level_pos(), lzr_world.get_level_size())

				minetest.log("action", "[lzr_editor] Finished setting up new level area")
			end

			if rebuild_room then
				minetest.log("action", "[lzr_editor] Resizing level area and/or updating level boundaries ...")
				if relocate_room then
					lzr_levels.reset_level_area(true, lzr_world.get_level_pos(), lzr_world.get_level_size())
				end
				-- Resize room. This action is asynchronous

				lzr_gui.set_loading_gui(player)
				lzr_player.set_editor_inventory(player, true)
				lzr_player.set_loading_inventory_formspec(player)
				lzr_gamestate.set_loading(true)

				lzr_levels.resize_room(old_level_size, lzr_world.get_level_size(), nodes, after_rebuild)
				minetest.log("action", "[lzr_editor] Finished resizing level area and/or updating level boundaries")
			else
				-- If room does not need to be rebuilt, we can call after_rebuild with success
				-- immediately
				after_rebuild(true)
			end
		end
		if fields.level_ok or fields.level_cancel then
			lzr_util.show_inventory_formspec_if_supported(player)
		end
		return
	-- Level flags editor
	elseif formname == "lzr_editor:level_flags" then
		for f=1, #lzr_levels.flags do
			local flag = lzr_levels.flags[f]
			local new_state
			if fields["flag_"..flag] == "true" then
				temp_flags_state[flag] = true
				new_state = true
			elseif fields["flag_"..flag] == "false" then
				temp_flags_state[flag] = false
				new_state = false
			end
			-- Make sure only one show_controls_* flag can be enabled at the same time.
			if new_state == true and string.sub(flag, 1, 14) == "show_controls_" then
				local disabled_other_flag = true
				-- If enabling one show_controls_* flag, disable all the other ones
				for l=1, #lzr_levels.flags do
					local lflag = lzr_levels.flags[l]
					if flag ~= lflag and string.sub(lflag, 1, 14) == "show_controls_" then
						if temp_flags_state[lflag] == true then
							temp_flags_state[lflag] = false
							disabled_other_flag = true
						end
					end
				end
				-- Resend dialog if neccessary (due to updated checkboxes)
				if disabled_other_flag then
					show_level_flags_dialog(pname, true)
				end
			end
		end
		if fields.ok or (fields.quit and fields.key_enter) then
			local flags_string = ""
			for flag, state in pairs(temp_flags_state) do
				if state == true then
					if flags_string ~= "" then
						flags_string = flags_string .. "|"
					end
					flags_string = flags_string .. flag
				end
			end
			temp_settings_state.flags = flags_string

			local state = table.copy(level_state)
			merge_settings(state, temp_settings_state)
			show_settings_dialog(player, state)
		end
		if fields.cancel or (fields.quit and not fields.key_enter) then
			temp_flags_state = {}

			local state = table.copy(level_state)
			merge_settings(state, temp_settings_state)
			show_settings_dialog(player, state)
		end

	-- Edit NPC speech
	elseif formname == "lzr_editor:npc_speech" then
		if fields.ok or (fields.quit and fields.key_enter) then
			if fields.level_npc_goldie then
				if not temp_settings_state.npc_texts then
					temp_settings_state.npc_texts = {}
				end
				temp_settings_state.npc_texts.goldie = fields.level_npc_goldie
			end

			local state = table.copy(level_state)
			merge_settings(state, temp_settings_state)
			show_settings_dialog(player, state)
			return
		end
		if fields.cancel or (fields.quit and not fields.key_enter) then
			local state = table.copy(level_state)
			merge_settings(state, temp_settings_state)
			show_settings_dialog(player, state)
		end

	-- Select level boundary
	elseif string.sub(formname, 1, 27) == "lzr_editor:select_item_page" then
		local rest = string.sub(formname, 28, string.len(formname))
		local split = string.split(rest, "%%", true, 2)
		local page = tonumber(split[1])
		local boundaryname = split[2]

		-- Change page in select_item dialog
		if page ~= nil then
			if fields.previous and page > 1 then
				select_item.show_dialog_page(player:get_player_name(), boundaryname, page - 1)
				return
			elseif fields.next then
				select_item.show_dialog_page(pname, boundaryname, page + 1)
				return
			end
		end

		local item
		for field,_ in pairs(fields) do
			if string.sub(field, 1, 5) == "item_" then
				item = string.sub(field, 6, string.len(field))
				break
			end
		end
		if item then
			temp_settings_state[boundaryname] = item
			local state = table.copy(level_state)
			merge_settings(state, temp_settings_state)
			show_settings_dialog(player, state)
			return
		elseif fields.quit or fields.cancel then
			select_item.reset_player_info(pname)

			local state = table.copy(level_state)
			merge_settings(state, temp_settings_state)
			show_settings_dialog(player, state)
		end

	elseif formname == "lzr_editor:level_save" or formname == "lzr_editor:level_load" then
		if fields.level_list then
			local evnt = minetest.explode_textlist_event(fields.level_list)
			if evnt.type == "CHG" or evnt.type == "DBL" then
				local file = level_file_textlist_state[evnt.index]
				if file then
					if formname == "lzr_editor:level_save" then
						show_save_load_dialog(player, true, file)
					else
						show_save_load_dialog(player, false, file)
					end
					return
				end
			end
		elseif (fields.load or (formname == "lzr_editor:level_load" and fields.key_enter)) and fields.file_name then
			if fields.file_name == "" then
				minetest.chat_send_player(pname, S("No level name provided."))
				return
			end
			if check_for_slash(fields.file_name) then
				minetest.chat_send_player(pname, S("File name must not contain slash or backslash!"))
				return false
			end
			local exists = lzr_util.file_exists(minetest.get_worldpath().."/levels", fields.file_name..".mts")
			if not exists then
				minetest.chat_send_player(pname, S("Level file does not exist!"))
				return
			end
			local ok, filename = load_level(fields.file_name, player)
			if not ok then
				minetest.chat_send_player(pname, S("Error reading level file!"))
			end
		elseif (fields.save or (formname == "lzr_editor:level_save" and fields.key_enter)) and fields.file_name then
			if fields.file_name == "" then
				minetest.chat_send_player(pname, S("No level name provided."))
				return
			end
			if check_for_slash(fields.file_name) then
				minetest.chat_send_player(pname, S("File name must not contain slash or backslash!"))
				return false
			end
			local ok, filename, filename2 = save_level(fields.file_name)
			if ok and filename and filename2 then
				minetest.chat_send_player(pname, S("Level saved to @1 and @2.", filename, filename2))
			elseif ok and filename then
				minetest.chat_send_player(pname, S("Level saved to @1, but could not write metadata to @2.", filename, filename2))
			else
				minetest.chat_send_player(pname, S("Error writing level file!"))
			end
		end
		if fields.cancel then
			lzr_util.show_inventory_formspec_if_supported(player)
		end
		return
	end

	if fields.__lzr_level_editor_exit then
		exit_editor()
		return
	elseif fields.__lzr_level_editor_settings then
		temp_settings_state = table.copy(level_state)
		show_settings_dialog(player, temp_settings_state)
		return
	elseif fields.__lzr_level_editor_save then
		show_save_load_dialog(player, true, level_state.file_name)
		return
	elseif fields.__lzr_level_editor_load then
		show_save_load_dialog(player, false, level_state.file_name)
		return
	elseif fields.__lzr_level_editor_get_item then
		lzr_getitem.show_formspec(pname)
		return
	end
end)

-- Returns the number of custom level files.
lzr_editor.count_custom_levels = function(ignore_autosave)
	local levels_files = minetest.get_dir_list(minetest.get_worldpath().."/levels", false)
	local count = 0
	for l=1, #levels_files do
		if string.lower(string.sub(levels_files[l], -4, -1)) == ".mts" then
			local id = string.sub(levels_files[l], 1, -5)
			if (not ignore_autosave) or (id ~= lzr_globals.AUTOSAVE_NAME) then
				count = count + 1
			end
		end
	end
	return count
end

-- Returns list of all custom level file names, sorted.
-- The file name suffix is omitted.
lzr_editor.get_custom_levels = function(get_level_names)
	local time1 = minetest.get_us_time()
	local levels_files = minetest.get_dir_list(minetest.get_worldpath().."/levels", false)
	local levels = {}
	local playerinfo = minetest.get_player_information("singleplayer")
	local lang = playerinfo.lang_code or "en"
	for l=1, #levels_files do
		if string.lower(string.sub(levels_files[l], -4, -1)) == ".mts" then
			local id = string.sub(levels_files[l], 1, -5)
			if get_level_names then
				local name = lzr_editor.get_custom_level_name(id, true)
				local tr_name = minetest.get_translated_string(lang, name)
				table.insert(levels, {id=id, name=tr_name})
			else
				table.insert(levels, id)
			end
		end
	end
	local compare
	if get_level_names then
		compare = function(level1, level2)
			return level1.name < level2.name
		end
	end
	table.sort(levels, compare)
	local time2 = minetest.get_us_time()
	minetest.log("verbose", "[lzr_editor] lzr_editor.get_custom_levels() took "..time2-time1.." µs")
	return levels
end

-- Returns proper level name of a custom level.
-- The proper level name is read from a matching CSV file.
-- If no such file is found, the empty string is returned.
-- * level_name: Level file name without suffix
-- * with_fallback: If true, will return "Untitled (<file name>)"
--   instead of the empty string if the level name is empty or undefined
lzr_editor.get_custom_level_name = function(level_name, with_fallback)
	local csv_file = io.open(minetest.get_worldpath().."/levels/"..level_name..".csv", "rb")
	local pname
	if csv_file then
		local csv_string = csv_file:read("*a")
		csv_file:close()
		local csv_parsed = rcsv.parse_csv(csv_string)
		if csv_parsed and #csv_parsed >= 1 then
			pname = csv_parsed[1][2]
		end
	end
	if not pname then
		pname = ""
	end
	if pname == "" and with_fallback then
		return S("Untitled (@1)", level_name)
	end
	return pname
end

-- Expose invisible blocks in editor mode
local invis_display_timer = 0
local INVIS_DISPLAY_UPDATE_TIME = 1
local INVIS_DISPLAY_RANGE = 8
minetest.register_globalstep(function(dtime)
	local state = lzr_gamestate.get_state()
	if state ~= lzr_gamestate.EDITOR then
		return
	end
	invis_display_timer = invis_display_timer + dtime
	if invis_display_timer < INVIS_DISPLAY_UPDATE_TIME then
		return
	end
	invis_display_timer = 0

	local player = minetest.get_player_by_name("singleplayer")
	if not player then
		return
	end
	local pos = vector.round(player:get_pos())
	local r = INVIS_DISPLAY_RANGE
	local vm = minetest.get_voxel_manip()
	local emin, emax = vm:read_from_map(vector.offset(pos, -r, -r, -r), vector.offset(pos, r, r, r))
	local area = VoxelArea:new({MinEdge = emin, MaxEdge = emax})
	local data = vm:get_data()
	for x=pos.x-r, pos.x+r do
	for y=pos.y-r, pos.y+r do
	for z=pos.z-r, pos.z+r do
		local vi = area:index(x,y,z)
		local nodename = minetest.get_name_from_content_id(data[vi])
		local texture
		-- TODO: Find a way to mark water source barriers.
		-- (as of MT 5.9.1, particles are invisible when underwater)
		if minetest.get_item_group(nodename, "rain_membrane") > 0 then
			texture = "lzr_core_rain_membrane.png"
		elseif minetest.get_item_group(nodename, "barrier") > 0 then
			texture = "lzr_core_barrier.png"
		end
		if texture then
			minetest.add_particle({
				pos = {x=x, y=y, z=z},
				texture = texture,
				glow = minetest.LIGHT_MAX,
				size = 8,
				expirationtime = 1.1,
			})
		end
	end
	end
	end
end)

minetest.register_chatcommand("editor_clear", {
	privs = {},
	params = "[ clear | regenerate ]",
	description = S("Remove all blocks in the current level area or regenerate the map"),
	func = function(name, param)
		local state = lzr_gamestate.get_state()
		if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then
			return false, S("Not in editor mode!")
		elseif lzr_gamestate.is_loading() then
			return false, S("Can’t do this while loading!")
		end
		local pos = lzr_world.get_level_pos()
		local size = lzr_world.get_level_size()
		local bounding_nodes = {
			node_wall = level_state.wall,
			node_ceiling = level_state.ceiling,
			node_floor = level_state.floor,
		}
		local done = function()
			lzr_gamestate.set_loading(false)
			minetest.chat_send_player(name, S("Level cleared."))
		end
		-- Clear level by removing all blocks
		if param == "" or param == "clear" then
			minetest.log("action", "[lzr_editor] Autosaving level before clearing it")
			autosave_level()
			minetest.log("action", "[lzr_editor] Clearing level ...")

			-- Clearing the level counts as "loading". This blocks all
			-- other commands until the operation is complete.
			lzr_gamestate.set_loading(true)

			lzr_triggers.reset_triggers()
			lzr_levels.build_room({pos=pos, size=size, nodes=bounding_nodes, clear=true, clear_border=false, callback_done=done})
			return true, S("Clearing level …")
		-- Clear level by regenerating the map
		elseif param == "regenerate" then
			minetest.log("action", "[lzr_editor] Autosaving level before clearing it")
			autosave_level()
			minetest.log("action", "[lzr_editor] Clearing level ...")

			lzr_gamestate.set_loading(true)

			lzr_triggers.reset_triggers()
			lzr_levels.reset_level_area(true, pos, size)
			lzr_levels.build_room({pos=pos, size=size, nodes=bounding_nodes, callback_done=done})
			return true, S("Clearing level …")
		else
			-- Invalid parameter
			return false
		end
	end,
})

-- Show a warning when issuing a WorldEdit command in the editor.
-- Using WorldEdit to change blocks can lead to an invalid level state.
-- In particular, it might break the trigger definitions
minetest.register_on_chatcommand(function(name, command, params)
	local cdef = minetest.registered_chatcommands[command]
	if not cdef then
		return
	end
	if lzr_editor.worldedit_warning then
		return
	end
	if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then
		return
	end
	local origin = cdef.mod_origin
	if cdef.mod_origin == "worldedit" or cdef.mod_origin == "worldedit_commands" or cdef.mod_origin == "worldedit_shortcommands" then
		local message = S("WARNING: Changing a trigger block with a WorldEdit command may break the triggers. You may need to call /reset_triggers after doing so.")
		minetest.chat_send_player(name, minetest.colorize(TXT_COLOR_WARNING, message))
		lzr_editor.worldedit_warning = true
	end
end)
