-- Finds a visible player at a certain position relative to the camera, otherwise returns nil
local function findSelectedPlayer(objectPosition, camera, playersService, maxClickDistance)
	if camera then
		local ray = camera:ScreenPointToRay(objectPosition.X, objectPosition.Y)
		local raycastResult = workspace:Raycast(ray.Origin, ray.Direction * maxClickDistance) -- sets the max click distance relative to camera
		if raycastResult then
			local hitPart = raycastResult.Instance
			local character = hitPart:FindFirstAncestorOfClass("Model")
			if character then
				-- Prevents selection of an invisible character
				local isVisible = false
				local transparencyValues = {}
				for _, descendant in pairs(character:GetDescendants()) do
					if descendant:IsA("BasePart") or descendant:IsA("Decal") then
						table.insert(transparencyValues, descendant.Transparency)
					end
				end

				-- Check that all character parts are invisible (Transparency = 1)
				for _, value in ipairs(transparencyValues) do
					if value == 0 then
						isVisible = true
					end
				end

				if isVisible then
					local targetPlayer = playersService:GetPlayerFromCharacter(character)
					if targetPlayer then
						return targetPlayer
					end
				end
			end
		end
	end
	return nil
end

ProfileCard.defaultProps = {
	Players = game:GetService("Players"),
	MainFrame = MainFrame,
	Camera = workspace.CurrentCamera,
	UpdateProfileRemoteFunction = script.Parent.Parent.RemoteEvents.UpdateProfileRemoteFunction,
	PulsePrompt = require(script.Parent.PulsePrompt),
}

local ViewTypes = {
	KeyboardView = Vector3.new(0, 6, 0),
	CenterView = Vector3.new(0, 1, 0),
	OriginalView = Vector3.new(0, 0, 0),
}

local gamepadInputs = {
	Enum.UserInputType.Gamepad1,
	Enum.UserInputType.Gamepad2,
	Enum.UserInputType.Gamepad3,
	Enum.UserInputType.Gamepad4,
	Enum.UserInputType.Gamepad5,
	Enum.UserInputType.Gamepad6,
	Enum.UserInputType.Gamepad7,
	Enum.UserInputType.Gamepad8,
}

