SpeedTracking = {}
TpsTracking = {}

function entity_speed(uuid, seconds)
	if not seconds then
		seconds = 1
	end

	local callback = function()
		local old_entity = SpeedTracking[uuid]
		local new_entity = client:find_entities(function(e)
			return e.uuid == uuid
		end)[1]

		if not new_entity then
			remove_listeners("tick", "speed-tracking_" .. uuid)
			SpeedTracking[uuid] = -1
			return
		end

		if old_entity then
			old_entity._distance = old_entity._distance + distance(old_entity.position, new_entity.position)
			old_entity.position = new_entity.position

			if old_entity._ticks < seconds * 20 then
				old_entity._ticks = old_entity._ticks + 1
			else
				remove_listeners("tick", "speed-tracking_" .. uuid)
				SpeedTracking[uuid] = old_entity._distance / seconds
			end
		else
			new_entity._ticks = 1
			new_entity._distance = 0
			SpeedTracking[uuid] = new_entity
		end
	end
	add_listener("tick", callback, "speed-tracking_" .. uuid)

	repeat
		sleep(seconds * 1000 / 10)
	until type(SpeedTracking[uuid]) == "number"

	local speed = SpeedTracking[uuid]
	SpeedTracking[uuid] = nil
	return speed
end

function tps(seconds)
	if not seconds then
		seconds = 1
	end

	add_listener("tick", function()
		if not TpsTracking.ticks then
			TpsTracking.ticks = 0
			TpsTracking.start = clock_gettime(0)
		else
			TpsTracking.ticks = TpsTracking.ticks + 1
			if TpsTracking.ticks >= seconds * 20 then
				TpsTracking.stop = clock_gettime(0)
				remove_listeners("tick", "tps_tracking")
			end
		end
	end, "tps_tracking")

	sleep(seconds * 1000)
	repeat
		sleep(20)
	until TpsTracking.stop

	local tps = seconds * 20 / (TpsTracking.stop - TpsTracking.start)
	TpsTracking = {}
	return tps
end

function nether_travel(pos, go_to_opts)
	info(string.format("going to %.2f %.2f %.2f through nether", pos.x, pos.y, pos.z))

	local portal_block_states = get_block_states({ "nether_portal" })
	local nether_pos = table.shallow_copy(pos)
	nether_pos.x = nether_pos.x / 8
	nether_pos.z = nether_pos.z / 8

	if client.dimension == "minecraft:overworld" then
		info("currently in overworld, finding nearest portal")
		local portals = client:find_blocks(client.position, portal_block_states)

		info(string.format("going to %.2f %.2f %.2f through nether", portals[1].x, portals[1].y, portals[1].z))
		client:go_to(portals[1], go_to_opts)
		while client.dimension ~= "minecraft:the_nether" do
			sleep(1000)
		end
		sleep(3000)
	end

	info(string.format("currently in nether, going to %.2f %.2f", nether_pos.x, nether_pos.z))
	client:go_to(nether_pos, { type = XZ_GOAL })
	while client.pathfinder.is_calculating or client.pathfinder.is_executing do
		sleep(1000)
	end

	info("arrived, looking for nearest portal")
	local portals_nether = client:find_blocks(client.position, portal_block_states)
	if not next(portals_nether) then
		warn("failed to find portals in the nether")
		return
	end

	local found_portal = false
	for _, portal in ipairs(portals_nether) do
		if (client.position.y > 127) == (portal.y > 127) then
			found_portal = true

			info(string.format("found valid portal, going to %.2f %.2f %.2f", portal.x, portal.y, portal.z))
			client:go_to(portal)
			while client.dimension ~= "minecraft:overworld" do
				sleep(1000)
			end
			sleep(3000)
		end

		if found_portal then
			break
		end
	end
	if not found_portal then
		warn("failed to find valid portals in the nether")
		return
	end

	info(string.format("back in overworld, going to %.2f %.2f %.2f", pos.x, pos.y, pos.z))
	client:go_to(pos, go_to_opts)
end

function interact_bed()
	local bed = client:find_blocks(
		client.position,
		get_block_states({
			"brown_bed",
			"white_bed",
			"yellow_bed",
		})
	)[1]
	if not bed then
		return
	end

	client:go_to(bed, { type = REACH_BLOCK_POS_GOAL, options = { without_mining = true } })
	while client.pathfinder.is_calculating or client.pathfinder.is_executing do
		sleep(500)
	end

	client:look_at(bed)
	client:block_interact(bed)
end

function closest_entity(target_kind)
	local self_pos = client.position
	local entities = client:find_entities(function(e)
		return e.kind == target_kind
	end)

	local closest_entity = entities[1]
	local closest_distance = distance(closest_entity.position, self_pos)
	for _, entity in ipairs(entities) do
		local dist = distance(entity.position, self_pos)
		if dist <= closest_distance then
			closest_entity = entity
			closest_distance = dist
		end
	end
	return closest_entity
end

function get_player(name)
	local target_uuid = nil
	for _, player in ipairs(client.tab_list) do
		if player.name == name then
			target_uuid = player.uuid
			break
		end
	end

	return client:find_entities(function(e)
		return e.kind == "minecraft:player" and e.uuid == target_uuid
	end)[1]
end

function distance(p1, p2)
	return math.sqrt((p2.x - p1.x) ^ 2 + (p2.y - p1.y) ^ 2 + (p2.z - p1.z) ^ 2)
end

function dump(object)
	if type(object) == "table" then
		local string = "{ "
		local parts = {}
		for key, value in pairs(object) do
			table.insert(parts, key .. " = " .. dump(value))
		end
		string = string .. table.concat(parts, ", ")
		return string .. " " .. "}"
	else
		return tostring(object)
	end
end

function dump_pretty(object, level)
	if not level then
		level = 0
	end
	if type(object) == "table" then
		local string = "{\n" .. string.rep("  ", level + 1)
		local parts = {}
		for key, value in pairs(object) do
			table.insert(parts, key .. " = " .. dump_pretty(value, level + 1))
		end
		string = string .. table.concat(parts, ",\n" .. string.rep("  ", level + 1))
		return string .. "\n" .. string.rep("  ", level) .. "}"
	else
		return tostring(object)
	end
end