-- Keeps track of which players are currently playing the game and associating
-- words with each player so they can guess what was assigned to them.


local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local Cryo = require(ReplicatedStorage.HeadsUp.Packages.Cryo)
local constants = require(ReplicatedStorage.HeadsUp.Constants)
local setupTag = require(ReplicatedStorage.HeadsUp.Modules.SetupTag)
local isPlayerOnPad = require(ReplicatedStorage.HeadsUp.Modules.isPlayerOnPad)

local RemoteEvents = ReplicatedStorage.HeadsUp.RemoteEvents

local nextPadId = 1

local function chooseRandom(list: { any })
	return list[Random.new():NextInteger(1, #list)]
end

local function setWord(player: Player)
	local wordsInUse = {}
	for _, otherPlayer in ipairs(Players:GetPlayers()) do
		local word = otherPlayer:GetAttribute(constants.WORD_ATTRIBUTE_NAME)
		if word then
			table.insert(wordsInUse, word)
		end
	end

	-- Filter out any words that are already in play so no players in the
	-- same session get the same word.
	local possibleWords = Cryo.List.filter(constants.WORDS, function(word)
		return not Cryo.List.find(wordsInUse, word)
	end)

	player:SetAttribute(constants.WORD_ATTRIBUTE_NAME, chooseRandom(possibleWords))
end

local function setNextActiveUserId(pad: Model, session: table, activeUserId: string)
	-- setNextActiveUserId is called when the client guessing a word has the same
	-- UserId as `activeUserId`. Thus table.find will always return a positive
	-- value, so it is safe to not guard for `nil` here.
	local nextIndex = table.find(session, activeUserId) + 1

	if nextIndex > #session then
		nextIndex = 1
	end

	local nextActiveUserId = session[nextIndex]

	pad:SetAttribute(constants.ACTIVE_PLAYER_ATTRIBUTE, nextActiveUserId)

	return nextActiveUserId
end

setupTag(constants.PAD_TAG_NAME, function(pad: Model)
	local padId = "Pad" .. tostring(nextPadId + 1)
	nextPadId += 1

	-- This table keeps track of the order that each player joined the current
	-- Heads Up session, which is used for choosing who the next player to guess
	-- their word will be
	local session = {}

	local sessionJoined = Instance.new("BindableEvent")
	local sessionLeft = Instance.new("BindableEvent")

	local function onNewWordRequested(player: Player)
		if player:GetAttribute(constants.WORD_ATTRIBUTE_NAME) then
			setWord(player)
		end
	end

	local function onWordGuessed(player: Player, word: string)
		local userId = tostring(player.UserId)
		local assignedWord = player:GetAttribute(constants.WORD_ATTRIBUTE_NAME)
		local activeUserId = pad:GetAttribute(constants.ACTIVE_PLAYER_ATTRIBUTE)

		if assignedWord and userId == activeUserId then
			local isCorrect = word:lower() == assignedWord:lower()

			if isCorrect then
				player:SetAttribute(constants.WORD_ATTRIBUTE_NAME, "")
			end

			for _, otherUserId in ipairs(session) do
				local otherPlayer = Players:GetPlayerByUserId(tonumber(otherUserId))
				RemoteEvents.WordGuessed:FireClient(otherPlayer, player, isCorrect)
			end

			setNextActiveUserId(pad, session, activeUserId)
		end
	end

	local function onSessionJoined(player: Player)
		local userId = tostring(player.UserId)

		if not pad:GetAttribute(constants.ACTIVE_PLAYER_ATTRIBUTE) then
			pad:SetAttribute(constants.ACTIVE_PLAYER_ATTRIBUTE, userId)
		end

		if not player:GetAttribute(constants.WORD_ATTRIBUTE_NAME) then
			table.insert(session, userId)
			setWord(player)
			player:SetAttribute(constants.SESSION_ATTRIBUTE, padId)
		end
	end

	local function onSessionLeft(player: Player)
		local userId = tostring(player.UserId)
		local activeUserId = pad:GetAttribute(constants.ACTIVE_PLAYER_ATTRIBUTE)

		if userId == activeUserId then
			local nextActiveUserId = setNextActiveUserId(pad, session, activeUserId)

			if userId == nextActiveUserId then
				pad:SetAttribute(constants.ACTIVE_PLAYER_ATTRIBUTE, nil)
			end
		end

		local index = table.find(session, userId)
		if index then
			table.remove(session, index)
		end

		player:SetAttribute(constants.SESSION_ATTRIBUTE, nil)
		player:SetAttribute(constants.WORD_ATTRIBUTE_NAME, nil)
	end

	local function onPlayerRemoving(player: Player)
		local userId = tostring(player.UserId)
		if table.find(session, userId) then
			onSessionLeft(player)
		end
	end

	local function onHeartbeat()
		-- Check for all player positions and see if they are within range of the game pads
		for _, player in ipairs(Players:GetPlayers()) do
			local isOnPad = isPlayerOnPad(player, pad)
			local sessionId = player:GetAttribute(constants.SESSION_ATTRIBUTE)

			if isOnPad and not sessionId then
				sessionJoined:Fire(player)
			elseif not isOnPad and sessionId == padId then
				sessionLeft:Fire(player)
			end
		end
	end

	RemoteEvents.NewWordRequested.OnServerEvent:Connect(onNewWordRequested)
	RemoteEvents.WordGuessed.OnServerEvent:Connect(onWordGuessed)
	sessionJoined.Event:Connect(onSessionJoined)
	sessionLeft.Event:Connect(onSessionLeft)
	RunService.Heartbeat:Connect(onHeartbeat)
	Players.PlayerRemoving:Connect(onPlayerRemoving)

	pad:SetAttribute(constants.SESSION_ATTRIBUTE, padId)
end)