function ProfileCard:init()
	self.state = {
		userId = self.props.userId,
		isEditing = false,
		showStatusError = false,
		showPrompt = true,
		viewType = ViewTypes.OriginalView,
		activeGamepad = false,
		adornee = nil
	}

	self.setHumanoidMovementEnabled = function(canMoveBool)
		-- Prevents character movement
		local LocalPlayer = self.props.Players.LocalPlayer
		local Character = LocalPlayer.Character or LocalPlayer.CharacterAdded:Wait()
		local HumanoidRootPart = Character:WaitForChild("HumanoidRootPart")
		if HumanoidRootPart then
			HumanoidRootPart.Anchored = not canMoveBool
		end
	end

	self.setKeyboardMovementEnabled = function(enabledBool)
		-- Prevents character movement and camera rotation
		if enabledBool then
			self.props.controls:Enable()
			ContextActionService:BindActionAtPriority("RightMouseDisable", function()
				return Enum.ContextActionResult.Pass
			end, false, Enum.ContextActionPriority.Medium.Value, Enum.UserInputType.MouseButton2)
		else
			self.props.controls:Disable()
			ContextActionService:BindActionAtPriority("RightMouseDisable", function()
				return Enum.ContextActionResult.Sink
			end, false, Enum.ContextActionPriority.Medium.Value, Enum.UserInputType.MouseButton2)
		end
	end

	self.getOtherGuis = function()
		local res = {}
		local playerGui = self.props.Players.LocalPlayer:FindFirstChild("PlayerGui")
		if playerGui then
			for _, instance in pairs(playerGui:GetDescendants()) do
				if
					(instance:IsA("ScreenGui") or instance:IsA("BillboardGui"))
					and not instance:GetAttribute("ShowInProfileCard")
					and instance.Enabled
				then
					table.insert(res, instance)
				end
			end
		end

		return res
	end

	self.getShownCoreGuis = function()
		local shownCoreGuis = Cryo.List.filter(Enum.CoreGuiType:GetEnumItems(), function(coreGuiType)
			-- The inclusion of these GUIs will cause the entire screen to flash after an edit to the ProfileCard is saved. Filtering out for now.
			if Enum.CoreGuiType.PlayerList == coreGuiType or Enum.CoreGuiType.All == coreGuiType then
				return false
			end

			return StarterGui:GetCoreGuiEnabled(coreGuiType)
		end)
		return shownCoreGuis
	end

	self.hideGuis = function()
		if callbacks.hideOtherGuis.isCallbackSet() then
			callbacks.hideOtherGuis.fireCallback()
			return
		end

		-- Hide ScreenGuis
		self.disabledGuis = self.getOtherGuis()
		for _, gui in ipairs(self.disabledGuis) do
			gui.Enabled = false
		end

		-- Hide CoreGuis
		self.disabledCoreGuis = self.getShownCoreGuis()
		for _, gui in ipairs(self.disabledCoreGuis) do
			StarterGui:SetCoreGuiEnabled(gui, false)
		end

		-- Hide mobile controls
		self.modalEnabled = UserInputService.ModalEnabled
		UserInputService.ModalEnabled = true
	end

	self.showGuis = function()
		if callbacks.showOtherGuis.isCallbackSet() then
			callbacks.showOtherGuis.fireCallback()
			return
		end

		-- Show guis that were hidden by hideGuis()
		for _, gui in ipairs(self.disabledGuis) do
			gui.Enabled = true
		end
		self.disabledGuis = {}

		for _, gui in ipairs(self.disabledCoreGuis) do
			StarterGui:SetCoreGuiEnabled(gui, true)
		end
		self.disabledCoreGuis = {}

		UserInputService.ModalEnabled = self.modalEabled
	end

	self.toggleEdit = function(object)
		if not self.state.isEditing then
			self:setState({ isEditing = true })
			self.props.Camera.CameraType = Enum.CameraType.Scriptable
			self.hideGuis()

			if UserInputService.TouchEnabled then
				self:setState({ viewType = ViewTypes.CenterView })
			end

			-- Prevent character movement
			self.setHumanoidMovementEnabled(false)
			if UserInputService.KeyboardEnabled then
				self.setKeyboardMovementEnabled(false)
			end
		else
			local filteredTextTable = self.props.UpdateProfileRemoteFunction:InvokeServer(
				self.props.Players.LocalPlayer,
				object.Parent.Status.Text
			)

			if filteredTextTable.success then
				self.showGuis()
				self:setState({ isEditing = false, showStatusError = false, viewType = ViewTypes.OriginalView })
				self.props.Camera.CameraType = Enum.CameraType.Custom

				-- Restore movement
				self.setHumanoidMovementEnabled(true)
				if UserInputService.KeyboardEnabled then
					self.setKeyboardMovementEnabled(true)
				end
			else
				self:setState({ showStatusError = true })

				if UserInputService.TouchEnabled then
					self:setState({ viewType = ViewTypes.CenterView })
				end
			end
		end
	end

	self.updateViewToKeyboard = function()
		self:setState({ viewType = ViewTypes.KeyboardView })
	end

	self.unmount = function()
		self:setState({ userId = Roact.None, isEditing = false })
	end

	self.onInputBegan = function(input, gameProcessedEvent)
		local isValidInput = input.UserInputType == Enum.UserInputType.MouseButton1
			or input.UserInputType == Enum.UserInputType.Touch
		if isValidInput and not self.state.isEditing and not gameProcessedEvent then
			-- Prevents opening a new profile card while editing; also prevents profile card from closing when interacting with engine related UI (eg. chat button)
			local inputPos = input.Position
			local playerGui = self.props.Players.LocalPlayer:WaitForChild("PlayerGui")
			local profileCard = playerGui:FindFirstChild("ProfileCard")
			local shouldUnmount = true

			if profileCard then
				local guis = playerGui:GetGuiObjectsAtPosition(inputPos.X, inputPos.Y)
				for _, g in ipairs(guis) do
					local billboard = g:FindFirstAncestorOfClass("BillboardGui")
					if billboard and billboard == profileCard then
						shouldUnmount = false
					end
				end
			end

			if shouldUnmount then
				local player = findSelectedPlayer(
					inputPos,
					self.props.Camera,
					self.props.Players,
					self.props.configuration.maxClickDistance
				)
				if player then
					-- Opens a new profile card with the relevant player details
					self:setState({ userId = player.UserId, showPrompt = false, viewType = ViewTypes.OriginalView })
				else
					-- Hide an open profile card if input is not on the card itself
					self.unmount()
				end
			end
		end
	end

	self.checkViewport = function(testPos)
		local _, inViewport = self.props.Camera:WorldToViewportPoint(testPos)
		return inViewport
	end

	self.determineStudOffset = function(object)
		if not object then
			return Vector3.new(0, 0, 0)
		elseif self.state.viewType == ViewTypes.KeyboardView or self.state.viewType == ViewTypes.CenterView then
			return self.state.viewType
		end

		local copyCFrame = object.CFrame
		local rightCFrame = copyCFrame + Vector3.new(Constants.STUD_OFFSET_X, 1, 0)
		local leftCFrame = copyCFrame + Vector3.new(-Constants.STUD_OFFSET_X, 1, 0)
		local studsOffsetVector = Vector3.new(0, 0, 0)

		if self.checkViewport(rightCFrame.Position) then
			studsOffsetVector = Vector3.new(Constants.STUD_OFFSET_X, 1, 0)
		elseif self.checkViewport(leftCFrame.Position) then
			studsOffsetVector = Vector3.new(-Constants.STUD_OFFSET_X, 1, 0)
		end

		if self.state.viewType == ViewTypes.OriginalView then
			ViewTypes.OriginalView = studsOffsetVector
		end

		return studsOffsetVector
	end

	self.getAdornee = function(player)
		local adornee = player == self.props.Players.LocalPlayer and player.Character:FindFirstChild("HumanoidRootPart")
			or Anchor.createAnchor(player, player.Character:FindFirstChild("HumanoidRootPart").Position)
		return adornee
	end
	
	self.profileCard = Roact.createRef()
