-- Construct skydive button indicator GUI
local function createIndicatorImage(imageId)
	local image = Instance.new("ImageLabel")
	image.Image = imageId
	image.BackgroundTransparency = 1
	image.Size = UDim2.new(0, 100, 0, 100)
	return image
end

local Skydiver = {}
Skydiver.__index = Skydiver

function Skydiver:setOnDisabled(cb)
	self.onDisabled = cb
end

function Skydiver.new()
	local self = setmetatable({}, Skydiver)

	self.enabled = false
	self.enabledTime = 0

	self.straightDive = false

	self.character = nil
	self.humanoid = nil
	self.humanoidRootPart = nil

	self.autoDeploy = true
	self.onlyAutoDeployOnce = true

	self.waitingForJump = false
	self.didJump = false
	self.Jumped = Instance.new("BindableEvent")

	self.freefallAnimation = nil
	self.fallAnimationAsset = Instance.new("Animation")
	self.fallAnimationAsset.AnimationId = "rbxassetid://616087089"

	self.onClientEventConn = nil

	--[[
		Character management
	]]

	local character = localPlayer.Character
	if character then
		self:onCharacterAdded(character)
	end

	localPlayer.CharacterAdded:Connect(function(char)
		self:onCharacterAdded(char)
	end)

	--[[
		User Interface
	]]

	-- The skydiver prompt is an optional dependency.
	-- Only create a skydiver prompt if the depedency was provided.
	if Skydiver.ActionPromptGui then
		local actionPromptImages = {
			keyboard = createIndicatorImage("rbxassetid://2032127907"),
			gamepad = createIndicatorImage("rbxassetid://2032438935"),
			touch = createIndicatorImage("rbxassetid://3869394984"),
		}

		self.actionPromptGui = Skydiver.ActionPromptGui.new("", "To Skydive", actionPromptImages)
	end

	return self
end

function Skydiver:createContrail(part)
	local contrail = BeamTemplate:Clone()
	contrail.Name = part.Name
	contrail.BeamHolder.Position = part.Position
	contrail.BeamPart.Position = part.Position
	contrail.WeldConstraint.Part1 = part
	contrail.BeamPart.HeightBodyPosition.Position = part.Position + Vector3.new(0,30,0)
	contrail.Parent = self.character
	return contrail
end

function Skydiver:setupContrails()
	if not self.humanoidRootPart then return end

	self.LeftFootBeam = self:createContrail(self.character:WaitForChild("LeftFoot"))
	self.RightFootBeam = self:createContrail(self.character:WaitForChild("RightFoot"))
	self.LeftHandBeam = self:createContrail(self.character:WaitForChild("LeftHand"))
	self.RightHandBeam = self:createContrail(self.character:WaitForChild("RightHand"))
end

function Skydiver:destroyContrails()
	if self.LeftFootBeam then
		self.LeftFootBeam:Destroy()
		self.LeftFootBeam = nil
	end
	if self.RightFootBeam then
		self.RightFootBeam:Destroy()
		self.RightFootBeam = nil
	end
	if self.LeftHandBeam then
		self.LeftHandBeam:Destroy()
		self.LeftHandBeam = nil
	end
	if self.RightHandBeam then
		self.RightHandBeam:Destroy()
		self.RightHandBeam = nil
	end
end

function Skydiver:disableContrails()
	self.LeftFootBeam.Beam.Enabled = false
	self.RightFootBeam.Beam.Enabled = false
	self.LeftHandBeam.Beam.Enabled = false
	self.RightHandBeam.Beam.Enabled = false
end

function Skydiver:enableContrails()
	self.LeftFootBeam.Beam.Enabled = true
	self.RightFootBeam.Beam.Enabled = true
	self.LeftHandBeam.Beam.Enabled = true
	self.RightHandBeam.Beam.Enabled = true
end

function Skydiver:updateContrails()
	local leftFootPos = self.character:WaitForChild("LeftFoot").Position
	local rightFootPos = self.character:WaitForChild("RightFoot").Position
	local leftHandPos = self.character:WaitForChild("LeftHand").Position
	local rightHandPos = self.character:WaitForChild("RightHand").Position

	self.LeftFootBeam.BeamPart.HeightBodyPosition.Position = leftFootPos + Vector3.new(0,30,0)
	self.RightFootBeam.BeamPart.HeightBodyPosition.Position = rightFootPos + Vector3.new(0,30,0)
	self.LeftHandBeam.BeamPart.HeightBodyPosition.Position = leftHandPos + Vector3.new(0,30,0)
	self.RightHandBeam.BeamPart.HeightBodyPosition.Position = rightHandPos + Vector3.new(0,30,0)

	self.LeftFootBeam.BeamPart.XZBodyPosition.Position = leftFootPos + (self.character.LeftFoot.CFrame.RightVector * Vector3.new(-5,0,-7))
	self.RightFootBeam.BeamPart.XZBodyPosition.Position = rightFootPos + (self.character.RightFoot.CFrame.RightVector  * Vector3.new(5,0,7))
	self.LeftHandBeam.BeamPart.XZBodyPosition.Position = leftHandPos + (self.character.LeftHand.CFrame.RightVector * Vector3.new(-7,0,-5))
	self.RightHandBeam.BeamPart.XZBodyPosition.Position = rightHandPos + (self.character.RightHand.CFrame.RightVector * Vector3.new(7,0,5))
