--[[
	We need a lot of information to determine which collisions to filter, but they are all
	condensed into one place to minimize redundant loops in case someone is spam-spawning
	characters with hundreds of limbs. Returns the following information:
	
	limbs:
	{
		["Arm"] = { LeftUpperArm, LeftLowerArm, LeftHand, RightUpperArm, RightLowerArm, RightHand},
		["Head"] = { Head },
		["Torso"] = { LowerTorso, UpperTorso },
		...	
	}
	
	limbRootParts:
	{ {Part=Head, Type=Head}, {Part=LeftUpperArm, Type="Arm"}, {Part=RightUpperArm, Type="Arm"}, ... }
	
	limbParents:
	{
		["Arm"] = { "Torso" },
		["Head"] = { "Torso" },
		["Torso"] = {},
		...	
	}
--]]
function getLimbs(characterRoot, attachmentMap)
	local limbs = {}
	local limbRootParts = {}
	local limbParents = {}

	local function parsePart(part, lastLimb)
		if part.Name ~= "HumanoidRootPart" then
			local limbType = getLimbType(part.Name)
			limbs[limbType] = limbs[limbType] or {}
			
			table.insert(limbs[limbType], part)
			
			local limbData = limbs[limbType]
			if limbType ~= lastLimb then
				limbParents[limbType] = limbParents[limbType] or {}
				if lastLimb then
					limbParents[limbType][lastLimb] = true
				end
				
				table.insert(limbRootParts, {Part=part, Type=limbType})
				lastLimb = limbType
			end
		end
		
		for _,v in pairs(part:GetChildren()) do
			if v:isA("Attachment") and attachmentMap[v.Name] then
				local part1 = attachmentMap[v.Name].Attachment1.Parent
				if part1 and part1 ~= part then
					parsePart(part1, lastLimb)
				end
			end
		end
	end
	parsePart(characterRoot)
	
	return limbs, limbRootParts, limbParents
end

function createNoCollision(part0, part1)
	local noCollision = Instance.new("NoCollisionConstraint")
	noCollision.Name = part0.Name.."<->"..part1.Name
	noCollision.Part0 = part0
	noCollision.Part1 = part1
	
	return noCollision
end

return function(attachmentMap, characterRoot)
	local noCollisionConstraints = Instance.new("Folder")
	noCollisionConstraints.Name = "NoCollisionConstraints"
	
	local limbs, limbRootParts, limbParents = getLimbs(characterRoot, attachmentMap)
	
	--[[
		Disable collisions between all limb roots (e.g. left/right shoulders, head, etc) unless
		one of them is a parent of the other (handled in next step). This is to ensure limbs
		maintain free range of motion and we don't have issues like
			- Large shoulders clipping with the head and all of them being unable to move
			- Legs being stuck together because rotating would cause the collision boxes to intersect
	--]]
	for i=1, #limbRootParts do
		for j=i+1, #limbRootParts do
			local limbType0, limbType1 = limbRootParts[i].Type, limbRootParts[j].Type

			if not (limbParents[limbType0][limbType1] or limbParents[limbType1][limbType0]) then
				createNoCollision(limbRootParts[i].Part, limbRootParts[j].Part).Parent = noCollisionConstraints
			end
		end
	end

	--[[
		Disable collisions between limbs and their parent limbs. This is mostly to address
		bundle torsos having insane hitboxes that touch more than just limb roots
	--]]
	for limbType, parts in pairs(limbs) do
		for parentLimbType,_ in pairs(limbParents[limbType]) do
			for _,part2 in pairs(limbs[parentLimbType]) do
				for _,part in pairs(parts) do
					createNoCollision(part, part2).Parent = noCollisionConstraints
				end
			end
		end
	end
	
	return noCollisionConstraints
end