end

function ProfileCard:render()
	-- Prevent gamepad interaction
	if table.find(gamepadInputs, UserInputService:GetLastInputType()) ~= nil or self.state.activeGamepad then
		return
	end

	local Players = self.props.Players
	local player

	if self.state.userId then
		player = Players:GetPlayerByUserId(self.state.userId)
	end

	return Roact.createFragment({
		Roact.createElement(ProfileCardContext.Provider, { userId = self.state.userId }, {
			ProfileCard = player and Roact.createElement("BillboardGui", {
				Active = true,
				Size = UDim2.new(0, Constants.GUI_SIZE_X, 0, Constants.GUI_SIZE_Y),
				AlwaysOnTop = self.props.configuration.alwaysOnTop,
				Adornee = self.getAdornee(player),
				StudsOffset = self.determineStudOffset(player.Character:FindFirstChild("HumanoidRootPart")),
				
				[Roact.Ref] = self.profileCard,
			}, {
				MainFrame = Roact.createElement(self.props.MainFrame, {
					controls = self.props.controls,
					isEditing = self.state.isEditing,
					toggleEdit = self.toggleEdit,
					showStatusError = self.state.showStatusError,
					updateViewToKeyboard = self.updateViewToKeyboard,
				}),
			}),
			ExternalEventConnection = Roact.createElement(ExternalEventConnection, {
				event = UserInputService.InputBegan, -- Maybe use Touch/Tap in world, specifically for mobile
				callback = self.onInputBegan,
			}),
			-- Creates a blur effect when editing
			BlurEffectPortal = self.state.isEditing and self.props.configuration.showBlur and Roact.createElement(
				Roact.Portal,
				{ target = self.props.Camera },
				{ BlurEffect = Roact.createElement("BlurEffect") }
			),
		}),
		PersonalIndicator = Roact.createElement(self.props.PulsePrompt, {
			Players = Players,
			adornee = self.props.adornee,
			isEnabled = self.state.showPrompt,
			userId = Players.LocalPlayer.UserId,
		}),
	})
end

function ProfileCard:didMount()
	UserInputService.LastInputTypeChanged:Connect(function(lastInputType)
		if table.find(gamepadInputs, lastInputType) ~= nil then
			self:setState({
				activeGamepad = true,
				isEditing = false,
				showStatusError = false,
				viewType = ViewTypes.OriginalView,
			})
			self.setHumanoidMovementEnabled(true)
			self.props.Camera.CameraType = Enum.CameraType.Custom
			return
		end
		
		if self.state.activeGamepad then
			self:setState({ activeGamepad = false })
		end
	end)
end

function ProfileCard:didUpdate(prevProps)
	if self.state.userId then
		self.profileCard:getValue():SetAttribute("ShowInProfileCard", true)
		
		-- Hide the Profile Card from view once it leaves the screen
		local adornee = self.getAdornee(self.props.Players:GetPlayerByUserId(self.state.userId))
		local adorneeOffset = self.determineStudOffset(self.Adornee)

		coroutine.wrap(function(object)
			local inViewport = self.checkViewport(object.Position)
			while inViewport and object do
				wait(1)
				
				local newCFrame = object.CFrame
				newCFrame = newCFrame * CFrame.new(adorneeOffset)
				inViewport = self.checkViewport(object.Position)
			end
			self.unmount()
		end)(adornee)
	end
end

return withConfiguration(ProfileCard)