-----------------------------------PATHER--------------------------------------

local popupAdornee
local function getPopupAdorneePart()
	--Handle the case of the adornee part getting deleted (camera changed, maybe)
	if popupAdornee and not popupAdornee.Parent then
		popupAdornee = nil
	end
	
	--If the adornee doesn't exist yet, create it
	if not popupAdornee then
		popupAdornee = Instance.new("Part")		
		popupAdornee.Name = "ClickToMovePopupAdornee"
		popupAdornee.Transparency = 1
		popupAdornee.CanCollide = false
		popupAdornee.Anchored = true
		popupAdornee.Size = Vector3.new(2, 2, 2)
		popupAdornee.CFrame = CFrame.new()
		
		popupAdornee.Parent = workspace.CurrentCamera
	end
	
	return popupAdornee
end

local activePopups = {}
local function createNewPopup(popupType)
	local newModel = Instance.new("ImageHandleAdornment")
	
	newModel.AlwaysOnTop = false
	newModel.Transparency = 1
	newModel.Size = ZERO_VECTOR2
	newModel.SizeRelativeOffset = ZERO_VECTOR3
	newModel.Image = "rbxasset://textures/ui/move.png"
	newModel.ZIndex = 20
	
	local radius = 0
	if popupType == "DestinationPopup" then
		newModel.Color3 = Color3.fromRGB(0, 175, 255)
		radius = 1.25
	elseif popupType == "DirectWalkPopup" then
		newModel.Color3 = Color3.fromRGB(0, 175, 255)
		radius = 1.25
	elseif popupType == "FailurePopup" then
		newModel.Color3 = Color3.fromRGB(255, 100, 100)
		radius = 1.25
	elseif popupType == "PatherPopup" then
		newModel.Color3 = Color3.fromRGB(255, 255, 255)
		radius = 1
		newModel.ZIndex = 10
	end
	newModel.Size = Vector2.new(5, 0.1) * radius
	
	local dataStructure = {}
	dataStructure.Model = newModel	
	
	activePopups[#activePopups + 1] = newModel
	
	function dataStructure:TweenIn()
		local tweenInfo = TweenInfo.new(1.5, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out)
		local tween1 = TweenService:Create(newModel, tweenInfo, { Size = Vector2.new(2,2) * radius })
		tween1:Play()
		TweenService:Create(newModel, TweenInfo.new(0.25, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, 0, false, 0.1), { Transparency = 0, SizeRelativeOffset = Vector3.new(0, radius * 1.5, 0) }):Play()
		return tween1
	end
	
	function dataStructure:TweenOut()
		local tweenInfo = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.In)
		local tween1 = TweenService:Create(newModel, tweenInfo, { Size = ZERO_VECTOR2 })
		tween1:Play()
		
		coroutine.wrap(function()
			tween1.Completed:Wait()
			
			for i = 1, #activePopups do
				if activePopups[i] == newModel then
					table.remove(activePopups, i)
					break
				end
			end
		end)()
		return tween1
	end
	
	function dataStructure:Place(position, dest)
		-- place the model at position
		if not self.Model.Parent then
			local popupAdorneePart = getPopupAdorneePart()
			self.Model.Parent = popupAdorneePart
			self.Model.Adornee = popupAdorneePart

			--Start the 10-stud long ray 2.5 studs above where the tap happened and point straight down to try to find
			--the actual ground position.
			local ray = Ray.new(position + Vector3.new(0, 2.5, 0), Vector3.new(0, -10, 0))
			local hitPart, hitPoint, hitNormal = workspace:FindPartOnRayWithIgnoreList(ray, { workspace.CurrentCamera, Player.Character })
			
			self.Model.CFrame = CFrame.new(hitPoint) + Vector3.new(0, -radius,0)
		end
	end
	
	return dataStructure
end

