#region Copyright & License Information
/*
 * Copyright (c) The OpenRA Developers and Contributors
 * This file is part of OpenRA, which is free software. It is made
 * available to you under the terms of the GNU General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version. For more
 * information, see COPYING.
 */
#endregion

using System.Collections.Generic;
using System.Diagnostics;
using OpenRA.Network;

namespace OpenRA.Server
{
	public sealed class VoteKickTracker
	{
		[FluentReference("kickee")]
		const string InsufficientVotes = "notification-insufficient-votes-to-kick";

		[FluentReference]
		const string AlreadyVoted = "notification-kick-already-voted";

		[FluentReference("kicker", "kickee")]
		const string VoteKickStarted = "notification-vote-kick-started";

		[FluentReference]
		const string UnableToStartAVote = "notification-unable-to-start-a-vote";

		[FluentReference("kickee", "percentage")]
		const string VoteKickProgress = "notification-vote-kick-in-progress";

		[FluentReference("kickee")]
		const string VoteKickEnded = "notification-vote-kick-ended";

		readonly Dictionary<int, bool> voteTracker = new();
		readonly Dictionary<Session.Client, long> failedVoteKickers = new();
		readonly Server server;

		Stopwatch voteKickTimer;
		(Session.Client Client, Connection Conn) kickee;
		(Session.Client Client, Connection Conn) voteKickerStarter;

		public VoteKickTracker(Server server)
		{
			this.server = server;
		}

		// Only admins and alive players can participate in a vote kick.
		bool ClientHasPower(Session.Client client) => client.IsAdmin || (!client.IsObserver && !server.HasClientWonOrLost(client));

		public void Tick()
		{
			if (voteKickTimer == null)
				return;

			if (!server.Conns.Contains(kickee.Conn))
			{
				EndKickVote();
				return;
			}

			if (voteKickTimer.ElapsedMilliseconds > server.Settings.VoteKickTimer)
				EndKickVoteAndBlockKicker();
		}

		public bool VoteKick(Connection conn, Session.Client kicker, Connection kickeeConn, Session.Client kickee, int kickeeID, bool vote)
		{
			var voteInProgress = voteKickTimer != null;

			if (server.State != ServerState.GameStarted
				|| (kickee.IsAdmin && server.Type != ServerType.Dedicated)
				|| (!voteInProgress && !vote) // Disallow starting a vote with a downvote
				|| (voteInProgress && this.kickee.Client != kickee) // Disallow starting new votes when one is already ongoing.
				|| !ClientHasPower(kicker))
			{
				server.SendFluentMessageTo(conn, UnableToStartAVote);
				return false;
			}

			short eligiblePlayers = 0;
			var isKickeeOnline = false;
			var adminIsDeadButOnline = false;
			foreach (var c in server.Conns)
			{
				var client = server.GetClient(c);
				if (client != kickee && ClientHasPower(client))
					eligiblePlayers++;

				if (c == kickeeConn)
					isKickeeOnline = true;

				if (client.IsAdmin && (client.IsObserver || server.HasClientWonOrLost(client)))
					adminIsDeadButOnline = true;
			}

			if (!isKickeeOnline)
			{
				EndKickVote();
				return false;
			}

			if (eligiblePlayers < 2 || (adminIsDeadButOnline && !kickee.IsAdmin && eligiblePlayers < 3))
			{
				if (!kickee.IsObserver && !server.HasClientWonOrLost(kickee))
				{
					// Vote kick cannot be the sole deciding factor for a game.
					server.SendFluentMessageTo(conn, InsufficientVotes, new object[] { "kickee", kickee.Name });
					EndKickVote();
					return false;
				}
				else if (vote)
				{
					// If only a single player is playing, allow him to kick observers.
					EndKickVote(false);
					return true;
				}
			}

			if (!voteInProgress)
			{
				// Prevent vote kick spam abuse.
				if (failedVoteKickers.TryGetValue(kicker, out var time))
				{
					if (time + server.Settings.VoteKickerCooldown > kickeeConn.ConnectionTimer.ElapsedMilliseconds)
					{
						server.SendFluentMessageTo(conn, UnableToStartAVote);
						return false;
					}
					else
						failedVoteKickers.Remove(kicker);
				}

				Log.Write("server", $"Vote kick started on {kickeeID}.");
				voteKickTimer = Stopwatch.StartNew();
				server.SendFluentMessage(VoteKickStarted, "kicker", kicker.Name, "kickee", kickee.Name);
				server.DispatchServerOrdersToClients(new Order("StartKickVote", null, false) { ExtraData = (uint)kickeeID }.Serialize());
				this.kickee = (kickee, kickeeConn);
				voteKickerStarter = (kicker, conn);
			}

			if (!voteTracker.ContainsKey(conn.PlayerIndex))
				voteTracker[conn.PlayerIndex] = vote;
			else
			{
				server.SendFluentMessageTo(conn, AlreadyVoted);
				return false;
			}

			short votesFor = 0;
			short votesAgainst = 0;
			foreach (var c in voteTracker)
			{
				if (c.Value)
					votesFor++;
				else
					votesAgainst++;
			}

			// Include the kickee in eligeablePlayers, so that in a 2v2 or any other even team
			// matchup one team could not vote out the other team's player.
			if (ClientHasPower(kickee))
			{
				eligiblePlayers++;
				votesAgainst++;
			}

			var votesNeeded = eligiblePlayers / 2 + 1;
			server.SendFluentMessage(VoteKickProgress,
				"kickee", kickee.Name,
				"percentage", votesFor * 100 / eligiblePlayers);

			// If a player or players during a vote lose or disconnect, it is possible that a downvote will
			// kick a client. Guard against that situation.
			if (vote && (votesFor >= votesNeeded))
			{
				EndKickVote(false);
				return true;
			}

			// End vote if it can never succeed.
			if (eligiblePlayers - votesAgainst < votesNeeded)
			{
				EndKickVoteAndBlockKicker();
				return false;
			}

			voteKickTimer.Restart();
			return false;
		}

		void EndKickVoteAndBlockKicker()
		{
			// Make sure vote kick is in progress.
			if (voteKickTimer == null)
				return;

			if (server.Conns.Contains(voteKickerStarter.Conn))
				failedVoteKickers[voteKickerStarter.Client] = voteKickerStarter.Conn.ConnectionTimer.ElapsedMilliseconds;

			EndKickVote();
		}

		void EndKickVote(bool sendMessage = true)
		{
			// Make sure vote kick is in progress.
			if (voteKickTimer == null)
				return;

			if (sendMessage)
				server.SendFluentMessage(VoteKickEnded, "kickee", kickee.Client.Name);

			server.DispatchServerOrdersToClients(new Order("EndKickVote", null, false) { ExtraData = (uint)kickee.Client.Index }.Serialize());

			voteKickTimer = null;
			voteKickerStarter = (null, null);
			kickee = (null, null);
			voteTracker.Clear();
		}
	}
}
