eb_levels = {}

local S = core.get_translator("eb_levels")

local levels = {}
local current_level = nil
local is_loading = false
local LEVEL_DISTANCE = 1000
local LEVEL_POS_LIMIT = 29000
local EXIT_CHECK_TIME = 0.25
local EXIT_TIME = 0.2
local FALLOUT_HEIGHT = 16
local EXTEND_COLUMN_HEIGHT = 120
local last_level_pos = vector.zero()


local extend_column = function(pos)
	local posses = {}
	local node = core.get_node(pos)
	for y=1,EXTEND_COLUMN_HEIGHT do
		table.insert(posses, {x=pos.x,y=pos.y-y,z=pos.z})
	end
	core.bulk_set_node(posses, node)
end

eb_levels.current_level_pos = function()
	if current_level then
		return levels[current_level].world_pos
	else
		return nil
	end
end

eb_levels.current_level_timeofday = function()
	if current_level then
		return levels[current_level].timeofday
	else
		return nil
	end
end

eb_levels.leave_level = function()
	current_level = nil
	eb_eyeballs.deactivate_eyeballs()
	eb_eyeballs.deactivate_eyeball_fail()
	eb_music.stop_track()
end

eb_levels.register_level = function(level_def)
	local def = table.copy(level_def)
	def.world_pos = table.copy(last_level_pos)
	if level_def.world_pos_offset then
		-- HACK:
		-- world_pos_offset offsets the level position from the regular
		-- position so that 2×2 graffiti nodes render correctly.
		-- TODO: Remove this hack and redo graffiti nodes to be indepentent
		-- of node position
		def.world_pos = vector.add(def.world_pos, level_def.world_pos_offset)
	end

	last_level_pos.x = last_level_pos.x + LEVEL_DISTANCE
	if last_level_pos.x > LEVEL_POS_LIMIT then
		last_level_pos.x = 0
		last_level_pos.z = last_level_pos.z + LEVEL_DISTANCE
		if last_level_pos.z > LEVEL_POS_LIMIT then
			core.log("error", "[eb_levels] Level pos limit surpassed. Can't register level")
			return
		end
	end

	local schem = core.read_schematic(core.get_modpath("eb_levels").."/schematics/"..level_def.schem, {write_yslice_prob="none"})
	if not schem then
		core.log("error", "[eb_levels] Could not load level schematic '"..tostring(level_def.schem).."'")
		return
	end
	def.size = table.copy(schem.size)
	if not def.timeofday then
		def.timeofday = 0.5 -- day
	end

	table.insert(levels, def)
end

eb_levels.player_must_be_seen = function()
	if not current_level then
		return false
	end
	local level = levels[current_level]
	if level.player_must_be_seen == false then
		return false
	else
		return true
	end
end

eb_levels.get_eyeballs_in_current_level = function()
	if not current_level then
		return nil
	end
	local level = levels[current_level]
	local pos1, pos2 = level.world_pos, vector.add(level.world_pos, level.size)
	local objs = core.get_objects_in_area(pos1, pos2)
	local mobile_eyeballs = {}
	for o=1, #objs do
		local obj = objs[o]
		if eb_eyeballs.object_is_eyeball(obj) then
			local lua = obj:get_luaentity()
			table.insert(mobile_eyeballs, lua)
		end
	end
	local static_eyeballs = core.find_nodes_in_area(pos1, pos2, {"group:eyeball"})
	return static_eyeballs, mobile_eyeballs
end

eb_levels.get_receiver_nodes_in_current_level = function()
	if not current_level then
		return nil
	end
	local level = levels[current_level]
	local pos1, pos2 = level.world_pos, vector.add(level.world_pos, level.size)
	local nodes = core.find_nodes_in_area(pos1, pos2, {"group:receiver"})
	return nodes
end


