local S = minetest.get_translator("hades_simulation")
local F = minetest.formspec_escape

-- Maximum player health
local MAX_PLAYER_HP = 20

local COLOR_SIM_MSG = "#00FF00"
local COLOR_DUMMY_NAMETAG = "#00FF00"

hades_simulation = {}

local storage = minetest.get_mod_storage()

local mod_hudbars = minetest.get_modpath("hudbars") ~= nil

local stages = {}

-- Boilerplate for compatibiliity with pre-5.9.0
-- versions of minetest
local hud_def_type_field
if minetest.features.hud_def_type_field then
	hud_def_type_field = "type"
else
	hud_def_type_field = "hud_elem_type"
end

local MAX_X = 30000
local MIN_X = -30000

-- Lower left front corner of simulation area border of the first simulation area;
-- subsequent areas will be offset from that position
local SIMULATION_POS = { x = 30000, y = 30000, z = 30000 }
-- Default player spawn position within simuation area
local SIMULATION_SPAWN_POS = { x = 12, y = 3, z = 12 }
-- Default initial look yaw when spawning in simulation
local SIMULATION_SPAWN_YAW = 0
-- Size of simulation area (excluding border)
local SIMULATION_SIZE = { x = 24, y = 24, z = 24 }
-- Distance between two simulation areas
local SIMULATION_MARGIN = 32

local player_hud_ids = {}

-- Holds the status of all simulations.
-- Key: Simulation ID (starting with 1)
-- Value: nil, or table with fields:
--	emerge_status: "not_emerged", "emerging", "emerged"
--	stage: <stage ID>
local simulation_status = {}

local show_simulation_terminal = function(player)
	local form = "formspec_version[6]"..
		"size[10,11]"..
		"label[0.475,0.475;"..F(S("Welcome to the Simulation Terminal! Select a training program:")).."]"
	local y = 1.475
	for stagename, stagedef in pairs(stages) do
		form = form .. "button_exit[3,"..y..";4,0.8;stage_"..stagename..";"..F(stagedef.title or stagename).."]"
		y = y + 1
	end
	minetest.show_formspec(player:get_player_name(), "hades_simulation:terminal", form)
end

-- Get position of the lower left front corner of the simulation
-- area with ID `sim_id` (ID starting at 1).
-- Returns nil if no simulation area is available for this ID.
-- (there is only a finite number of available simulation areas)
local get_simulation_pos = function(sim_id)
	-- sim_id 1 starts at SIMULATION_POS, other sim_ids are offset
	-- on the X axis
	local pos = table.copy(SIMULATION_POS)
	pos.x = pos.x - (sim_id-1) * (SIMULATION_SIZE.x + SIMULATION_MARGIN)
	if pos.x < MIN_X then
		return nil
	end
	return pos
end

-- Returns an ID of a free (unoccupied and unreserved) simulation to enter
local get_free_simulation = function()
	local players = minetest.get_connected_players()
	local taken = {}
	local max_taken_id
	for p=1, #players do
		local player = players[p]
		local meta = player:get_meta()
		local sim_id = meta:get_int("hades_simulation:in_simulation")
		if sim_id > 0 then
			taken[sim_id] = true
			if not max_taken_id or sim > max_taken_id then
				max_taken_id = sim_id
			end
		end
	end
	-- Check reservations
	for sim_id, status in pairs(simulation_status) do
		if status.reserved_for then
			taken[sim_id] = true
			if not max_taken_id or sim_id > max_taken_id then
				max_taken_id = sim_id
			end
		end
	end

	if #taken == 0 then
		-- If nothing was taken, return simulation #1
		return 1
	end
	for t=1, max_taken_id do
		if not taken[t] then
			return t
		end
	end
	-- If all players are in a simulation, pick the maximum taken simulation
	-- ID, add 1 and check if this is a valid ID. If yes, return it,
	-- otherwise we fail.
	local sim_id = max_taken_id + 1
	if get_simulation_pos(sim_id) then
		return sim_id
	end
	return nil
end

-- Checks if the given simulation is occupied by nobody
local is_simulation_unoccupied = function(sim_id)
	local players = minetest.get_connected_players()
	local taken = {}
	local max_taken_id
	for p=1, #players do
		local player = players[p]
		local meta = player:get_meta()
		local their_sim = meta:get_int("hades_simulation:in_simulation")
		if their_sim > 0 and sim_id == their_sim then
			return false
		end
	end
	return true
end

local sound_metal
if minetest.get_modpath("hades_sounds") then
	sound_metal = hades_sounds.node_sound_metal_defaults()
