local function m(m,W,h,o)debug.profilebegin("\113\117\101\114\121\080\111\105\110\116")local D=#blacklist h=h+nearPlaneZ local s=m+W*h local p=inf local c=inf local Q=m repeat local D,K=workspace:FindPartOnRayWithIgnoreList(ray(Q,s-Q),blacklist,false,true)if D then if canOcclude(D)then local W={D}local Q=workspace:FindPartOnRayWithWhitelist(ray(s,K-s),W,true)local t=(K-m).Magnitude if Q then local m=false if o then m=workspace:FindPartOnRayWithWhitelist(ray(o,s-o),W,true)or workspace:FindPartOnRayWithWhitelist(ray(s,o-s),W,true)end if m then c=t elseif h<p then p=t end else c=t end end blacklist[#blacklist+1]=D Q=K-W*.001 end until c<inf or not D eraseFromEnd(blacklist,D)debug.profileend()return p-nearPlaneZ,c-nearPlaneZ end local function W(W,h)debug.profilebegin("\113\117\101\114\121\086\105\101\119\112\111\114\116")local o=W.p local D=W.rightVector local s=W.upVector local p=-W.lookVector local c=camera.ViewportSize local Q=inf local K=inf for W=0,1,1 do local t=D*((W-.5)*projX)for D=0,1,1 do local g=s*((D-.5)*projY)local Y=o+nearPlaneZ*(t+g)local k=(camera:ViewportPointToRay(c.x*W,c.y*D)).Origin local a,f=m(Y,p,h,k)if f<Q then Q=f end if a<K then K=a end end end debug.profileend()return K,Q end local function h(W,h,o)debug.profilebegin("\116\101\115\116\080\114\111\109\111\116\105\111\110")local D=W.p local s=W.rightVector local p=W.upVector local c=-W.lookVector do debug.profilebegin("\101\120\116\114\097\112\111\108\097\116\101")local W=.0625 local s=1.25 local p=(getCollisionPoint(D,o.posVelocity*s)-D).Magnitude local c=o.posVelocity.magnitude for W=0,min(s,o.rotVelocity.magnitude+p/c),W do local D=o.extrapolate(W)if m(D.p,-D.lookVector,h)>=h then return false end end debug.profileend()end do debug.profilebegin("\116\101\115\116\079\102\102\115\101\116\115")for W,o in ipairs(SCAN_SAMPLE_OFFSETS)do local Q=o local K,t=getCollisionPoint(D,s*Q.x+p*Q.y)if m(K,((D+c*h)-K).Unit,h)==inf then return false end end debug.profileend()end debug.profileend()return true end local function o(m,o,D)debug.profilebegin("\112\111\112\112\101\114")subjectRoot=subjectPart and subjectPart:GetRootPart()or subjectPart local s=o local p,c=W(m,o)if c<s then s=c end if p<s and h(m,o,D)then s=p end subjectRoot=nil debug.profileend()return s end return o