eb_levels.clear_level = function(level_num)
	local level = levels[level_num]
	local pos1, pos2 = level.world_pos, vector.add(level.world_pos, level.size)
	local objs = core.get_objects_in_area(pos1, pos2)
	for o=1, #objs do
		local obj = objs[o]
		if eb_eyeballs.object_is_eyeball(obj) or eb_floating_blocks.object_is_floating_block(obj) then
			obj:remove()
		end
	end
	local posses = {}
	for x=pos1.x, pos2.x do
	for y=pos1.y, pos2.y do
	for z=pos1.z, pos2.z do
		table.insert(posses, {x=x, y=y, z=z})
	end
	end
	end
	core.bulk_set_node(posses, {name="air"})
end

eb_levels.start_level = function(level_num, player)
	local level = levels[level_num]
	local emerge_callback = function(blockpos, action, calls_remaining, param)
		if calls_remaining > 0 then
			return
		end
		is_loading = false
		if action == core.EMERGE_FROM_MEMORY or action == core.EMERGE_FROM_DISK or action == core.EMERGE_GENERATED then
			eb_levels.clear_level(level_num)
			local ok = core.place_schematic(param.world_pos, core.get_modpath("eb_levels").."/schematics/"..param.schem, "0", {}, true)
			local spawn_pos
			local max_pos = vector.add(param.world_pos, param.size)
			local findnodes = core.find_nodes_in_area(param.world_pos, max_pos, {"group:start", "group:eyeball_mobile_spawner", "group:floating_block_spawner", "group:column_extend", "group:wall_extend", "group:exit", "group:graffiti"})
			local triggernodes = core.find_nodes_in_area(param.world_pos, max_pos, {"group:trigger"})
			local receivernodes = core.find_nodes_in_area(param.world_pos, max_pos, {"group:receiver"})

			local trigger_positions = {}
			for n=1, #triggernodes do
				local tpos = triggernodes[n]
				local tnode = core.get_node(tpos)
				local tdef = core.registered_nodes[tnode.name]
				local tid
				if tdef and core.get_item_group(tnode.name, "trigger") == 1 then
					if tdef.paramtype2 == "facedir" then
						tid = math.floor(tnode.param2 / 32)
					elseif tdef.paramtype2 == "wallmounted" then
						tid = math.floor(tnode.param2 / 8)
					elseif tdef.paramtype2 == "4dir" then
						tid = math.floor(tnode.param2 / 4)
					else
						tid = tnode.param2
					end
					core.log("verbose", "[eb_levels] Trigger id at "..core.pos_to_string(tpos).." is "..tid)
					if trigger_positions[tid] then
						core.log("error", "[eb_levels] Trigger id "..tid.." is used twice in level at "..core.pos_to_string(tpos).."!")
					end
					trigger_positions[tid] = tpos
				end
			end
			local touch_controls = false
			if param.player and param.player:is_player() then
				local info = core.get_player_window_information(param.player:get_player_name())
				if info and info.touch_controls then
					touch_controls = true
				end
			end
			for f=1, #findnodes do
				local fpos = findnodes[f]
				local fnode = core.get_node(fpos)
				if touch_controls and fnode.name == "eb_graffiti:wasd" then
					core.set_node(fpos, {name="eb_graffiti:chosen", param2=fnode.param2})
					core.log("action", "[eb_levels] 'WASD' graffiti replaced because player has touch controls")
				elseif core.get_item_group(fnode.name, "graffiti") == 1 then
					local fdef = core.registered_nodes[fnode.name]
					if fdef and fdef.on_construct then
						fdef.on_construct(fpos)
					end
				elseif core.get_item_group(fnode.name, "start") == 1 then
					spawn_pos = fpos
				elseif core.get_item_group(fnode.name, "eyeball_mobile_spawner") ~= 0 then
					local fdef = core.registered_nodes[fnode.name]
					local entname = fdef._eb_spawned_entity
					local obj = core.add_entity(fpos, entname)
					if obj then
						local ent = obj:get_luaentity()
						if ent then
							local eye_id = fnode.param2
							ent:_set_eye_id(eye_id)
							if core.get_item_group(fnode.name, "eyeball_mobile_spawner") == 1 then
								ent:_set_state(eb_eyeballs.MOBILE_EYEBALL_STATE_CLOSED)
							else
								ent:_set_state(eb_eyeballs.MOBILE_EYEBALL_STATE_OPEN)
							end
							if param.mobile_paths then
								local path = param.mobile_paths[eye_id]
								if path then
									ent:_set_path(path)
									if path.start_active then
										ent:_activate_path()
									end
								end
							end
							if param.mobile_rotations and param.mobile_rotations[eye_id] then
								obj:set_rotation(param.mobile_rotations[eye_id])
							end
							if param.mobile_autorots and param.mobile_autorots[eye_id] then
								ent:_activate_autorot()
							end
						end
					end
					core.remove_node(fpos)
				elseif core.get_item_group(fnode.name, "floating_block_spawner") ~= 0 then
					local fdef = core.registered_nodes[fnode.name]
					local entname = fdef._eb_spawned_floating_block
					local obj = core.add_entity(fpos, entname)
					if obj then
						local ent = obj:get_luaentity()
						if ent then
							local fb_id = fnode.param2
							if param.floating_blocks and param.floating_blocks[fb_id] then
								if param.floating_blocks[fb_id].rotation then
									obj:set_rotation(param.floating_blocks[fb_id].rotation)
								end
								if param.floating_blocks[fb_id].automatic_rotate then
									obj:set_properties({automatic_rotate=param.floating_blocks[fb_id].automatic_rotate})
								end
							end
						end
					end
					core.remove_node(fpos)
				elseif core.get_item_group(fnode.name, "column_extend") ~= 0 or core.get_item_group(fnode.name, "wall_extend") ~= 0 then
					extend_column(fpos)
				elseif core.get_item_group(fnode.name, "exit") ~= 0 then
					local fdef = core.registered_nodes[fnode.name]
					if fdef and fdef.on_construct then
						fdef.on_construct(fpos)
					end
				end
			end
			if param.triggers then
				for tid, trigger in pairs(param.triggers) do
					if trigger_positions[tid] then
						local tpos = trigger_positions[tid]
						local tmeta = core.get_meta(tpos)
						tmeta:set_string("eye_id", trigger.eye_id)
						if trigger.trigger_type == "path" then
							tmeta:set_int("trigger_type", 1)
						else
							tmeta:set_int("trigger_type", 0)
						end
						core.log("verbose", "[eb_levels] Trigger set: at "..core.pos_to_string(tpos))
					end
				end
			end
			if not spawn_pos then
				core.log("error", "[eb_levels] No start block in level schematic: "..param.schem)
				return
			end
			spawn_pos.y = spawn_pos.y + 0.501
			if ok == nil then
				core.log("error", "[eb_levels] Could not place level schematic: "..param.schem)
				return
			end
			if param.music then
				eb_music.play_track(param.music)
			else
				eb_music.stop_track()
			end
			eb_sky.set_sky(param.sky)
			eb_sky.set_cloud_height(param.cloud_height)
			eb_eyeballs.activate_static_eyeballs_in_area(param.world_pos, max_pos)
			if param.player and param.player:is_player() then
				eb_teleport.set_pos(param.player, spawn_pos)
				if param.spawn_yaw then
					param.player:set_look_horizontal(param.spawn_yaw)
				end
				param.player:set_look_vertical(0)
			end
			core.set_timeofday(param.timeofday)
			if param.player_must_be_seen == false then
				eb_eyeballs.deactivate_eyeball_fail()
			else
				eb_eyeballs.activate_eyeball_fail()
			end
			local pmeta = param.player:get_meta()
			pmeta:set_int("eb_levels:current_level", level_num)

			core.log("action", "[eb_levels] Level load emerge succeeded: "..param.schem)
		elseif action == core.EMERGE_CANCELLED then
			core.log("error", "[eb_levels] Level load emerge cancelled: "..param.schem)
		elseif action == core.EMERGE_ERRORED then
			core.log("error", "[eb_levels] Level load emerge errored: "..param.schem)
		end
	end
	eb_eyeballs.deactivate_eyeballs()
	eb_eyeballs.deactivate_eyeball_fail()
	is_loading = true
	current_level = level_num
	local minpos = table.copy(level.world_pos)
	minpos.y = minpos.y - EXTEND_COLUMN_HEIGHT - 1
	local maxpos = vector.add(level.world_pos, level.size)
	core.log("action", "[eb_levels] Emerging area between "..core.pos_to_string(minpos).." and "..core.pos_to_string(maxpos).." ...")
	core.emerge_area(minpos, maxpos, emerge_callback, {
		world_pos=level.world_pos,
		size=level.size,
		spawn_yaw=level.spawn_yaw,
		schem=level.schem,
		player=player,
		mobile_paths=level.mobile_paths,
		mobile_rotations=level.mobile_rotations,
		mobile_autorots=level.mobile_autorots,
		triggers=level.triggers,
		floating_blocks=level.floating_blocks,
		music=level.music,
		sky=level.sky,
		cloud_height=level.cloud_height,
		timeofday=level.timeofday,
		player_must_be_seen=level.player_must_be_seen,
	})