end

function Skydiver:setWaitingForJump(waiting)
	if waiting == self.waitingForJump then
		return
	end
	self.waitingForJump = waiting

	if self.waitingForJump then
		ContextActionService:BindActionAtPriority("ExitDeliveryVehicle", function(...) self:onJumpAction(...) end, false, 1000000, Enum.KeyCode.Space, Enum.KeyCode.ButtonA, Enum.UserInputType.Touch)

		if self.actionPromptGui then
			self.actionPromptGui:setVisible(true)
		end
	else
		ContextActionService:UnbindAction("ExitDeliveryVehicle")

		if self.actionPromptGui then
			self.actionPromptGui:setVisible(false)
		end
	end
end

function Skydiver:waitForJumped()
	if not self.didJump then
		self.Jumped.Event:Wait()
	end
end

function Skydiver:setEnabled(enabled)
	if enabled == self.enabled then
		return
	end
	self.enabled = enabled
	if enabled then
		self.enabledTime = tick()

		if self.humanoid then -- make sure player is in correct state to start skydiving
			self.humanoid:ChangeState(Enum.HumanoidStateType.Physics)
			local activeTracks = self.humanoid:GetPlayingAnimationTracks() -- stop any previous animations
			for i, v in pairs(activeTracks) do
				v:Stop()
			end
		end

		if self.character and self.humanoidRootPart then
			self:setupSkydivingForCharacter()
		end

		RunService:BindToRenderStep("SkydiverUpdate", Enum.RenderPriority.Last.Value, function(dt) self:onUpdate(dt) end)
	else
		if self.onlyAutoDeployOnce then
			self.autoDeploy = false
		end

		if self.freefallAnimation then
			self.freefallAnimation:Stop()
			self.freefallAnimation:Destroy()
			self.freefallAnimation = nil
		end

		if self.PitchAttachment then
			self.PitchAttachment:Destroy()
			self.PitchAttachment = nil
		end

		if self.RootRigAttachment then
			self.RootRigAttachment:Destroy()
			self.RootRigAttachment = nil
		end

		if self.SkydivePart then
			self.SkydivePart:Destroy()
			self.SkydivePart = nil
		end

		if self.BodyVelocity then
			self.BodyVelocity:Destroy()
			self.BodyVelocity = nil
		end

		if self.humanoid then
			self.humanoid:ChangeState(Enum.HumanoidStateType.Running)
		end

		if self.onClientEventConn then
			self.onClientEventConn:Disconnect()
			self.onClientEventConn = nil
		end

		self:destroyContrails()

		if self.onDisabled then
			self:onDisabled()
		end

		RunService:UnbindFromRenderStep("SkydiverUpdate")
	end
end

function Skydiver:onCharacterAdded(character)
	self.character = character

	self.humanoid = self.character:WaitForChild("Humanoid")
	self.humanoidRootPart = self.character:WaitForChild("HumanoidRootPart")

	if self.enabled then
		self:setupSkydivingForCharacter()
	end

	self.onClientEventConn = EventData.OnClientEvent:Connect(function(cmd)
		if cmd == "ExitedDeliveryVehicle" then
			if not self.didJump then
				self:setWaitingForJump(false)
				self.didJump = true
				self.Jumped:Fire()
			end
		end
	end)
end

function Skydiver:setupSkydivingForCharacter()
	if not self.humanoidRootPart then
		return
	end

	if self.humanoid and not self.freefallAnimation then
		self.freefallAnimation = self.humanoid:LoadAnimation(self.fallAnimationAsset)
		self.freefallAnimation.Looped = true
		self.freefallAnimation.Priority = Enum.AnimationPriority.Action
		self.freefallAnimation:Play()
	end

	if not self.PitchAttachment then
		self.PitchAttachment = Instance.new("Attachment")
		self.PitchAttachment.Orientation = Vector3.new(0,-90,0)
		self.PitchAttachment.Position = Vector3.new(0,-0.35,0)
		self.PitchAttachment.Axis = Vector3.new(0,0,1)
		self.PitchAttachment.SecondaryAxis = Vector3.new(0,1,0)
		self.PitchAttachment.Parent = self.humanoidRootPart
	end

	if not self.RootRigAttachment then
		self.RootRigAttachment = Instance.new("Attachment")
		self.RootRigAttachment.Orientation = Vector3.new(0,0,0)
		self.RootRigAttachment.Position = Vector3.new(0,-0.35,0)
		self.RootRigAttachment.Axis = Vector3.new(1,0,0)
		self.RootRigAttachment.SecondaryAxis = Vector3.new(0,1,0)
		self.RootRigAttachment.Parent = self.humanoidRootPart
	end

	if not self.SkydivePart then
		self.SkydivePart = SkydivePartTemplate:Clone()
		self.SkydivePart.PitchAndRollOrientation.Attachment1 = self.PitchAttachment
		self.SkydivePart.YawOrientation.Attachment0 = self.RootRigAttachment
		self.SkydivePart.Parent = workspace
	end

	if not self.BodyVelocity then
		self.BodyVelocity = Instance.new("BodyVelocity")
		self.BodyVelocity.P = 6000
		self.BodyVelocity.MaxForce = Vector3.new(1000,2000,1000)
		self.BodyVelocity.Parent = self.humanoidRootPart
	end

	self:setupContrails()
