-- Very inefficient, but provides all available info
function Util.debugPrint(tbl, indent)
	local out = ""

	indent = indent or "  "
	local vStr
	local kStr

	for k,v in pairs(tbl) do
		local t = type(v)

		local kConversionSuccess, kConversionResult = pcall(function() return tostring(k) end)
		if kConversionSuccess then
			kStr = kConversionResult
		else
			kStr = "[" .. type(k) .. "]"
		end

		if t == "table" then
			vStr = string.format("\n%s%s", "", Util.debugPrint(v, indent .. "  "))
		else
			local vConversionSuccess, vConversionResult = pcall(function() return tostring(v) end)
			if vConversionSuccess then
				vStr = vConversionResult
			else
				vStr = "[" .. type(v) .. "]"
			end
		end

		out = string.format("%s%s%s: %s\n", out, indent, kStr, vStr)
	end

	return out
end

function Util.tableToString(tbl, indent)
	local out = ""

	indent = indent or "  "
	local vStr

	for k,v in pairs(tbl) do
		local t = type(v)

		if t == "table" then
			vStr = string.format("\n%s%s", "", Util.tableToString(v, indent .. "  "))

		elseif t == "nil" then
			vStr = "[nil]"

		elseif t == "string" then
			vStr = string.format("\"%s\"", v)

		elseif t == "boolean" then
			vStr = Util.boolToString(v)

		elseif t == "userdata" then
			vStr = "[USERDATA]"

		elseif t == "function" then
			vStr = "[FUNCTION]"

		else
			vStr = v
		end

		k = tostring(k)

		out = string.format("%s%s%s: %s\n", out, indent, k, vStr)
	end

	return out
end

function Util.newRemoteEvent(name, parent)
	local existing = parent:FindFirstChild(name)

	if existing then
		Log.Warn.Util("RemoteEvent %s under %s already exists", name, parent.Name)

		return existing
	end

	local event = Instance.new("RemoteEvent", parent)
	event.Name = name

	return event
end

function Util.isPlayerInGroup(player, groups)
	for _, groupId in ipairs(groups) do
		if player:IsInGroup(groupId) then
			return true
		end
	end
end

function Util.isPlayerIDInTable(player, ids)
	for _, id in ipairs(ids) do
		if player.UserId == id then
			return true
		end
	end
end

function Util.lerp(start, stop, t)
	return start * (1 - t) + stop * t
end

function Util.getClientFocus()
	return CollectionService:GetTagged(Util._clientFocusTag)[1]
end

function Util.setClientFocus(instance)
	if RunService:IsClient() then
		-- Remove any tags
		for _, obj in ipairs(CollectionService:GetTagged(Util._clientFocusTag)) do
			CollectionService:RemoveTag(obj, Util._clientFocusTag)
		end

		-- Tag the specified object
		CollectionService:AddTag(instance, Util._clientFocusTag)
	end
end

Util.highestBotNum = 150
function Util.playerIsBot(player)
	local numOnEnd = tonumber(string.sub(player.Name, 11))
	if numOnEnd ~= nil and
	   string.len(player.Name) > 10 and
	   string.sub(player.Name, 1, 10) == "rbxreftest" and
	   numOnEnd <= Util.highestBotNum and
	   numOnEnd >= 1
	then
		return true
	end
	return false
end

function Util.playerIsAlive(player)
	if player and player.Character then
		local humanoid = player.Character:FindFirstChildOfClass("Humanoid")
		if humanoid and humanoid:GetState() ~= Enum.HumanoidStateType.Dead then
			return true
		end
	end
	return false
end

function Util.ancestorHasTag(instance, tag)
	local currentInstance = instance
	while currentInstance do
		if CollectionService:HasTag(currentInstance, tag) then
			return true
		elseif not currentInstance.Parent then
			return false
		else
			currentInstance = currentInstance.Parent
		end
	end

	return false
end

function Util.getTableKeys(tbl)
	local keys = {}

	for k in pairs(tbl) do
		table.insert(keys, k)
	end

	return keys
end

function Util.loadCharacter(player)
	if Conf.force_rthro then
		local avatars = Conf.avatarOutfits
		local effectiveId = player.UserId
		local outfitId = avatars[(effectiveId % (#avatars - 1)) + 1]

		local playerDesc = Players:GetHumanoidDescriptionFromUserId(player.UserId)
		local desc = Players:GetHumanoidDescriptionFromOutfitId(outfitId)

		-- Load animations from configuration
		for property, value in pairs(Conf.avatarAnimations) do
			desc[property] = value
		end

		local emotes = playerDesc:GetEquippedEmotes()
		desc:SetEquippedEmotes(emotes)

		player:LoadCharacterWithHumanoidDescription(desc)
	else
		player:LoadCharacter()
	end
end