end

eb_levels.restart_level = function(player)
	if current_level then
		eb_levels.start_level(current_level, player)
	end
end

eb_eyeballs.register_on_eyeball_fail(function()
	local player = core.get_player_by_name("singleplayer")
	eb_levels.restart_level(player)
end)

local timer = 0
local exit_timer = 0
core.register_globalstep(function(dtime)
	if not current_level then
		return
	end
	timer = timer + dtime
	if timer < EXIT_CHECK_TIME then
		return
	end
	timer = 0
	local player = core.get_player_by_name("singleplayer")
	if not player then
		exit_timer = 0
		return
	end
	local ppos = player:get_pos()
	-- Fall out
	local pmeta = player:get_meta()
	if pmeta:get_int("eb_immortal:immortal") ~= 1 then
		if ppos.y < levels[current_level].world_pos.y - FALLOUT_HEIGHT then
			eb_transition.start_transition("death", 300)
			eb_levels.restart_level(player)
			exit_timer = 0
			return
		end
	end

	-- Level exit
	ppos.y = ppos.y - 0.9
	local footnode = core.get_node(ppos)
	if core.get_item_group(footnode.name, "exit") == 1 and not eb_teleport.is_teleporting() and eb_teleport.can_teleport(player) then
		local seen_good, seen_evil = eb_eyeballs.get_seen_counters()
		if (not eb_levels.player_must_be_seen()) or (seen_good and seen_good >= 1 and seen_evil == 0) then
			exit_timer = exit_timer + EXIT_CHECK_TIME + dtime
			if exit_timer >= EXIT_TIME then
				local next_level = current_level + 1
				if next_level > #levels then
					next_level = 1
				end
				core.sound_play({name="eb_levels_level_complete", gain=1}, {to_player=player:get_player_name()}, true)
				eb_hud.update_eyeball_counters(nil, nil)
				eb_hud.update_danger_meter(nil)
				eb_teleport.teleport_send(player, function(player)
					eb_transition.start_transition("teleport", 265)
					local spawn = eb_levels.start_level(next_level, player)
					exit_timer = 0
					return true
				end)
				return
			end
		else
			exit_timer = 0
		end
	else
		exit_timer = 0
	end
end)

core.register_chatcommand("level", {
	description = S("Go to level"),
	params = S("<level number>"),
	privs = { server = true },
	func = function(name, param)
		local player = core.get_player_by_name(name)
		if not player then
			return false, S("No player.")
		end
		if eb_editor.player_in_editor(player) then
			return false, S("Editor Mode is active; can’t go to level")
		end
		if eb_teleport.is_teleporting() then
			eb_teleport.abort_teleport(player)
		end
		local lvl = tonumber(param)
		if not lvl then
			return false
		end
		lvl = math.floor(lvl)
		if not levels[lvl] then
			return false, S("Level doesn’t exist!")
		end

		eb_levels.start_level(lvl, player)
		eb_transition.start_transition("teleport", 255)
		return true
	end,
})

core.register_on_joinplayer(function(player)
	if eb_editor.player_in_editor(player) then
		return
	end
	local pmeta = player:get_meta()
	local lvl = pmeta:get_int("eb_levels:current_level")
	if not levels[lvl] then
		lvl = 1
	end
	eb_levels.start_level(lvl, player)
end)

dofile(core.get_modpath("eb_levels").."/levels.lua")