local function queryPoint(origin, unitDir, dist, lastPos)
	debug.profilebegin('queryPoint')

	local originalSize = #blacklist

	dist = dist + nearPlaneZ
	local target = origin + unitDir*dist

	local softLimit = inf
	local hardLimit = inf
	local movingOrigin = origin

	repeat
		local entryPart, entryPos = workspace:FindPartOnRayWithIgnoreList(ray(movingOrigin, target - movingOrigin), blacklist, false, true)

		if entryPart then
			if canOcclude(entryPart) then
				local wl = {entryPart}
				local exitPart = workspace:FindPartOnRayWithWhitelist(ray(target, entryPos - target), wl, true)

				local lim = (entryPos - origin).Magnitude

				if exitPart then
					local promote = false
					if lastPos then
						promote =
							workspace:FindPartOnRayWithWhitelist(ray(lastPos, target - lastPos), wl, true) or
							workspace:FindPartOnRayWithWhitelist(ray(target, lastPos - target), wl, true)
					end

					if promote then
						-- Ostensibly a soft limit, but the camera has passed through it in the last frame, so promote to a hard limit.
						hardLimit = lim
					elseif dist < softLimit then
						-- Trivial soft limit
						softLimit = lim
					end
				else
					-- Trivial hard limit
					hardLimit = lim
				end
			end

			blacklist[#blacklist + 1] = entryPart
			movingOrigin = entryPos - unitDir*1e-3
		end
	until hardLimit < inf or not entryPart

	eraseFromEnd(blacklist, originalSize)

	debug.profileend()
	return softLimit - nearPlaneZ, hardLimit - nearPlaneZ
end

local function queryViewport(focus, dist)
	debug.profilebegin('queryViewport')

	local fP =  focus.p
	local fX =  focus.rightVector
	local fY =  focus.upVector
	local fZ = -focus.lookVector

	local viewport = camera.ViewportSize

	local hardBoxLimit = inf
	local softBoxLimit = inf

	-- Center the viewport on the PoI, sweep points on the edge towards the target, and take the minimum limits
	for viewX = 0, 1 do
		local worldX = fX*((viewX - 0.5)*projX)

		for viewY = 0, 1 do
			local worldY = fY*((viewY - 0.5)*projY)

			local origin = fP + nearPlaneZ*(worldX + worldY)
			local lastPos = camera:ViewportPointToRay(
				viewport.x*viewX,
				viewport.y*viewY
			).Origin

			local softPointLimit, hardPointLimit = queryPoint(origin, fZ, dist, lastPos)

			if hardPointLimit < hardBoxLimit then
				hardBoxLimit = hardPointLimit
			end
			if softPointLimit < softBoxLimit then
				softBoxLimit = softPointLimit
			end
		end
	end
	debug.profileend()

	return softBoxLimit, hardBoxLimit
end

local function testPromotion(focus, dist, focusExtrapolation)
	debug.profilebegin('testPromotion')

	local fP = focus.p
	local fX = focus.rightVector
	local fY = focus.upVector
	local fZ = -focus.lookVector

	do
		-- Dead reckoning the camera rotation and focus
		debug.profilebegin('extrapolate')

		local SAMPLE_DT = 0.0625
		local SAMPLE_MAX_T = 1.25

		local maxDist = (getCollisionPoint(fP, focusExtrapolation.posVelocity*SAMPLE_MAX_T) - fP).Magnitude
		-- Metric that decides how many samples to take
		local combinedSpeed = focusExtrapolation.posVelocity.magnitude

		for dt = 0, min(SAMPLE_MAX_T, focusExtrapolation.rotVelocity.magnitude + maxDist/combinedSpeed), SAMPLE_DT do
			local cfDt = focusExtrapolation.extrapolate(dt) -- Extrapolated CFrame at time dt

			if queryPoint(cfDt.p, -cfDt.lookVector, dist) >= dist then
				return false
			end
		end

		debug.profileend()
	end

	do
		-- Test screen-space offsets from the focus for the presence of soft limits
		debug.profilebegin('testOffsets')

		for _, offset in ipairs(SCAN_SAMPLE_OFFSETS) do
			local scaledOffset = offset
			local pos, isHit = getCollisionPoint(fP, fX*scaledOffset.x + fY*scaledOffset.y)
			if queryPoint(pos, (fP + fZ*dist - pos).Unit, dist) == inf then
				return false
			end
		end

		debug.profileend()
	end

	debug.profileend()
	return true
end

local function Popper(focus, targetDist, focusExtrapolation)
	debug.profilebegin('popper')

	subjectRoot = subjectPart and subjectPart:GetRootPart() or subjectPart

	local dist = targetDist
	local soft, hard = queryViewport(focus, targetDist)
	if hard < dist then
		dist = hard
	end
	if soft < dist and testPromotion(focus, targetDist, focusExtrapolation) then
		dist = soft
	end

	subjectRoot = nil

	debug.profileend()
	return dist
end

return Popper