end
local sound_grid = { footstep = { name = "hades_simulation_grid_footstep", gain = 0.2, } }
-- Grid nodes for the inner part of the simulation room,
minetest.register_node("hades_simulation:grid", {
	description = S("Simulation Grid"),
	tiles = {"hades_simulation_grid.png"},
	is_ground_content = false,
	-- Prevent players to build at simulation walls
	pointable = "blocking",
	diggable = false,
	on_blast = function() end,
	sounds = sound_grid,
	groups = { virtual = 1, simulation_grid = 1, not_in_creative_inventory = 1 },
})
minetest.register_node("hades_simulation:grid_floor", {
	description = S("Simulation Grid Floor"),
	tiles = {"hades_simulation_grid.png"},
	is_ground_content = false,
	-- ALLOW players to build on simulation floor
	pointable = true,
	diggable = false,
	on_blast = function() end,
	sounds = sound_grid,
	groups = { virtual = 1, simulation_grid = 1, not_in_creative_inventory = 1 },
})
-- Same as normal grid node, but it propagates sunlight
-- (so the room isn't pitch black)
minetest.register_node("hades_simulation:grid_ceiling", {
	description = S("Simulation Grid Ceiling"),
	tiles = {"hades_simulation_grid_ceiling.png", "hades_simulation_grid.png", "hades_simulation_grid_ceiling.png"},
	paramtype = "light",
	sunlight_propagates = true,
	is_ground_content = false,
	-- Prevent players to build at simulation ceiling
	pointable = "blocking",
	diggable = false,
	on_blast = function() end,
	sounds = sound_grid,
	groups = { virtual = 1, simulation_grid = 1, not_in_creative_inventory = 1 },
})
-- Hull nodes are used for the outside of the simulation
-- room.
minetest.register_node("hades_simulation:hull", {
	description = S("Simulation Hull"),
	tiles = {"hades_simulation_hull.png"},
	is_ground_content = false,
	pointable = "blocking",
	diggable = false,
	sounds = sound_metal,
	on_blast = function() end,
	groups = { virtual = 1, simulation_hull = 1, not_in_creative_inventory = 1 },
})
-- Same as hull node, but propagates sunlight
minetest.register_node("hades_simulation:hull_ceiling", {
	description = S("Simulation Hull Ceiling"),
	tiles = {"hades_simulation_hull_ceiling.png"},
	paramtype = "light",
	sunlight_propagates = true,
	is_ground_content = false,
	sounds = sound_metal,
	pointable = true,
	diggable = false,
	on_blast = function() end,
	groups = { virtual = 1, simulation_hull = 1,not_in_creative_inventory = 1 },
})




-- Returns true if the given player is currently in a simulation.
-- `player` must be a player object; only works if the player
-- is online.
hades_simulation.is_in_simulation = function(player)
	local meta = player:get_meta()
	return meta:get_int("hades_simulation:in_simulation") ~= 0
end

-- Removes objects in a simulation room at pos
local clean_simulation_room = function(pos)
	minetest.log("info", "[hades_simulation] Removing objects in simulation room at "..minetest.pos_to_string(pos))
	local minpos = vector.subtract(pos, 1, 1, 1)
	local maxpos = vector.add(pos, SIMULATION_SIZE)
	maxpos = vector.add(maxpos, vector.new(1, 1, 1))
	local objs = minetest.get_objects_in_area(minpos, maxpos)
	for o=1, #objs do
		local obj = objs[o]
		if obj:is_valid() and not obj:is_player() then
			obj:remove()
		end
	end
end

local build_simulation_room = function(pos)
	minetest.log("info", "[hades_simulation] Building simulation room at "..minetest.pos_to_string(pos))
	local posses_hull_ceiling = {}
	local posses_hull = {}
	local posses_floor = {}
	local posses_ceiling = {}
	local posses_border = {}
	local posses_air = {}
	local size = vector.add(SIMULATION_SIZE, {x=1,y=1,z=1})
	for x=-1,size.x+1 do
	for z=-1,size.z+1 do
	for y=-1,size.y+1 do
		local offset = {x=x-1, y=y-1, z=z-1}
		if (x >= 1 and x < size.x) and
		(y >= 1 and y < size.y) and
		(z >= 1 and z < size.z) then
			table.insert(posses_air, vector.add(pos, offset))
		elseif (x >= 0 and x < size.x+1) and
		(y >= 0 and y < size.y+1) and
		(z >= 0 and z < size.z+1) then
			if y == size.y then
				table.insert(posses_ceiling, vector.add(pos, offset))
			elseif y == 0 then
				table.insert(posses_floor, vector.add(pos, offset))
			else
				table.insert(posses_border, vector.add(pos, offset))
			end
		else
			if y < size.y+1 then
				table.insert(posses_hull, vector.add(pos, offset))
			else
				table.insert(posses_hull_ceiling, vector.add(pos, offset))
			end
		end
	end
	end
	end
	minetest.bulk_set_node(posses_hull_ceiling, {name="hades_simulation:hull_ceiling"})
	minetest.bulk_set_node(posses_hull, {name="hades_simulation:hull"})
	minetest.bulk_set_node(posses_floor, {name="hades_simulation:grid_floor"})
	minetest.bulk_set_node(posses_ceiling, {name="hades_simulation:grid_ceiling"})
	minetest.bulk_set_node(posses_border, {name="hades_simulation:grid"})
	minetest.bulk_set_node(posses_air, {name="air"})
end

local build_simulation_stage = function(pos, stage)
	local stagedef = stages[stage]
	minetest.place_schematic(pos, stagedef.schematic, "0", {}, true, "")
end

local emerge_simulation_callback = function(blockpos, action, calls_remaining, param)
	minetest.log("verbose", "[hades_simulation] emerge_simulation_callback() ...")
	if action == minetest.EMERGE_ERRORED then
		minetest.log("error", "[hades_simulation] Simulation room #"..param.sim_id.." emerging error.")
	elseif action == minetest.EMERGE_CANCELLED then
		minetest.log("error", "[hades_simulation] Simulation room #"..param.sim_id.." emerging cancelled.")
	elseif calls_remaining == 0 and (action == minetest.EMERGE_GENERATED or action == minetest.EMERGE_FROM_DISK or action == minetest.EMERGE_FROM_MEMORY) then
		build_simulation_room(param.pos)
		if simulation_status[param.sim_id] then
			simulation_status[param.sim_id].emerge_status = "emerged"
		end
		if param.stage then
			build_simulation_stage(param.pos, param.stage)
		end
		minetest.log("action", "[hades_simulation] Simulation room #"..param.sim_id.." emerged and built.")
		if param.on_ready then
			param.on_ready()
		end
	end
end

local prepare_simulation_room = function(sim_id, stage, reserve_for, on_ready)
	local pos = get_simulation_pos(sim_id)
	if simulation_status[sim_id] and reserve_for and simulation_status[sim_id].reserved_for and simulation_status[sim_id].reserved_for ~= reserve_for then
		-- Simulation is reserved
		return false
	end
	simulation_status[sim_id] = {
		emerge_status = "emerging",
		stage = stage,
		reserved_for = reserve_for,
	}
	minetest.log("verbose", "[hades_simulation] Starting to emerge simulation room ...")
	minetest.emerge_area(pos, vector.add(pos, SIMULATION_SIZE), emerge_simulation_callback, {pos=pos, stage=stage, sim_id = sim_id, on_ready = on_ready})
	return true
end

local add_simulation_huds = function(player)
	local hudid_note = player:hud_add({
		[hud_def_type_field] = "text",
		text = S("Simulation active"),
		position = { x = 0, y = 1 },
		number = 0x00FF00,
		z_index = 100,
		scale = { x = 100, y = 100 },
		alignment = { x = 1, y = -1 },
		offset = { x = 10, y = -10 },
		size = { x = 2, y = 2 },
	})
	local hudid_vignette = player:hud_add({
		[hud_def_type_field] = "image",
		text = "hades_simulation_vignette.png^[multiply:#15380e",
		position = { x = 0.5, y = 0.5 },
		z_index = -410,
		scale = { x = -100, y = -100 },
		alignment = 0,
	})

	player_hud_ids[player:get_player_name()] = {
		note = hudid_note,
		vignette = hudid_vignette,
	}
end
local remove_simulation_huds = function(player)
	local hudids = player_hud_ids[player:get_player_name()]
	if hudids then
		player:hud_remove(hudids.note)
		player:hud_remove(hudids.vignette)
		player_hud_ids[player:get_player_name()] = nil
	end
end

hades_sky.register_sky("simulation", {
	init = function(player)
		player:set_sun({visible=false})
		player:set_sky({type="regular", clouds=false, sky_color={
			day_sky = "#002000",
			day_horizon = "#002000",
			dawn_sky = "#002000",
			dawn_horizon = "#002000",
			night_sky = "#002000",
			night_horizon = "#002000",
			fog_tint_type = "custom",
			fog_sun_tint = "#008000",
			fog_moon_tint = "#008000",
		}})
		player:override_day_night_ratio(1)
	end,
	drop = function(player)
		player:override_day_night_ratio(nil)
	end,
	update = function() end,
})

local set_simulation_sky = function(player, enable)
	if enable then
		hades_sky.set_sky(player, "simulation")
	else
		hades_sky.set_sky(player, "world")
	end
end

-- Update the player inventory when leaving/entering the simulation.
local sim_inv = function(player, is_entering)
	local unghostify = true
	if is_entering then
		-- On entering, we "ghostify" the inventories which means
		-- the player inventory contents are copied to a special place,
		-- and the regular inventory is cleared
		hades_ghostinv.ghostify_inventories(player, "___sim")
	else
		-- On leaving, the player inventory is "unghostified".
		-- The old player inventory that was copied before gets restored
		hades_ghostinv.unghostify_inventories(player, "___sim")
	end
end

local enter_simulation = function(player, sim_id)
	local meta = player:get_meta()
	local insim = hades_simulation.is_in_simulation(player)

	-- Shortcut to clear the reservation
	-- in case of error.
	local clres = function()
		if simulation_status[sim_id] then
			simulation_status[sim_id].reserved_for = nil
		end
	end

	if insim then
		-- Already in simulation
		return false, "already_in_simulation"
	end
	-- Check if simulation was initialized
	local ss = simulation_status[sim_id]
	if not ss then
		return false, "no_init"
	elseif ss.emerge_status == "not_emerged" then
		return false, "no_init"
	elseif ss and ss.emerge_status == "emerging" then
		return false, "emerging"
	end

	-- The simulation room we want to enter is reserved for
	-- another player
	if ss.reserved_for and ss.reserved_for ~= player:get_player_name() then
		return false, "wrong_reservation"
	end

	local stage = ss.stage
	-- Check if player has full breath
	local props = player:get_properties()
	if player:get_breath() < props.breath_max then
		clres()
		return false, "not_enough_breath"
	end

	-- Check if player moves too fast
	local vel = player:get_velocity()
	local epsilon = 0.001
	if math.abs(vel.x) > 0.001 or math.abs(vel.y) > 0.001 or math.abs(vel.z) > 0.001 then
		clres()
		return false, "too_fast"
	end

	-- Check if we can enter simulation
	if not is_simulation_unoccupied(sim_id) then
		clres()
		return false, "simulation_occupied"
	end

	local meta = player:get_meta()
	local hp_max = props.hp_max
	meta:set_int("hades_simulation:health", hp_max)
	local rpos = player:get_pos()
	meta:set_string("hades_simuation:return_pos", minetest.pos_to_string(rpos, 1))

	-- Disable drowning in simulation.
	-- IMPORTANT: No other mod should touch the drowning flag
	-- while we're in the simulation
	player:set_flags({drowning=false})
	minetest.log("info", "[hades_simulation] Drowning of "..player:get_player_name().." disabled")

	local stagedef = stages[stage]
	local sim_pos = get_simulation_pos(sim_id)
	player:set_pos(vector.add(sim_pos, stagedef.spawn_pos))

	meta:set_int("hades_simulation:in_simulation", sim_id)

	-- Spawn avatar at return pos
	local obj = minetest.add_entity(rpos, "hades_simulation:avatar", player:get_player_name())
	if obj then
		obj:set_yaw(player:get_look_horizontal())
	end

	player:set_yaw(stagedef.spawn_yaw)
	set_simulation_sky(player, true)

	-- ghostify inventories (swap "real" with "simulated" inventories)
	sim_inv(player, true)

	-- Add reality beacon
	local last_hotbar_slot = player:hud_get_hotbar_itemcount()
	-- We can set the stack directly because we start with an empty inventory
	-- after sim_inv()
	local inv = player:get_inventory()

	inv:set_stack("main", last_hotbar_slot, "hades_simulation:reality_beacon")

	-- Starter items for this stage
	if stagedef.items then
		for i=1, #stagedef.items do
			inv:add_item("main", stagedef.items[i])
		end
	end

	clean_simulation_room(sim_pos)

	hades_coords.set_pos_offset(player, vector.multiply(sim_pos, -1))
	add_simulation_huds(player)

	if mod_hudbars then
		hb.change_hudbar(player, "simhealth", 20, 20)
		hb.unhide_hudbar(player, "simhealth")
	end

	minetest.log("action", "[hades_simulation] "..player:get_player_name().." entered the simulation #"..sim_id)

	local title = stagedef.title or stage
	minetest.chat_send_player(player:get_player_name(), minetest.colorize(COLOR_SIM_MSG, S("Welcome to the simulation in stage @1!", title)))
	if stagedef and stagedef.goals then
		if stagedef.goals.description then
			minetest.chat_send_player(player:get_player_name(), minetest.colorize(COLOR_SIM_MSG, S("Stage goal: @1", stagedef.goals.description)))
		end
		if stagedef.goals.hint then
			minetest.chat_send_player(player:get_player_name(), minetest.colorize(COLOR_SIM_MSG, S("Hint: @1", stagedef.goals.hint)))
		end
	end

	return true
end

local leave_simulation = function(player)
	local meta = player:get_meta()
	local insim = hades_simulation.is_in_simulation(player)
	if not insim then
		-- Already not in a simulation
		return false
	end
	player:set_flags({drowning=true})
	minetest.log("info", "[hades_simulation] Drowning of "..player:get_player_name().." enabled")
	local meta = player:get_meta()
	local rposstr = meta:get_string("hades_simuation:return_pos")
	local rpos = minetest.string_to_pos(rposstr)
	if rpos then
		-- Remove avatar/avatars assigned to player at return pos
		local objs = minetest.get_objects_inside_radius(rpos, 2)
		for o=1, #objs do
			local obj = objs[o]
			if obj:is_valid() then
				local ent = obj:get_luaentity()
				if ent and obj.name == "hades_simulation:avatar" then
					if ent._representing == player:get_player_name() then
						obj:remove()
						minetest.log("info", "[hades_simulation] Avatar of "..player:get_player_name().." removed")
					end
				end
			end
		end
		player:set_pos(rpos)
	else
		minetest.log("error", "[hades_simulation] Player "..player:get_player_name().." has no return position for leaving the simulation")
		-- Return to avatar pos
		-- TODO: Return to world spawn
		player:set_pos({x=0,y=10,z=0})
	end
	local sim_id = meta:get_int("hades_simulation:in_simulation")
	if sim_id > 0 then
		local sim_pos = get_simulation_pos(sim_id)
		clean_simulation_room(sim_pos)
	end

	meta:set_int("hades_simulation:in_simulation", 0)
	hades_coords.set_pos_offset(player, nil)
	set_simulation_sky(player, false)
	sim_inv(player, false)

	remove_simulation_huds(player)

	if mod_hudbars then
		hb.hide_hudbar(player, "simhealth")
	end

	minetest.log("action", "[hades_simulation] "..player:get_player_name().." left the simulation")
	return true
end

minetest.register_on_joinplayer(function(player)
	local name = player:get_player_name()
	local meta = player:get_meta()

	local props = player:get_properties()
	local sim_hp = meta:get_int("hades_simulation:health")
	hb.init_hudbar(player, "simhealth", sim_hp, props.hp_max, true)

	local insim = hades_simulation.is_in_simulation(player)
	-- Player must join the game outside of simulation,
	-- to keep simulation rooms free for other players
	minetest.log("info", "[hades_simulation] Is leaving the simulation on joining")
	leave_simulation(player)
end)

minetest.register_on_leaveplayer(function(player)
	local name = player:get_player_name()
	player_hud_ids[name] = nil
	local meta = player:get_meta()
	local sim_id = meta:get_int("hades_simulation:in_simulation")
	if sim_id > 0 then
		local sim_pos = get_simulation_pos(sim_id)
		clear_simulation_room(sim_pos)
	end
end)

minetest.register_on_dieplayer(function(player)
	local name = player:get_player_name()
	local meta = player:get_meta()
	local insim = hades_simulation.is_in_simulation(player)
	if not insim then
		-- Not in a simulation, nothing to do
		return
	end
	-- Throw player out of simulation
	meta:set_int("hades_simulation:in_simulation", 0)
	set_simulation_sky(player, false)
	hades_coords.set_pos_offset(player, nil)
	remove_simulation_huds(player)
end)

-- Handle player HP changes while in simulation
minetest.register_on_player_hpchange(function(player, hp_change, reason)
	if hades_simulation.is_in_simulation(player) then
		-- Hunger and food poisoning are not part of the simulation, so they hurt
		-- the player directly
		if reason.type == "set_hp" and reason.from == "mod" then
			if reason._custom_reason == "hp_command" then
				if hp_change < 0 then
					leave_simulation(player)
					minetest.chat_send_player(player:get_player_name(), S("You got hurt in reality. Simulation shutdown."))
				end
				return hp_change
			elseif reason._custom_reason == "hunger" or reason._custom_reason == "food_poisoning" then
				-- Also kick out the player from the simulation
				leave_simulation(player)
				if reason._custom_reason == "hunger" then
					minetest.chat_send_player(player:get_player_name(), S("You’re too hungry for the simulation. Simulation shutdown."))
				elseif reason._custom_reason == "food_poisoning" then
					minetest.chat_send_player(player:get_player_name(), S("You’re too sick for the simulation. Simulation shutdown."))
				end
				return hp_change
			-- HP increase affects real health, except when '/hp' command was used
			elseif reason._custom_reason ~= "hp_command" and hp_change > 0 then
				return hp_change
			end
		end
		-- All other forms of damage reduce the simulated player HP instead

		-- Manually play damage sound except for fall damage because fall damage *does*
		-- cause the engine to play a sound regardless (as of Luanti 5.11.0)
		if hp_change < 0 and reason.type ~= "fall" then
			minetest.sound_play("player_damage", {to_player=player:get_player_name()}, true)
		end
		local meta = player:get_meta()
		local hp_sim = meta:get_int("hades_simulation:health")
		hp_sim = math.max(0, hp_sim + hp_change)
		meta:set_int("hades_simulation:health", hp_sim)
		-- Kick player out of simulation if dead
		if hp_sim <= 0 then
			minetest.chat_send_player(player:get_player_name(), S("You died in the simulation. Simulation shutdown."))
			leave_simulation(player)
		else
			hb.change_hudbar(player, "simhealth", hp_sim)
		end
		return 0
	end
	return hp_change
end, true)

-- Periodically check if all players have passed
-- their stage goals. If yes, call the "victory"
-- and make them leave.
-- Also checks if the player still holds the home
-- beacon. If not, also make them leave the
-- simulation
local simtimer = 0
local SIM_CHECK_TIME = 1.0
minetest.register_globalstep(function(dtime)
	simtimer = simtimer + dtime
	if simtimer < SIM_CHECK_TIME then
		return
	end
	simtimer = 0
	local players = minetest.get_connected_players()
	for p=1, #players do
		local player = players[p]
		local pname = player:get_player_name()
		local meta = player:get_meta()
		local sim_id = meta:get_int("hades_simulation:in_simulation")
		local sim_status = simulation_status[sim_id]
		if sim_status then
			local stage = sim_status.stage
			local stagedef = stages[stage]
			local goals = stagedef.goals

			local inv = player:get_inventory()
			-- Leave simulation of player lost their reality beacon
			if not inv:contains_item("main", "hades_simulation:reality_beacon") then
				minetest.chat_send_player(pname, minetest.colorize(COLOR_SIM_MSG, S("Simulation shutdown.")))
				leave_simulation(player)			

			-- Check goals
			elseif goals then
				local all_goals_passed = true
				-- Items goal: have all required items in inventory
				-- (only the "main" list is checked)
				if goals.items then
					for i=1, #goals.items do
						if not inv:contains_item("main", goals.items[i]) then
							all_goals_passed = false
							break
						end
					end
				end
				-- "stand on node" goal: Player must stand on a particular node
				if goals.stand_on_node then
					local pos = player:get_pos()
					local bnode = minetest.get_node(vector.offset(pos, 0, -1, 0))
					if bnode.name ~= goals.stand_on_node then
						all_goals_passed = false
					end
				end
				if all_goals_passed then
					minetest.chat_send_player(pname, minetest.colorize(COLOR_SIM_MSG, S("This stage has been PASSED! Simulation shutdown.")))
					leave_simulation(player)
				end
			end
		end
	end
end)

-- Special item that allows player to leave simulation,
-- By dropping it or otherwise removing it from their inventory.
-- TODO: Also leave simulation when using the item
-- (tricky to do because of item slot overrides)
minetest.register_tool("hades_simulation:reality_beacon", {
	description = S("Reality Beacon"),
	_tt_help = S("Drop it to leave simulation"),
	inventory_image = "hades_simulation_reality_beacon.png",
	wield_image = "hades_simulation_reality_beacon.png",
	groups = {
		virtual = 1,
		disable_repair = 1,
		not_in_creative_inventory = 1,
	},
	on_use = function(itemstack, user, pointed_thing)
		if user and hades_simulation.is_in_simulation(user) then
			minetest.chat_send_player(user:get_player_name(), minetest.colorize(COLOR_SIM_MSG, S("Drop this item to leave the simulation.")))
		end
		return itemstack
	end,
	on_drop = function(itemstack, dropper, pointed_thing)
		if dropper and hades_simulation.is_in_simulation(dropper) then
			minetest.chat_send_player(dropper:get_player_name(), minetest.colorize(COLOR_SIM_MSG, S("Simulation shutdown.")))
			leave_simulation(dropper)
			return itemstack
		else
			return ""
		end
	end,
})

minetest.register_on_player_receive_fields(function(player, formname, fields)
	if formname ~= "hades_simulation:terminal" then
		return
	end
	local pname = player:get_player_name()
	if hades_simulation.is_in_simulation(player) then
		minetest.chat_send_player(pname, minetest.colorize("#FFFF00", S("SIMULCEPTION DETECTED: You can’t run a simulation within a simulation.")))
		return
	end

	local sim_id = get_free_simulation()
	if not sim_id then
		minetest.chat_send_player(pname, S("All simulation slots are currently occupied. Please come back later."))
	end
	local on_ready = function()
		local ok, fail_reason = enter_simulation(player, sim_id)
		if not ok then
			local msg
			if fail_reason == "already_in_simulation" then
				msg = S("Already in simulation.")
			elseif fail_reason == "not_enough_breath" then
				msg = S("Catch your breath first.")
			elseif fail_reason == "too_fast" then
				msg = S("You need to stop moving first.")
			elseif fail_reason == "simulation_occupied" then
				msg = S("This simulation slot (#@1) is used by another player. Please try another simulation slot or come back later.", sim_id)
			elseif fail_reason == "no_init" then
				msg = S("This simulation slot (#@1) has not been initialized yet. Use “/simbuild”.", sim_id)
			elseif fail_reason == "emerging" then
				msg = S("This simulation slot (#@1) is not ready yet. Please come back later.", sim_id)
			elseif fail_reason == "wrong_reservation" then
				msg = S("The simulation slot (#@1) was reserved by another player. Please try again.", sim_id)
			else
				minetest.log("error", "[hades_simulation] Unknown error while attempting to enter simulation (player="..tostring(name)..")")
				msg = S("Cannot enter simulation: Unknown error.")
			end
			minetest.chat_send_player(pname, msg)
		end
	end
	local chosen_stage
	for stagename, _ in pairs(stages) do
		if fields["stage_"..stagename] then
			chosen_stage = stagename
			break
		end
	end
	if not chosen_stage then
		return
	end
	local ok = prepare_simulation_room(sim_id, chosen_stage, pname, on_ready)
	if not ok then
		minetest.chat_send_player(pname, S("The simulation slot (#@1) was reserved by another player. Please try again.", sim_id))
	else
		minetest.chat_send_player(pname, S("Entering simulation …"))
	end
end)

-- Terminal to enter the simulation.
-- This node should be placed by admins only in safe locations,
-- so it cannot be abused by players to escape
-- from fights.
minetest.register_node("hades_simulation:terminal", {
	description = S("Simulation Terminal"),
	-- TODO: Remove the "EXPERIMENTAL" label when the simulation is no longer experimental
	_tt_help = S("EXPERIMENTAL").."\n"..S("Allows entering the simulated reality"),
	tiles = {
		"hades_simulation_terminal_top.png",
		"hades_simulation_terminal_bottom.png",
		"hades_simulation_terminal_side.png",
		"hades_simulation_terminal_side.png",
		"hades_simulation_terminal_side.png",
		{name="hades_simulation_terminal_front_anim.png", animation={type="vertical_frames", aspect_w=16, aspect_h=16, length=3.5}},
	},
	light_source = 5,
	paramtype = "light",
	paramtype2 = "4dir",
	sunlight_propagates = false,
	is_ground_content = false,
	sounds = sound_metal,
	pointable = true,
	groups = { cracky = 2, not_in_creative_inventory = 1 },
	on_rightclick = function(pos, node, clicker)
		if not clicker then
			return
		end
		local cname = clicker:get_player_name()
		if hades_simulation.is_in_simulation(clicker) then
			--~ Simulception is a word I invented for when you run a simulation within a simulation. A wordplay on the movie “Inception”
			minetest.chat_send_player(cname, minetest.colorize("#FFFF00", S("SIMULCEPTION DETECTED: You can’t run a simulation within a simulation.")))
			return
		end
		show_simulation_terminal(clicker)

	end,
	on_construct = function(pos)
		local meta = minetest.get_meta(pos)
		meta:set_string("infotext", S("Simulation Terminal"))
	end,
})


-- Register stages
hades_simulation.register_stage = function(name, def)
	stages[name] = table.copy(def)
end

local SCHEM_PATH = minetest.get_modpath("hades_simulation").."/schematics/"

hades_simulation.register_stage("tutorial_walk_1", {
	title = S("Movement (part 1)"),
	schematic = SCHEM_PATH .. "/hades_simulation_tutorial_walk_1.mts",
	spawn_pos = vector.new(22, 1.5, 1),
	spawn_yaw = 0,
	goals = {
		description = S("Avoid the lava and go to the checkered blocks."),
		stand_on_node = "hades_tiles:floor_basalt_marble",
	},
})
hades_simulation.register_stage("tutorial_walk_2", {
	title = S("Movement (part 2)"),
	schematic = SCHEM_PATH .. "/hades_simulation_tutorial_walk_2.mts",
	spawn_pos = vector.new(23, 8.5, 0),
	spawn_yaw = math.pi/8,
	goals = {
		description = S("Go to the checkered blocks."),
		stand_on_node = "hades_tiles:floor_basalt_marble",
	},
})
hades_simulation.register_stage("tutorial_moss", {
	title = S("Finding Water"),
	schematic = SCHEM_PATH .. "/hades_simulation_tutorial_moss.mts",
	spawn_pos = vector.new(1, 2.5, 22),
	spawn_yaw = math.pi,
	items = {
		"hades_core:pick_steel",
		"hades_core:shovel_steel",
		"hades_liquidtanks:tank",
	},
	goals = {
		description = S("Dig around to find water, then put it in your glass tank."),
		hint = S("Look for mossy stone."),
		items = { "hades_liquidtanks:tank_water" },
	},
})
hades_simulation.register_stage("tutorial_lava_build", {
	title = S("Compressed Blocks"),
	schematic = SCHEM_PATH .. "/hades_simulation_tutorial_lava_build.mts",
	spawn_pos = vector.new(20, 4.5, 4),
	spawn_yaw = math.pi/8,
	items = {
		"hades_core:shovel_steel",
	},
	goals = {
		description = S("Go to the checkered blocks."),
		hint = S("You can craft volcanic ash blocks and build a bridge. They don’t fall."),
		stand_on_node = "hades_tiles:floor_basalt_marble",
	},
})


-- A avatar entity representing a player in 'reality'
-- when the player is in a simulation. This avatar is vulnerable
-- to attacks and environment damage and when it receives
-- any damage, the real player is kicked out of the simulation
minetest.register_entity("hades_simulation:avatar", {
	initial_properties = {
		visual = "mesh",
		visual_size = {x=1, y=1},
		mesh = "character.b3d",
		shaded = false,
		show_on_minimap = true,
		textures = { "hades_simulation_virtual_skin.png" },
		backface_culling = true,
		damage_texture_modifier = "^[colorize:#FF0000FF",
		hp_max = 20,
		collisionbox = { -0.3, 0, -0.3, 0.3, 1.77, 0.3 },
		selectionbox = { -0.3, 0, -0.3, 0.3, 1.77, 0.3, rotate = false },
		physical = true,
		collide_with_objects = false,
	},
	on_activate = function(self, staticdata)
		if staticdata then
			self._representing = staticdata
			self.object:set_properties({
				nametag = S("Avatar of @1", self._representing),
				nametag_color = COLOR_DUMMY_NAMETAG,
			})
		end

		-- Insta-remove avatar if not assigned to a player,
		-- player does not exist or is not in simulation
		if not self._representing then
			self.object:remove()
			return
		end
		local player = minetest.get_player_by_name(self._representing)
		if not player then
			self.object:remove()
			return
		end
		if not hades_simulation.is_in_simulation(player) then
			self.object:remove()
			return
		end

		self.object:set_animation({x=0, y=79}, 30, 0, true)
		local grav = tonumber(minetest.settings:get("movement_gravity")) or 10
		self.object:set_acceleration({x=0, y=-grav, z=0})
		self._start_pos = self.object:get_pos()
	end,
	_timer = 0,
	get_staticdata = function(self)
		if self._representing then
			return self._representing
		else
			return ""
		end
	end,
	on_step = function(self, dtime)
		if not self._representing then
			return
		end

		self._timer = self._timer + dtime
		if self._timer < 1.0 then
			return
		end
		self._timer = 0

		local player = minetest.get_player_by_name(self._representing)
		if not player then
			return
		end

		-- Remove avatar if player is no longer in simulation
		if not hades_simulation.is_in_simulation(player) then
			self.object:remove()
			return
		end


		-- Kick player back to reality if avatar moved too much from original position
		local pos = self.object:get_pos()
		if self._start_pos then
			local dist = vector.distance(pos, self._start_pos)
			if dist > 0.9 then
				minetest.log("info", "[hades_simulation] Avatar of "..self._representing.." is in a drowning node or touches a damage_per_second node!")
				if hades_simulation.is_in_simulation(player) then
					leave_simulation(player)
					self.object:remove()
					minetest.chat_send_player(player:get_player_name(), minetest.colorize(COLOR_SIM_MSG, S("Your avatar has moved! Simulation shutdown.")))
					return
				end
			end
		end

		-- Kick player back to reality if avatar is in a dangerous node
		local head = table.copy(pos)
		local feet = table.copy(pos)
		feet.y = feet.y + 0.5
		head.y = head.y + 1.5
		local hnode = minetest.get_node(head)
		local hdef = minetest.registered_nodes[hnode.name]
		local fnode = minetest.get_node(feet)
		local fdef = minetest.registered_nodes[fnode.name]

		if hdef.drowning > 0 or hdef.damage_per_second > 0 or fdef.damage_per_second > 0 then
			minetest.log("info", "[hades_simulation] Avatar of "..self._representing.." is in a drowning node or touches a damage_per_second node!")
			if hades_simulation.is_in_simulation(player) then
				leave_simulation(player)
				self.object:remove()
				minetest.chat_send_player(player:get_player_name(), minetest.colorize(COLOR_SIM_MSG, S("Your avatar is in danger! Simulation shutdown.")))
				return
			end
		end
	end,
	on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, direction)
		if not self._representing then
			self.object:remove()
			return
		end

		local player = minetest.get_player_by_name(self._representing)
		if not player then
			return
		end
		minetest.log("info", "[hades_simulation] Avatar of "..self._representing.." got punched!")
		if hades_simulation.is_in_simulation(player) then
			leave_simulation(player)
			player:punch(puncher, time_from_last_punch, tool_capabilities, direction)
			self.object:remove()
			minetest.chat_send_player(self._representing, minetest.colorize(COLOR_SIM_MSG, S("Your avatar was punched! Simulation shutdown.")))
			return
		end
	end,
	on_deactivate = function(self, removal)
		if not removal or not self._representing then
			return
		end
		local player = minetest.get_player_by_name(self._representing)
		if not player then
			return
		end
		minetest.log("info", "[hades_simulation] Avatar of "..self._representing.." was deleted!")
		if hades_simulation.is_in_simulation(player) then
			minetest.chat_send_player(self._representing, minetest.colorize(COLOR_SIM_MSG, S("Your avatar was destroyed! Simulation shutdown.")))
			leave_simulation(player)
			return
		end
	end,
	on_death = function(self)
		if not self._representing then
			return
		end
		local player = minetest.get_player_by_name(self._representing)
		if not player then
			return
		end
		minetest.log("info", "[hades_simulation] Avatar of "..self._representing.." died!")
		if hades_simulation.is_in_simulation(player) then
			minetest.chat_send_player(self._representing, minetest.colorize(COLOR_SIM_MSG, S("Your avatar was destroyed! Simulation shutdown.")))
			leave_simulation(player)
			return
		end
	end,
})

-- Statbars for simulated health statbars
if mod_hudbars then
	hb.register_hudbar("simhealth", 0xFFFFFF, S("Health"), { bar = "hades_simulation_bar_health.png", icon = "hades_simulation_icon_health.png", bgicon = "hades_simulation_bgicon_health.png" }, MAX_PLAYER_HP, MAX_PLAYER_HP, true)
end

