-- Only call this function when there are teams or it will wait forever
function PlayerMatchInfo.WaitForTeamSet(player)
	while PlayerMatchInfo.playerMatchInfoByName[player.Name] == nil or PlayerMatchInfo.playerMatchInfoByName[player.Name].teamNumber == 0 do
		wait()
	end
end

function PlayerMatchInfo.WillPlayerRespawn(player)
	return PlayerMatchInfo.teamInfo[PlayerMatchInfo.GetField(player, "teamNumber")].numRespawnTickets > 0 or
		   PlayerMatchInfo.playerMatchInfoByName[player.Name].waitingForRespawn
end

function PlayerMatchInfo.RegisterKill(target, dealer)
	-- Update kill/death counts
	if target == dealer then
		-- Subtract 1 from kills and add 1 to deaths (since player killed themself)
		PlayerMatchInfo.playerMatchInfoByName[target.Name].lastKiller = target
		PlayerMatchInfo.playerMatchInfoByName[target.Name].deaths = PlayerMatchInfo.GetField(target, "deaths") + 1
		PlayerMatchInfo.playerMatchInfoByName[target.Name].kills = math.max(0, PlayerMatchInfo.GetField(target, "kills") - 1)
		PlayerMatchInfo.UpdateLeaderboard(target.Name)
	else
		-- Update kills and deaths normally
		PlayerMatchInfo.playerMatchInfoByName[target.Name].deaths = PlayerMatchInfo.GetField(target, "deaths") + 1
		if dealer ~= nil then
			PlayerMatchInfo.playerMatchInfoByName[target.Name].lastKiller = dealer
			PlayerMatchInfo.playerMatchInfoByName[dealer.Name].kills = PlayerMatchInfo.GetField(dealer, "kills") + 1
			PlayerMatchInfo.UpdateLeaderboard(dealer.Name)
		end
	end

	local targetTeamInfo = PlayerMatchInfo.teamInfo[PlayerMatchInfo.GetField(target, "teamNumber")]
	local teamRespawnTicketCount = 0

	-- Update teamInfo for target player and set waitingForRespawn to true
	if PlayerMatchInfo.getCurrentStageName() == "Gameplay" then
		if Conf.num_teams > 0 then
			teamRespawnTicketCount = targetTeamInfo.numRespawnTickets
			if teamRespawnTicketCount > 0 then
				targetTeamInfo.numRespawnTickets = teamRespawnTicketCount - 1
				PlayerMatchInfo.playerMatchInfoByName[target.Name].waitingForRespawn = true
			else
				targetTeamInfo.numAlivePlayers = targetTeamInfo.numAlivePlayers - 1
				PlayerMatchInfo.playerMatchInfoByName[target.Name].isPermanentlyDead = true
			end
		elseif Conf.game_mode_name == "DEFAULT" then
			PlayerMatchInfo.playerMatchInfoByName[target.Name].isPermanentlyDead = true
			PlayerMatchInfo.teamInfo[1] = PlayerMatchInfo.teamInfo[1] - 1
		end
	end

	if IsServer then
		-- Notify clients of kill
		PlayerMatchInfoEvent:FireAllClients("RegisterKill", target, dealer)

		-- Check win condition
		PlayerMatchInfo.CheckWinCondition(dealer)

		-- Respawn player that was killed
		if Conf.num_teams > 0 and teamRespawnTicketCount > 0 then
			RespawnEvent:Fire(target)
		elseif Conf.num_teams <= 0 and Conf.respawn then
			RespawnEvent:Fire(target)
		end
	end
end

