using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Barotrauma
{
    partial class Voting
    {
        public interface IVote
        {
            public Client VoteStarter { get; }
            public VoteType VoteType { get; }
            public float Timer { get; set; }

            public VoteState State { get; set; }

            public void Finish(Voting voting, bool passed);
        }

        public class SubmarineVote : IVote
        {
            public Client VoteStarter { get; }
            public VoteType VoteType { get; }
            public float Timer { get; set; }

            public VoteState State { get; set; }

            public SubmarineInfo Sub;
            public bool TransferItems;

            public SubmarineVote(Client starter, SubmarineInfo subInfo, bool transferItems, VoteType voteType)
            {
                Sub = subInfo;
                TransferItems = transferItems;
                VoteType = voteType;
                State = VoteState.Started;
                VoteStarter = starter;
            }

            public void Finish(Voting voting, bool passed)
            {
                if (passed)
                {
                    if (GameMain.Server != null && !GameMain.Server.TrySwitchSubmarine())
                    {
                        passed = false;
                        State = VoteState.Failed;
                    }
                }
                else
                {
                    voting.RegisterRejectedVote(this);
                }
                voting.StopSubmarineVote(passed);                
            }
        }

        public class TransferVote : IVote
        {
            public Client VoteStarter { get; }
            public VoteType VoteType { get; }
            public float Timer { get; set; }

            public VoteState State { get; set; }

            //null = bank
            public readonly Client From, To;
            public readonly int TransferAmount;

            public TransferVote(Client starter, Client from, int transferAmount, Client to)
            {
                VoteStarter = starter;
                From = from;
                To = to;
                TransferAmount = transferAmount;
                State = VoteState.Started;
                VoteType = VoteType.TransferMoney;
            }

            public void Finish(Voting voting, bool passed)
            {
                if (passed)
                {
                    Wallet fromWallet = From == null ? (GameMain.GameSession.GameMode as MultiPlayerCampaign)?.Bank : From.Character?.Wallet;
                    if (fromWallet != null && fromWallet.TryDeduct(TransferAmount))
                    {
                        Wallet toWallet = To == null ? (GameMain.GameSession.GameMode as MultiPlayerCampaign)?.Bank : To.Character?.Wallet;
                        toWallet?.Give(TransferAmount);
                    }
                }
                else
                {
                    voting.RegisterRejectedVote(this);
                }
                voting.StopMoneyTransferVote(passed);
            }
        }

        public static IVote ActiveVote;

        private static readonly Queue<IVote> pendingVotes = new Queue<IVote>();

        private readonly TimeSpan rejectedVoteCooldown = new TimeSpan(0, 1, 0);

        private readonly Dictionary<Client, (VoteType voteType, DateTime time)> rejectedVoteTimes = new Dictionary<Client, (VoteType voteType, DateTime time)>();

        private void StartSubmarineVote(SubmarineInfo subInfo, bool transferItems, VoteType voteType, Client sender)
        {
            var subVote = new SubmarineVote(
                sender,
                subInfo,
                transferItems,
                voteType);
            StartOrEnqueueVote(subVote);
            GameMain.Server.UpdateVoteStatus(checkActiveVote: false);
        }

        public void StopSubmarineVote(bool passed)
        {
            if (ActiveVote is not SubmarineVote) { return; }
            StopActiveVote(passed);
        }

        public void StopMoneyTransferVote(bool passed)
        {
            if (ActiveVote is not TransferVote) { return; }
            StopActiveVote(passed);
        }

        public void StopActiveVote(bool passed)
        {
            ActiveVote.State = passed ? VoteState.Passed : VoteState.Failed;
            GameMain.Server.UpdateVoteStatus(checkActiveVote: false);

            for (int i = 0; i < GameMain.NetworkMember.ConnectedClients.Count; i++)
            {
                GameMain.NetworkMember.ConnectedClients[i].SetVote(ActiveVote.VoteType, 0);
            }

            ActiveVote = null;
            if (pendingVotes.Any())
            {
                ActiveVote = pendingVotes.Dequeue();
                ActiveVote.VoteStarter?.SetVote(ActiveVote.VoteType, 2);
            }
        }

        public void StartTransferVote(Client starter, Client from, int transferAmount, Client to)
        {
            if (ShouldRejectVote(starter, VoteType.TransferMoney))
            {
                return;
            }
            StartOrEnqueueVote(new TransferVote(starter, from, transferAmount, to));
            GameMain.Server.UpdateVoteStatus(checkActiveVote: false);
        }

        private static void StartOrEnqueueVote(IVote vote)
        {
            if (ActiveVote == null)
            {
                ActiveVote = vote;
            }
            else
            {
                pendingVotes.Enqueue(vote);
            }
        }

        public bool CanVoteToStartRound(Client client)
        {
            return !client.AFK || !GameMain.Server.ServerSettings.AllowAFK;
        }

        public bool CanVoteToEndRound(Client client)
        {
            return client.HasSpawned && client.InGame;
        }

        private bool ShouldRejectVote(Client sender, VoteType voteType)
        {
            if (rejectedVoteTimes.ContainsKey(sender))
            {
                TimeSpan remainingCooldown = (rejectedVoteTimes[sender].time + rejectedVoteCooldown) - DateTime.Now;
                if (rejectedVoteTimes[sender].voteType == voteType &&
                    remainingCooldown.TotalSeconds > 0)
                {
                    GameMain.Server.SendDirectChatMessage(
                        TextManager.FormatServerMessage("voterejectedpleasewait", ("[time]", ((int)remainingCooldown.TotalSeconds).ToString())),
                        sender, ChatMessageType.ServerMessageBox);
                    return true;
                }
            }
            return false;
        }

        protected void RegisterRejectedVote(IVote vote)
        {
            if (vote.VoteStarter != null)
            {
                rejectedVoteTimes[vote.VoteStarter] = (vote.VoteType, DateTime.Now);
            }
        }

        public void Update(float deltaTime)
        {
            if (ActiveVote == null) { return; }
            
            ActiveVote.Timer += deltaTime;

            var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame);
            if (ActiveVote.Timer >= GameMain.NetworkMember.ServerSettings.VoteTimeout || inGameClients.Count() == 1)
            {
                var eligibleClients = inGameClients.Where(c => c != ActiveVote.VoteStarter);

                // Do not take unanswered into account for total
                int yes = eligibleClients.Count(c => c.GetVote<int>(ActiveVote.VoteType) == 2);
                int no = eligibleClients.Count(c => c.GetVote<int>(ActiveVote.VoteType) == 1);
                int total = yes + no;

                bool passed = false;
                //total can be zero if the client who initiated the vote has left
                if (total > 0)
                {
                    passed = 
                        yes / (float)total >= GameMain.NetworkMember.ServerSettings.VoteRequiredRatio ||
                        inGameClients.Count() == 1;
                }
                ActiveVote.Finish(this, passed);
            }
        }

        public static void ResetVotes(IEnumerable<Client> connectedClients, bool resetKickVotes)
        {
            foreach (Client client in connectedClients)
            {
                client.ResetVotes(resetKickVotes);
            }
        }

        public void ServerRead(IReadMessage inc, Client sender, DoSProtection dosProtection)
        {
            if (GameMain.Server == null || sender == null) { return; }

            byte voteTypeByte = inc.ReadByte();
            VoteType voteType = VoteType.Unknown;
            try
            {
                voteType = (VoteType)voteTypeByte;
            }
            catch (Exception e)
            {
                DebugConsole.ThrowError("Failed to cast vote type \"" + voteTypeByte + "\"", e);
                return;
            }

            switch (voteType)
            {
                case VoteType.Sub:
                    int equalityCheckVal = inc.ReadInt32();
                    string hash = equalityCheckVal > 0 ? string.Empty : inc.ReadString();
                    SubmarineInfo sub = equalityCheckVal > 0 ?
                        SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Type == SubmarineType.Player && s.EqualityCheckVal == equalityCheckVal) :
                        SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Type == SubmarineType.Player && s.MD5Hash.StringRepresentation == hash);
                    sender.SetVote(voteType, sub);
                    break;
                case VoteType.Mode:
                    string modeIdentifier = inc.ReadString();
                    GameModePreset mode = GameModePreset.List.Find(gm => gm.Identifier == modeIdentifier);
                    if (mode == null || !mode.Votable) { break; }
                    var prevHighestVoted = HighestVoted<GameModePreset>(VoteType.Mode, GameMain.Server.ConnectedClients);
                    sender.SetVote(voteType, mode);
                    var newHighestVoted = HighestVoted<GameModePreset>(VoteType.Mode, GameMain.Server.ConnectedClients);
                    if (prevHighestVoted != newHighestVoted)
                    {
                        GameMain.NetLobbyScreen.SelectedModeIdentifier = mode.Identifier;
                        GameMain.NetLobbyScreen.LastUpdateID++;
                    }
                    break;
                case VoteType.EndRound:
                    if (!sender.HasSpawned) { return; }
                    sender.SetVote(voteType, inc.ReadBoolean());
                    break;
                case VoteType.Kick:
                    byte kickedClientID = inc.ReadByte();

                    if (!GameMain.Server.ServerSettings.AllowVoteKick)
                    {
                        DebugConsole.ThrowError($"Client {sender.Name} attempted to vote to kick a client, even though vote kicking is disabled. Ignoring the vote.");
                    }
                    else if ((DateTime.Now - sender.JoinTime).TotalSeconds < GameMain.Server.ServerSettings.DisallowKickVoteTime)
                    {
                        GameMain.Server.SendDirectChatMessage($"ServerMessage.kickvotedisallowed", sender);
                    }
                    else
                    {
                        Client kicked = GameMain.Server.ConnectedClients.Find(c => c.SessionId == kickedClientID);
                        if (kicked != null && kicked.Connection != GameMain.Server.OwnerConnection && !kicked.HasKickVoteFrom(sender))
                        {
                            kicked.AddKickVote(sender);
                            Client.UpdateKickVotes(GameMain.Server.ConnectedClients);
                            GameMain.Server.SendChatMessage($"ServerMessage.HasVotedToKick~[initiator]={sender.Name}~[target]={kicked.Name}", ChatMessageType.Server, null);
                        }
                    }
                    break;
                case VoteType.StartRound:
                    bool ready = inc.ReadBoolean();
                    if (ready != sender.GetVote<bool>(VoteType.StartRound))
                    {
                        sender.SetVote(VoteType.StartRound, ready);
                        GameServer.Log(GameServer.ClientLogName(sender) + (ready ? " is ready to start the game." : " is not ready to start the game."), ServerLog.MessageType.ServerMessage);
                    }
                    break;
                case VoteType.PurchaseAndSwitchSub:
                case VoteType.PurchaseSub:
                case VoteType.SwitchSub:
                case VoteType.TransferMoney:
                    bool startVote = inc.ReadBoolean();
                    if (startVote)
                    {
                        if (voteType == VoteType.TransferMoney)
                        {
                            int amount = inc.ReadInt32();
                            int fromClientId = inc.ReadByte();
                            int toClientId = inc.ReadByte();
                            if (!ShouldRejectVote(sender, voteType))
                            {
                                pendingVotes.Enqueue(new TransferVote(sender,
                                    GameMain.Server.ConnectedClients.Find(c => c.SessionId == fromClientId),
                                    amount,
                                    GameMain.Server.ConnectedClients.Find(c => c.SessionId == toClientId)));
                            }
                        }
                        else
                        {
                            string subName = inc.ReadString();
                            SubmarineInfo subInfo = GameMain.GameSession.OwnedSubmarines.FirstOrDefault(s => s.Name == subName) ?? SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName);
                            bool transferItems = inc.ReadBoolean();
                            if (!ShouldRejectVote(sender, voteType))
                            {
                                if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign && 
                                    (campaign.CanPurchaseSub(subInfo, sender) || GameMain.GameSession.IsSubmarineOwned(subInfo)))
                                {
                                    StartSubmarineVote(subInfo, transferItems, voteType, sender);
                                }
                            }
                        }
                    }
                    else
                    {
                        sender.SetVote(voteType, (int)inc.ReadByte());
                    }
                    break;
                case VoteType.Traitor:
                    int clientId = inc.ReadInt32();
                    if (sender.InGame && sender.Character != null)
                    {
                        var client = GameMain.Server.ConnectedClients.FirstOrDefault(c => c.SessionId == clientId);
                        sender.SetVote(voteType, client);
                        if (client?.Character != null)
                        {
                            string msg = TextManager.GetWithVariable("traitor.blamebutton.dialog", "[name]", client.Character.DisplayName).Value;
                            ChatMessage.HandleSpamFilter(sender, msg, out bool flaggedAsSpam);
                            if (!flaggedAsSpam)
                            {
                                GameMain.Server.SendChatMessage(
                                   msg,
                                    ChatMessageType.Radio, senderClient: sender, senderCharacter: sender.Character);
                                sender.LastSentChatMessages.Add(msg);
                            }
                        }
                    }
                    break;
            }

            inc.ReadPadBits();

            using (dosProtection.Pause(sender))
            {
                GameMain.Server.UpdateVoteStatus();
            }
        }

        public void ServerWrite(IWriteMessage msg)
        {
            if (GameMain.Server == null) { return; }

            msg.WriteBoolean(GameMain.Server.ServerSettings.AllowSubVoting);
            if (GameMain.Server.ServerSettings.AllowSubVoting)
            {
                bool isMultiSub = GameMain.NetLobbyScreen.SelectedMode == GameModePreset.PvP;
                msg.WriteBoolean(isMultiSub);

                var subVoters = isMultiSub ?
                                    GameMain.Server.ConnectedClients.Where(static c => c.PreferredTeam is CharacterTeamType.Team1) :
                                    GameMain.Server.ConnectedClients;

                IReadOnlyDictionary<SubmarineInfo, int> voteList = GetVoteCounts<SubmarineInfo>(VoteType.Sub, subVoters);
                msg.WriteByte((byte)voteList.Count);
                foreach (KeyValuePair<SubmarineInfo, int> vote in voteList)
                {
                    msg.WriteByte((byte)vote.Value);
                    msg.WriteString(vote.Key.Name);
                }

                if (isMultiSub)
                {
                    var separatistsVotes = GetVoteCounts<SubmarineInfo>(VoteType.Sub, GameMain.Server.ConnectedClients.Where(static c => c.PreferredTeam is CharacterTeamType.Team2));
                    msg.WriteByte((byte)separatistsVotes.Count);

                    foreach (var (info, amount) in separatistsVotes)
                    {
                        msg.WriteByte((byte)amount);
                        msg.WriteString(info.Name);
                    }
                }
            }
            msg.WriteBoolean(GameMain.Server.ServerSettings.AllowModeVoting);
            if (GameMain.Server.ServerSettings.AllowModeVoting)
            {
                IReadOnlyDictionary<GameModePreset, int> voteList = GetVoteCounts<GameModePreset>(VoteType.Mode, GameMain.Server.ConnectedClients);
                msg.WriteByte((byte)voteList.Count);
                foreach (KeyValuePair<GameModePreset, int> vote in voteList)
                {
                    msg.WriteByte((byte)vote.Value);
                    msg.WriteIdentifier(vote.Key.Identifier);
                }
            }
            msg.WriteBoolean(GameMain.Server.ServerSettings.AllowEndVoting);
            if (GameMain.Server.ServerSettings.AllowEndVoting)
            {
                msg.WriteByte((byte)GameMain.Server.ConnectedClients.Count(c => CanVoteToEndRound(c) && c.GetVote<bool>(VoteType.EndRound)));
                msg.WriteByte((byte)GameMain.Server.ConnectedClients.Count(c => CanVoteToEndRound(c)));
            }

            msg.WriteBoolean(GameMain.Server.ServerSettings.AllowVoteKick);

            msg.WriteByte((byte)(ActiveVote?.State ?? VoteState.None));
            if (ActiveVote != null)
            {
                msg.WriteByte((byte)ActiveVote.VoteType);
                if (ActiveVote.State != VoteState.None && ActiveVote.VoteType != VoteType.Unknown)
                {
                    var eligibleClients = GameMain.Server.ConnectedClients.Where(c => c.InGame && c != ActiveVote.VoteStarter);

                    var yesClients = eligibleClients.Where(c => c.GetVote<int>(ActiveVote.VoteType) == 2);
                    msg.WriteByte((byte)yesClients.Count());
                    foreach (Client c in yesClients)
                    {
                        msg.WriteByte(c.SessionId);
                    }

                    var noClients = eligibleClients.Where(c => c.GetVote<int>(ActiveVote.VoteType) == 1);
                    msg.WriteByte((byte)noClients.Count());
                    foreach (Client c in noClients)
                    {
                        msg.WriteByte(c.SessionId);
                    }

                    msg.WriteByte((byte)eligibleClients.Count());

                    switch (ActiveVote.State)
                    {
                        case VoteState.Started:
                            msg.WriteByte(ActiveVote.VoteStarter.SessionId);
                            msg.WriteByte((byte)GameMain.Server.ServerSettings.VoteTimeout);

                            switch (ActiveVote.VoteType)
                            {
                                case VoteType.PurchaseSub:
                                case VoteType.PurchaseAndSwitchSub:
                                case VoteType.SwitchSub:
                                    SubmarineVote vote = ActiveVote as SubmarineVote;
                                    msg.WriteString(vote.Sub.Name);
                                    msg.WriteBoolean(vote.TransferItems);
                                    break;
                                case VoteType.TransferMoney:
                                    var transferVote = (ActiveVote as TransferVote);
                                    msg.WriteByte(transferVote.From?.SessionId ?? 0);
                                    msg.WriteByte(transferVote.To?.SessionId ?? 0);
                                    msg.WriteInt32(transferVote.TransferAmount);
                                    break;
                            }

                            break;
                        case VoteState.Running:
                            // Nothing specific
                            break;
                        case VoteState.Passed:
                        case VoteState.Failed:
                            msg.WriteBoolean(ActiveVote.State == VoteState.Passed);
                            switch (ActiveVote.VoteType)
                            {
                                case VoteType.PurchaseSub:
                                case VoteType.PurchaseAndSwitchSub:
                                case VoteType.SwitchSub:
                                    var subVote = ActiveVote as SubmarineVote;
                                    msg.WriteString(subVote.Sub.Name);
                                    msg.WriteBoolean(subVote.TransferItems);
                                    break;
                            }
                            break;
                    }                    
                }
            }            

            var readyClients = GameMain.Server.ConnectedClients.Where(c => c.GetVote<bool>(VoteType.StartRound));
            msg.WriteByte((byte)readyClients.Count());
            foreach (Client c in readyClients)
            {
                msg.WriteByte(c.SessionId);
            }

            msg.WritePadBits();
        }
    }
}
