-- This function assumes the character exists when it's called and has default behavior,
-- i.e. developer is not parenting the character somewhere manually
function CharacterLoadedWrapper._waitForLoadedAsync(self: ClassType, character: Model)
	if not self:isLoaded() then
		-- Wait until the character is in Workspace so that the following conditions don't skip running.
		-- If the character is destroyed after this and never parented, :Wait() never resumes, causing the thread
		-- to yield forever, preventing the character from being garbage collected. Bug filed here:
		-- TODO: Add link to bug report
		if not character:IsDescendantOf(Workspace) then
			-- The character should be in workspace after waiting. However, deferred events may
			-- cause a character to get parented in and then back out before this :Wait() resumes,
			-- so we still need to check character.Parent later
			character.AncestryChanged:Wait()
		end

		-- Check character.Parent to avoid starting a :Wait() if the character is already destroyed
		-- which would keep the character from being garbage collected
		if character.Parent then
			if not isPrimaryPartSet(character) then
				character:GetPropertyChangedSignal("PrimaryPart"):Wait()
			end

			local humanoidMaybe = getHumanoid(character)
			if not humanoidMaybe then
				humanoidMaybe = waitForChildOfClassAsync(character, "Humanoid") :: Humanoid
			end

			local humanoid = humanoidMaybe :: Humanoid
			while not isHumanoidRootPartSet(humanoid) do
				humanoid.Changed:Wait() -- GetPropertyChangedSignal doesn't fire for RootPart, so we rely on .Changed
			end
		end

		-- Verify the character hasn't been destroyed and that no loaded criteria has become un-met.
		-- For example, the Humanoid being destroyed before the PrimaryPart was set
		if not self:isLoaded(character) then
			return
		end
	end

	-- Make sure this class wasn't destroyed while waiting for conditions to be true
	if self._destroyed then
		return
	end

	self:_listenForDeath(character)
	self.loaded:Fire(character)
end

function CharacterLoadedWrapper._listenForDeath(self: ClassType, character: Model)
	-- Debounce to prevent deferred events from letting .died event fire more than once,
	-- such as if the humanoid dies and the character is destroyed in the same compute cycle.
	-- With deferred events, that would fire both events on the next cycle, even if the connection
	-- is disconnected within the response to the event.
	local humanoid = getHumanoid(character) :: Humanoid

	local alreadyDied = false
	local diedConnection, removedConnection

	local function onDied()
		if alreadyDied then
			return
		end
		alreadyDied = true

		diedConnection:Disconnect()
		removedConnection:Disconnect()
		self.died:Fire(character)
	end

	diedConnection = humanoid.Died:Connect(onDied)

	-- .Destroying event would be preferred, but :LoadCharacter() just removes
	-- the character instead of destroying it. As long as that is the case, we
	-- can't use .Destroying to cover all edge cases.
	removedConnection = character.AncestryChanged:Connect(function()
		if not character:IsDescendantOf(Workspace) then
			onDied()
		end
	end)

	self._connections:add(diedConnection, removedConnection)
end

function CharacterLoadedWrapper.destroy(self: ClassType)
	self.loaded:DisconnectAll()
	self.died:DisconnectAll()
	self._destroyed = true

	self._connections:disconnect()
end

return CharacterLoadedWrapper