function PlayerMatchInfo.CheckWinCondition(dealer)
	if PlayerMatchInfo.getCurrentStageName() ~= "Gameplay" then
		return
	end

	if Conf.game_mode_name == "DEATHMATCH" then
		-- End game if there is only one player left
		if #Players:GetPlayers() == 1 then
			local lastPlayer = Players:GetPlayers()[1]
			PlayerMatchInfoEvent:FireAllClients("WinConditionMet", lastPlayer)
			print(string.format("%s won!", lastPlayer.Name))
			PlayerMatchInfo.getGameStageHandler():nextStage()
		end

		-- End game if the player that just got a kill has reached number of kills needed to win
		if dealer ~= nil and PlayerMatchInfo.GetField(dealer, "kills") >= Conf.killsNeededToWin then
			PlayerMatchInfoEvent:FireAllClients("WinConditionMet", dealer)
			print(string.format("%s won!", dealer.Name))
			PlayerMatchInfo.getGameStageHandler():nextStage()
		end
	elseif Conf.game_mode_name == "DEFAULT" then
		-- Determine who is still alive
		local playersAlive = {}
		for _, playerInstance in pairs(Players:GetPlayers()) do
			if playerInstance.Character and playerInstance.Character.Humanoid and playerInstance.Character.Humanoid.Health >= 1 then
				playersAlive[#playersAlive + 1] = playerInstance
			end
		end

		if #playersAlive == 1 then
			print(string.format("%s won!", playersAlive[1].Name))
			PlayerMatchInfoEvent:FireAllClients("WinConditionMet", playersAlive[1])
		elseif #playersAlive == 0 then
			print("Everyone died 😨")
			PlayerMatchInfoEvent:FireAllClients("WinConditionMet", nil)
		end

		if #playersAlive <= 1 then
			PlayerMatchInfo.getGameStageHandler():nextStage()
		end
	elseif Conf.game_mode_name == "TEAM_DEATHMATCH" then
		-- Determine if there is at least one player alive on each team
		local aliveTeams = {}
		for i = 1, Conf.num_teams do
			if PlayerMatchInfo.teamInfo[i].numAlivePlayers > 0 then
				table.insert(aliveTeams, i)
			end
		end

		if #aliveTeams == 1 then
			print(string.format("%s team won!", Conf.team_names[aliveTeams[1]]))
			PlayerMatchInfoEvent:FireAllClients("WinConditionMet", aliveTeams[1])
		elseif #aliveTeams == 0 then
			print("All teams died 😨")
			PlayerMatchInfoEvent:FireAllClients("WinConditionMet", 0)
		end

		if #aliveTeams <= 1 then
			PlayerMatchInfo.getGameStageHandler():nextStage()
		end
	end
end

function PlayerMatchInfo.NotifyLeaderboard(shouldStayOpen)
	if not IsServer then
		LeaderboardEvent:Fire(PlayerMatchInfo.playerMatchInfoByName, PlayerMatchInfo.playerLeaderboardOrder, shouldStayOpen)
	end
end

function PlayerMatchInfo.NotifyTeamScoreDisplay()
	if not IsServer then
		TeamScoreDisplayEvent:Fire(PlayerMatchInfo.teamInfo)
	end
end

function PlayerMatchInfo.UpdateLeaderboard(dealerName)
	local dealerIndex = 0
	local insertionIndex = nil
	for i, name in ipairs(PlayerMatchInfo.playerLeaderboardOrder) do
		if dealerName == name then
			dealerIndex = i
		end
		if insertionIndex == nil and PlayerMatchInfo.playerMatchInfoByName[name].kills < PlayerMatchInfo.playerMatchInfoByName[dealerName].kills then
			insertionIndex = i
		end
	end
	if insertionIndex == nil then
		table.insert(PlayerMatchInfo.playerLeaderboardOrder, dealerName)
		table.remove(PlayerMatchInfo.playerLeaderboardOrder, dealerIndex)
	elseif dealerIndex > insertionIndex then
		table.remove(PlayerMatchInfo.playerLeaderboardOrder, dealerIndex)
		table.insert(PlayerMatchInfo.playerLeaderboardOrder, insertionIndex, dealerName)
	elseif dealerIndex < insertionIndex then
		table.insert(PlayerMatchInfo.playerLeaderboardOrder, insertionIndex, dealerName)
		table.remove(PlayerMatchInfo.playerLeaderboardOrder, dealerIndex)
	end
end

function PlayerMatchInfo.getGameStageHandler()
	if not IsServer then
		return nil
	end

	if PlayerMatchInfo.gameStageHandler == nil then
		PlayerMatchInfo.gameStageHandler = require(game:GetService("ServerScriptService").Core.GameStageHandler).getInstance()
	end
	return PlayerMatchInfo.gameStageHandler
end

function PlayerMatchInfo.getStageManager()
	if IsServer then
		return nil
	end

	if PlayerMatchInfo.stageManager == nil then
		PlayerMatchInfo.stageManager = require(ReplicatedStorage.Core.StageManager)
	end
	return PlayerMatchInfo.stageManager
end

function PlayerMatchInfo.getCurrentStageName()
	if IsServer then
		return PlayerMatchInfo.getGameStageHandler().currentStageClassName
	else
		return PlayerMatchInfo.getStageManager().getStage()
	end
end

local function onCharacterAdded(character)
	if PlayerMatchInfo.playerMatchInfoByName[character.Name] then
		PlayerMatchInfo.playerMatchInfoByName[character.Name].waitingForRespawn = false
	end
end

local function onPlayerAdded(player)
	-- Initialize player's match info
	PlayerMatchInfo.playerMatchInfoByName[player.Name] = {
		didJump = false,
		kills = 0,
		deaths = 0,
		displayColor = Color3.fromRGB(255, 0, 50),
		teamNumber = 0,
		waitingForRespawn = false,
		lastKiller = nil,
		isPermanentlyDead = false,
	}
	table.insert(PlayerMatchInfo.playerLeaderboardOrder, player.Name)

	if Conf.num_teams == 0 then
		PlayerMatchInfo.teamInfo[1] = PlayerMatchInfo.teamInfo[1] + 1
		PlayerMatchInfo.NotifyTeamScoreDisplay()
	end

	-- Set the player's team if this game mode uses teams
	if IsServer and Conf.num_teams > 0 then
		-- Find team with least players
		local teamToAssign = 1
		for i = 2, Conf.num_teams do
			if PlayerMatchInfo.teamInfo[i].numAlivePlayers < PlayerMatchInfo.teamInfo[teamToAssign].numAlivePlayers then
				teamToAssign = i
			end
		end

		-- Assign team to player
		local teamInfo = PlayerMatchInfo.teamInfo[teamToAssign]
		teamInfo.playerList[player.Name] = true
		teamInfo.numAlivePlayers = teamInfo.numAlivePlayers + 1
		PlayerMatchInfo.playerMatchInfoByName[player.Name].displayColor = Conf.team_colors[teamToAssign]
		PlayerMatchInfo.playerMatchInfoByName[player.Name].teamNumber = teamToAssign
		PlayerMatchInfoEvent:FireAllClients("SetPlayerTeam", player, PlayerMatchInfo.playerMatchInfoByName[player.Name])
		PlayerMatchInfoEvent:FireClient(player, "SetAllMatchInfo", PlayerMatchInfo.playerMatchInfoByName, PlayerMatchInfo.playerLeaderboardOrder, PlayerMatchInfo.teamInfo)
	elseif IsServer then
		PlayerMatchInfoEvent:FireClient(player, "SetAllMatchInfo", PlayerMatchInfo.playerMatchInfoByName, PlayerMatchInfo.playerLeaderboardOrder, PlayerMatchInfo.teamInfo)
	end

	PlayerMatchInfo.NotifyLeaderboard()

	player.CharacterAdded:Connect(onCharacterAdded)
	if player.Character then
		onCharacterAdded(player.Character)
	end
end

local function onPlayerRemoving(player)
	if Conf.num_teams > 0 then
		local teamInfo = PlayerMatchInfo.teamInfo[PlayerMatchInfo.GetField(player, "teamNumber")]
		if teamInfo.numRespawnTickets > 0 or not PlayerMatchInfo.GetField(player, "isPermanentlyDead") then
			teamInfo.numAlivePlayers = teamInfo.numAlivePlayers - 1
			PlayerMatchInfo.NotifyTeamScoreDisplay()
		end
		teamInfo.playerList[player.Name] = nil
	elseif not PlayerMatchInfo.GetField(player, "isPermanentlyDead") then
		PlayerMatchInfo.teamInfo[1] = PlayerMatchInfo.teamInfo[1] - 1
		PlayerMatchInfo.NotifyTeamScoreDisplay()
	end

	PlayerMatchInfo.playerMatchInfoByName[player.Name] = nil

	for i, name in ipairs(PlayerMatchInfo.playerLeaderboardOrder) do
		if name == player.Name then
			table.remove(PlayerMatchInfo.playerLeaderboardOrder, i)
			break
		end
	end

	PlayerMatchInfo.NotifyLeaderboard()

	if IsServer then
		PlayerMatchInfo.CheckWinCondition()
	end
end

if IsServer then
	PlayerMatchInfoEvent.OnServerEvent:Connect(function(player, cmd, matchInfo)
		if cmd == "UpdatePlayerInfo" then
			PlayerMatchInfo.playerMatchInfoByName[player.Name] = matchInfo
			PlayerMatchInfoEvent:FireAllClients("UpdatePlayerInfo", player, matchInfo)
		end
	end)
	RegisterKillEvent.Event:Connect(function(target, dealer)
		PlayerMatchInfo.RegisterKill(target, dealer)
	end)
else
	PlayerMatchInfoEvent.OnClientEvent:Connect(function(cmd, ...)
		if cmd == "UpdatePlayerInfo" then
			local player, matchInfo = ...
			PlayerMatchInfo.playerMatchInfoByName[player.Name] = matchInfo
		elseif cmd == "SetPlayerTeam" then
			local player, matchInfo = ...
			if player ~= Players.LocalPlayer then -- local player's info will be set by 'SetAllMatchInfo'
				PlayerMatchInfo.playerMatchInfoByName[player.Name] = matchInfo
				local teamInfo = PlayerMatchInfo.teamInfo[matchInfo.teamNumber]
				teamInfo.playerList[player.Name] = true
				teamInfo.numAlivePlayers = teamInfo.numAlivePlayers + 1
				PlayerMatchInfo.NotifyLeaderboard()
				PlayerMatchInfo.NotifyTeamScoreDisplay()
			end
		elseif cmd == "RegisterKill" then
			local target, dealer = ...
			PlayerMatchInfo.RegisterKill(target, dealer)
			PlayerMatchInfo.NotifyLeaderboard()
			PlayerMatchInfo.NotifyTeamScoreDisplay()
		elseif cmd == "WinConditionMet" then
			local winMessage = nil
			local winMessageColor = Color3.fromRGB(255, 255, 255)
			if Conf.game_mode_name == "DEATHMATCH" then
				local winner = ...
				winMessage = winner.Name .. " Wins!"
				-- Set winner color on leaderboard to gold and force leaderboard to stay open
				PlayerMatchInfo.playerMatchInfoByName[winner.Name].displayColor = Color3.fromRGB(255, 223, 0)
				PlayerMatchInfo.NotifyLeaderboard(true)
			elseif Conf.game_mode_name == "DEFAULT" then
				local winner = ...
				if winner == nil then
					winMessage = "Nobody Wins!"
				else
					winMessage = winner.Name .. " Wins!"
				end
			elseif Conf.game_mode_name == "TEAM_DEATHMATCH" then
				local winningTeamNum = ...
				if winningTeamNum == 0 then
					winMessage = "No Team Wins!"
				else
					winMessage = Conf.team_names[winningTeamNum] .. " Team Wins!"
					winMessageColor = Conf.team_colors[winningTeamNum]
				end
			end
			EndGameMessageEvent:Fire(true, winMessage, winMessageColor)
		elseif cmd == "SetAllMatchInfo" then -- this lets the client know what team everyone already in the game is on
			PlayerMatchInfo.playerMatchInfoByName, PlayerMatchInfo.playerLeaderboardOrder, PlayerMatchInfo.teamInfo = ...
			for _, player in pairs(Players:GetPlayers()) do
				if PlayerMatchInfo.playerMatchInfoByName[player.Name] == nil then
					onPlayerAdded(player) -- this makes sure to add players that were added after "SetAllMatchInfo" was sent from server
										  -- but before this script started detecting new players with a PlayerAdded connection
				else
					player.CharacterAdded:Connect(onCharacterAdded)
					if player.Character then
						onCharacterAdded(player.Character)
					end
				end
			end
			PlayerMatchInfo.matchInfoSet = true
			PlayerMatchInfo.NotifyLeaderboard()
			PlayerMatchInfo.NotifyTeamScoreDisplay()
		end
	end)
end

Players.PlayerAdded:Connect(onPlayerAdded)
if IsServer then -- only do on server because it will send all tables to new clients with 'SetAllMatchInfo'
	for _, player in pairs(Players:GetPlayers()) do
		onPlayerAdded(player)
	end
end

if IsServer then
	PlayerMatchInfo.matchInfoSet = true -- will be set to true on client when it receives 'SetAllMatchInfo' from server
end

Players.PlayerRemoving:Connect(onPlayerRemoving)

return PlayerMatchInfo