end

function Skydiver:doJump()
	if not self.waitingForJump then
		return
	end

	EventData:FireServer("ExitDeliveryVehicle")
end

function Skydiver:onJumpAction(actionName, inputState)
	if inputState == Enum.UserInputState.Begin then
		self:doJump()
	end
end

function Skydiver:getAltitudeAboveGround()
	if not self.humanoidRootPart then
		return 0
	end

	debug.profilebegin("getAltitudeAboveGround")
	local ignoreList = {
		workspace.CurrentCamera,
		self.SkydivePart,
		workspace:FindFirstChild("DeliveryVehicle" .. tostring(PlayerMatchInfo.GetField(localPlayer, "teamNumber")))
	}
	for _, player in pairs(Players:GetPlayers()) do
		if player.Character then
			table.insert(ignoreList, player.Character)
		end
	end

	local rayDown = Ray.new(self.humanoidRootPart.Position, Vector3.new(0, -ALTITUDE_TEST_LENGTH, 0))
	local hitObject, hitPoint = workspace:FindPartOnRayWithIgnoreList(rayDown, ignoreList, false, false)
	debug.profileend()
	if not hitObject then
		return ALTITUDE_TEST_LENGTH
	else
		return math.abs(rayDown.Origin.Y - hitPoint.Y)
	end
end

function Skydiver:onUpdate(dt)
	if not self.enabled then
		return
	end

	local timeSinceEnabled = tick() - self.enabledTime
	local altitude = self:getAltitudeAboveGround()

	if altitude < DISABLE_ALTITUDE and self.enabledTime ~= 0 and timeSinceEnabled > 0.5 then
		self:setEnabled(false)
		return
	end

	if not self.humanoidRootPart or not self.humanoid then return end

	local pitchRange = FALLING_PITCH_RANGE
	local pitchOffset = 0
	local rollRange = FALLING_ROLL_RANGE
	local cameraPitchContribution = 0.3

	local cameraCFrame = workspace.CurrentCamera.CFrame
	local globalMoveVector = self.humanoid.MoveDirection
	local relativeMoveVector = cameraCFrame:vectorToObjectSpace(globalMoveVector)

	local cameraLook = cameraCFrame.lookVector
	local cameraYaw = math.atan2(-cameraLook.X, -cameraLook.Z)
	local cameraPitch = math.asin(cameraLook.Y)

	self.straightDive = cameraLook.Y < -0.95 and -relativeMoveVector.Z > 0.1

	local desiredPitch = (cameraPitchContribution * cameraPitch) + (pitchRange * relativeMoveVector.Z) + pitchOffset
	local desiredRoll = (rollRange * -relativeMoveVector.X)
	local targetCFrame = CFrame.new(self.humanoidRootPart.Position) *
							CFrame.Angles(0, cameraYaw, 0) *
							CFrame.Angles(self.straightDive and DIVE_PITCH or desiredPitch, 0, 0) *
							CFrame.Angles(0, 0, desiredRoll)

	self.SkydivePart.CFrame = targetCFrame

	local globalForce = Vector3.new(globalMoveVector.x, 0, globalMoveVector.z)
	local cameraLookXZ = Vector3.new(cameraLook.x, 0, cameraLook.z)

	local dot = globalForce:Dot(cameraLookXZ)
	local planeNormal = Vector3.new(0,1,0)
	local det = globalForce.x*cameraLookXZ.y*planeNormal.z + cameraLookXZ.x*planeNormal.y*globalForce.z + planeNormal.x*globalForce.y*cameraLookXZ.z -
				globalForce.z*cameraLookXZ.y*planeNormal.x - cameraLookXZ.z*planeNormal.y*globalForce.x - planeNormal.z*globalForce.y*cameraLookXZ.x
	local angle = math.atan2(det,dot) + math.pi/2

	local XZVelocity = XZ_VELOCITY

	-- do not apply too much force when moving backwards
	if angle < -math.pi * 0.05 or angle > math.pi * 1.05 then
		XZVelocity = XZVelocity * 0.3
	end

	local yVelocity = FREEFALL_VELOCITY
	if self.straightDive then
		yVelocity = DIVE_VELOCITY
	end

	self.BodyVelocity.Velocity = Vector3.new(globalForce.x * XZVelocity, yVelocity, globalForce.z * XZVelocity)
	self:updateContrails()
end

return Skydiver