local function createPopupPath(points, numCircles)
	-- creates a path with the provided points, using the path and number of circles provided
	local popups = {}
	local stopTraversing = false
	
	local function killPopup(i)
		-- kill all popups before and at i
		for iter, v in pairs(popups) do
			if iter <= i then
				local tween = v:TweenOut()
				spawn(function()
					tween.Completed:Wait()
					v.Model:Destroy()
				end)
				popups[iter] = nil
			end
		end
	end	
	
	local function stopFunction()
		stopTraversing = true
		killPopup(#points)
	end
	
	spawn(function()
		for i = 1, #points do
			if stopTraversing then
				break
			end

			local includeWaypoint = i % numCircles == 0
			                    and i < #points
			                    and (points[#points].Position - points[i].Position).magnitude > 4
			if includeWaypoint then
				local popup = createNewPopup("PatherPopup")
				popups[i] = popup
				local nextPopup = points[i+1]
				popup:Place(points[i].Position, nextPopup and nextPopup.Position or points[#points].Position)
				local tween = popup:TweenIn()
				wait(0.2)
			end
		end
	end)
	
	return stopFunction, killPopup
end

local function Pather(character, endPoint, surfaceNormal)
	local this = {}

	this.Cancelled = false
	this.Started = false

	this.Finished = Instance.new("BindableEvent")
	this.PathFailed = Instance.new("BindableEvent")
	
	this.PathComputing = false
	this.PathComputed = false
	
	this.TargetPoint = endPoint
	this.TargetSurfaceNormal = surfaceNormal
	
	this.DiedConn = nil
	this.SeatedConn = nil
	this.MoveToConn = nil
	this.BlockedConn = nil
	this.CurrentPoint = 0

	function this:Cleanup()
		if this.stopTraverseFunc then
			this.stopTraverseFunc()
			this.stopTraverseFunc = nil
		end

		if this.MoveToConn then
			this.MoveToConn:Disconnect()
			this.MoveToConn = nil
		end

		if this.BlockedConn then
			this.BlockedConn:Disconnect()
			this.BlockedConn = nil
		end

		if this.DiedConn then
			this.DiedConn:Disconnect()
			this.DiedConn = nil
		end

		if this.SeatedConn then
			this.SeatedConn:Disconnect()
			this.SeatedConn = nil
		end

		this.humanoid = nil
	end

	function this:Cancel()
		this.Cancelled = true
		this:Cleanup()
	end

	function this:OnPathInterrupted()
		-- Stop moving
		this.Cancelled = true
		this:OnPointReached(false)
	end

	function this:ComputePath()
		local humanoid = findPlayerHumanoid(Player)
		local torso = humanoid and humanoid.Torso
		local success = false
		if torso then
			if this.PathComputed or this.PathComputing then return end
			this.PathComputing = true
			success = pcall(function()
				this.pathResult = PathfindingService:FindPathAsync(torso.CFrame.p, this.TargetPoint)
			end)
			this.pointList = this.pathResult and this.pathResult:GetWaypoints()
			if this.pathResult then
				this.BlockedConn = this.pathResult.Blocked:Connect(function(blockedIdx) this:OnPathBlocked(blockedIdx) end)
			end
			this.PathComputing = false
			this.PathComputed = this.pathResult and this.pathResult.Status == Enum.PathStatus.Success or false
		end
		return true
	end

	function this:IsValidPath()
		if not this.pathResult then
			this:ComputePath()
		end
		return this.pathResult.Status == Enum.PathStatus.Success
	end

	this.Recomputing = false
	function this:OnPathBlocked(blockedWaypointIdx)
		local pathBlocked = blockedWaypointIdx >= this.CurrentPoint
		if not pathBlocked or this.Recomputing then
			return
		end

		this.Recomputing = true

		if this.stopTraverseFunc then
			this.stopTraverseFunc()
			this.stopTraverseFunc = nil
		end

		this.pathResult:ComputeAsync(this.humanoid.Torso.CFrame.p, this.TargetPoint)
		this.pointList = this.pathResult:GetWaypoints()
		this.PathComputed = this.pathResult and this.pathResult.Status == Enum.PathStatus.Success or false

		if SHOW_PATH then
			this.stopTraverseFunc, this.setPointFunc = createPopupPath(this.pointList, 4, true)
		end
		if this.PathComputed then
			this.humanoid = findPlayerHumanoid(Player)
			this.CurrentPoint = 1 -- The first waypoint is always the start location. Skip it.
			this:OnPointReached(true) -- Move to first point
		else
			this.PathFailed:Fire()
			this:Cleanup()
		end

		this.Recomputing = false
	end

	function this:OnPointReached(reached)

		if reached and not this.Cancelled then

			local nextWaypointIdx = this.CurrentPoint + 1

			if nextWaypointIdx > #this.pointList then
				-- End of path reached
				if this.stopTraverseFunc then
					this.stopTraverseFunc()
				end
				this.Finished:Fire()
				this:Cleanup()
			else
				local currentWaypoint = this.pointList[this.CurrentPoint]
				local nextWaypoint = this.pointList[nextWaypointIdx]

				-- If airborne, only allow to keep moving
				-- if nextWaypoint.Action ~= Jump, or path mantains a direction
				-- Otherwise, wait until the humanoid gets to the ground
				local currentState = this.humanoid:GetState()
				local isInAir = currentState == Enum.HumanoidStateType.FallingDown
					or currentState == Enum.HumanoidStateType.Freefall
					or currentState == Enum.HumanoidStateType.Jumping
				
				if isInAir then
					local shouldWaitForGround = nextWaypoint.Action == Enum.PathWaypointAction.Jump
					if not shouldWaitForGround and this.CurrentPoint > 1 then
						local prevWaypoint = this.pointList[this.CurrentPoint - 1]

						local prevDir = currentWaypoint.Position - prevWaypoint.Position
						local currDir = nextWaypoint.Position - currentWaypoint.Position

						local prevDirXZ = Vector2.new(prevDir.x, prevDir.z).Unit
						local currDirXZ = Vector2.new(currDir.x, currDir.z).Unit

						local THRESHOLD_COS = 0.996 -- ~cos(5 degrees)
						shouldWaitForGround = prevDirXZ:Dot(currDirXZ) < THRESHOLD_COS
					end

					if shouldWaitForGround then
						this.humanoid.FreeFalling:Wait()

						-- Give time to the humanoid's state to change
						-- Otherwise, the jump flag in Humanoid
						-- will be reset by the state change
						wait(0.1)
					end
				end

				-- Move to the next point
				if this.setPointFunc then
					this.setPointFunc(nextWaypointIdx)
				end
				
				if nextWaypoint.Action == Enum.PathWaypointAction.Jump then
					this.humanoid.Jump = true
				end
				this.humanoid:MoveTo(nextWaypoint.Position)

				this.CurrentPoint = nextWaypointIdx
			end
		else
			this.PathFailed:Fire()
			this:Cleanup()
		end
	end

	function this:Start()
		if CurrentSeatPart then
			return
		end
		
		this.humanoid = findPlayerHumanoid(Player)
		if not this.humanoid then
			this.PathFailed:Fire()
			return
		end

		if this.Started then return end
		this.Started = true
		
		if SHOW_PATH then
			-- choose whichever one Mike likes best
			this.stopTraverseFunc, this.setPointFunc = createPopupPath(this.pointList, 4)
		end

		if #this.pointList > 0 then
			this.SeatedConn = this.humanoid.Seated:Connect(function(reached) this:OnPathInterrupted() end)
			this.DiedConn = this.humanoid.Died:Connect(function(reached) this:OnPathInterrupted() end)
			this.MoveToConn = this.humanoid.MoveToFinished:Connect(function(reached) this:OnPointReached(reached) end)

			this.CurrentPoint = 1 -- The first waypoint is always the start location. Skip it.
			this:OnPointReached(true) -- Move to first point
		else
			this.PathFailed:Fire()
			if this.stopTraverseFunc then
				this.stopTraverseFunc()
			end
		end
	end
	
	this:ComputePath()
	if not this.PathComputed then
		-- set the end point towards the camera and raycasted towards the ground in case we hit a wall
		local offsetPoint = this.TargetPoint + this.TargetSurfaceNormal*1.5
		local ray = Ray.new(offsetPoint, Vector3.new(0,-1,0)*50)
		local newHitPart, newHitPos = RayCastIgnoreList(workspace, ray, getIgnoreList())
		if newHitPart then
			this.TargetPoint = newHitPos
		end
		-- try again
		this:ComputePath()
	end
	
	return this
end