using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Barotrauma.Networking
{
    partial class Client : IDisposable
    {
        public bool VoiceEnabled = true;

        public VoipServerDecoder VoipServerDecoder;

        public UInt16 LastRecvClientListUpdate
            = NetIdUtils.GetIdOlderThan(GameMain.Server.LastClientListUpdateID);

        public UInt16 LastSentServerSettingsUpdate
            = NetIdUtils.GetIdOlderThan(GameMain.Server.ServerSettings.LastUpdateIdForFlag[ServerSettings.NetFlags.Properties]);
        public UInt16 LastRecvServerSettingsUpdate
            = NetIdUtils.GetIdOlderThan(GameMain.Server.ServerSettings.LastUpdateIdForFlag[ServerSettings.NetFlags.Properties]);

        public UInt16 LastRecvLobbyUpdate
            = NetIdUtils.GetIdOlderThan(GameMain.NetLobbyScreen.LastUpdateID);

        public bool InitialLobbyUpdateSent;

        public UInt16 LastSentChatMsgID = 0; //last msg this client said
        public UInt16 LastRecvChatMsgID = 0; //last msg this client knows about

        public UInt16 LastSentEntityEventID = 0;
        public UInt16 LastRecvEntityEventID = 0;

        public readonly Dictionary<MultiPlayerCampaign.NetFlags, UInt16> LastRecvCampaignUpdate
            = new Dictionary<MultiPlayerCampaign.NetFlags, UInt16>();
        public UInt16 LastRecvCampaignSave = 0;

        public (UInt16 saveId, float time) LastCampaignSaveSendTime;

        public readonly List<ChatMessage> ChatMsgQueue = new List<ChatMessage>();
        public UInt16 LastChatMsgQueueID;

        //latest chat messages sent by this client
        public readonly List<string> LastSentChatMessages = new List<string>();
        public float ChatSpamSpeed;
        public float ChatSpamTimer;
        public int ChatSpamCount;

        public string RejectedName;

        public float KickAFKTimer;

        public double MidRoundSyncTimeOut;

        public bool NeedsMidRoundSync;
        //how many unique events the client missed before joining the server
        public UInt16 UnreceivedEntityEventCount;
        public UInt16 FirstNewEventID;
        
        //when was a specific entity event last sent to the client
        //  key = event id, value = NetTime.Now when sending
        public readonly Dictionary<UInt16, double> EntityEventLastSent = new Dictionary<UInt16, double>();
        
        //when was a position update for a given entity last sent to the client
        //  key = entity, value = NetTime.Now when sending
        public readonly Dictionary<Entity, float> PositionUpdateLastSent = new Dictionary<Entity, float>();
        public readonly Queue<Entity> PendingPositionUpdates = new Queue<Entity>();

        public bool ReadyToStart;

        public List<JobVariant> JobPreferences { get; set; }
        public JobVariant AssignedJob;

        public float DeleteDisconnectedTimer;

        public DateTime JoinTime;

        public static readonly TimeSpan NameChangeCoolDown = new TimeSpan(hours: 0, minutes: 0, seconds: 30);
        public DateTime LastNameChangeTime;

        private CharacterInfo characterInfo;
        public CharacterInfo CharacterInfo
        {
            get { return characterInfo; }
            set
            {
                if (characterInfo == value) { return; }
                if (characterInfo is { Character: null })
                {
                    //if a character hasn't spawned for this characterInfo, we can remove the info and free the sprites and such
                    characterInfo.Remove();
                }
                characterInfo = value;
            }
        }

        public string PendingName;

        public NetworkConnection Connection { get; set; }

        public bool SpectateOnly;
        public bool AFK;
        public bool? WaitForNextRoundRespawn;

        public int KarmaKickCount;

        private float syncedKarma = 100.0f;
        private float karma = 100.0f;
        public float Karma
        {
            get
            {
                if (GameMain.Server == null || !GameMain.Server.ServerSettings.KarmaEnabled || GameMain.GameSession?.GameMode is PvPMode) 
                { 
                    return 100.0f; 
                }
                if (HasPermission(ClientPermissions.KarmaImmunity)) { return 100.0f; }
                return karma;
            }
            set
            {
                if (GameMain.Server == null || !GameMain.Server.ServerSettings.KarmaEnabled || GameMain.GameSession?.GameMode is PvPMode) 
                { 
                    return;
                }
                karma = Math.Min(Math.Max(value, 0.0f), 100.0f);
                if (!MathUtils.NearlyEqual(karma, syncedKarma, 10.0f))
                {
                    syncedKarma = karma;
                    GameMain.NetworkMember.LastClientListUpdateID++;
                }
            }
        }

        private List<Client> kickVoters;

        public int KickVoteCount
        {
            get { return kickVoters.Count; }
        }

        public WeakReference<Character> PreviousCharacter;

        partial void InitProjSpecific()
        {
            kickVoters = new List<Client>();

            JobPreferences = new List<JobVariant>();

            VoipQueue = new VoipQueue(SessionId, true, true);
            VoipServerDecoder = new VoipServerDecoder(VoipQueue, this);
            GameMain.Server.VoipServer.RegisterQueue(VoipQueue);

            //initialize to infinity, gets set to a proper value when initializing midround syncing
            MidRoundSyncTimeOut = double.PositiveInfinity;

            JoinTime = DateTime.Now;
        }

        partial void DisposeProjSpecific()
        {
            GameMain.Server.VoipServer.UnregisterQueue(VoipQueue);
            VoipQueue.Dispose();
            if (characterInfo != null)
            {
                if (characterInfo.Character == null || characterInfo.Character.Removed)
                {
                    characterInfo?.Remove();
                    characterInfo = null;
                }
            }
        }

        public void InitClientSync()
        {
            LastSentChatMsgID = 0;
            LastRecvChatMsgID = ChatMessage.LastID;

            LastRecvLobbyUpdate = NetIdUtils.GetIdOlderThan(GameMain.NetLobbyScreen.LastUpdateID);
            InitialLobbyUpdateSent = false;
            LastRecvEntityEventID = 0;

            UnreceivedEntityEventCount = 0;
            NeedsMidRoundSync = false;
        }

        public static bool IsValidName(string name, ServerSettings serverSettings)
        {
            if (string.IsNullOrWhiteSpace(name)) { return false; }
            
            char[] disallowedChars =
            {
                //',', //previously disallowed because of the ban list format
                
                ';',
                '<',
                '>',
                
                '/', //disallowed because of server messages using forward slash as a delimiter (TODO: implement escaping)
                
                '\\',
                '[',
                ']',
                '"',
                '?'
            };
            if (name.Any(c => disallowedChars.Contains(c))) { return false; }

            foreach (char character in name)
            {
                if (!serverSettings.AllowedClientNameChars.Any(charRange => (int)character >= charRange.Start && (int)character <= charRange.End)) { return false; }
            }

            return true;
        }

        public bool AddressMatches(Address address)
        {
            return Connection.Endpoint.Address.Equals(address);
        }

        public void AddKickVote(Client voter)
        {
            if (voter != null && !kickVoters.Contains(voter)) { kickVoters.Add(voter); }
        }

        public void RemoveKickVote(Client voter)
        {
            kickVoters.Remove(voter);
        }

        public bool HasKickVoteFrom(Client voter)
        {
            return kickVoters.Contains(voter);
        }

        public bool HasKickVoteFromSessionId(int id)
        {
            return kickVoters.Any(k => k.SessionId == id);
        }

        public static void UpdateKickVotes(IReadOnlyList<Client> connectedClients)
        {
            foreach (Client client in connectedClients)
            {
                client.kickVoters.RemoveAll(voter => !connectedClients.Contains(voter));
            }
        }

        /// <summary>
        /// Reset what this client has voted for and the kick votes given to this client
        /// </summary>
        public void ResetVotes(bool resetKickVotes)
        {
            for (int i = 0; i < votes.Length; i++)
            {
                votes[i] = null;
            }
            if (resetKickVotes)
            {
                kickVoters.Clear();
            }
        }


        public void SetPermissions(ClientPermissions permissions, IEnumerable<DebugConsole.Command> permittedConsoleCommands)
        {
            Permissions = permissions;
            PermittedConsoleCommands.Clear();
            PermittedConsoleCommands.UnionWith(permittedConsoleCommands);
            if (Permissions.HasFlag(ClientPermissions.ManageSettings))
            {
                //ensure the client has the up-to-date server settings
                GameMain.Server?.ServerSettings?.ForcePropertyUpdate();
            }
        }

        public void GivePermission(ClientPermissions permission)
        {
            if (!Permissions.HasFlag(permission))
            {
                Permissions |= permission;
                if (permission.HasFlag(ClientPermissions.ManageSettings))
                {
                    //ensure the client has the up-to-date server settings
                    GameMain.Server?.ServerSettings?.ForcePropertyUpdate();
                }
            }
        }

        public void RemovePermission(ClientPermissions permission)
        {
            Permissions &= ~permission;
        }

        public bool HasPermission(ClientPermissions permission)
        {
            return Permissions.HasFlag(permission);
        }

        public bool TryTakeOverBot(Character botCharacter)
        {
            if (GameMain.Server == null)
            {
                DebugConsole.ThrowError($"TryTakeOverBot: Client {Name} requested to take over a bot but GameMain.Server is null!");
                return false;
            }
            if (GameMain.NetworkMember is not { ServerSettings.RespawnMode: RespawnMode.Permadeath })
            {
                DebugConsole.ThrowError($"Client {Name} requested to take over a bot but Permadeath is not enabled!");
                GameMain.Server.SendConsoleMessage($"Permadeath mode is not enabled, cannot take over a bot.", this, Color.Red);
                return false;
            }
            if (CharacterInfo == null)
            {
                DebugConsole.ThrowError($"Permadeath: Client {Name} requested to take over a bot, but they don't seem to have a character at all yet.");
                GameMain.Server.SendConsoleMessage($"Permadeath: Taking over a bot requires having a character that died first.", this, Color.Red);
                return false;
            }
            if (CharacterInfo is not { PermanentlyDead: true })
            {
                DebugConsole.ThrowError($"Permadeath: Client {Name} requested to take over a bot, but their character has not been permanently killed.");
                GameMain.Server.SendConsoleMessage($"Permadeath: Could not take over the bot, previous character not permanently killed.", this, Color.Red);
                return false;
            }
            if (!botCharacter.IsBot)
            {
                DebugConsole.ThrowError($"Permadeath: {Name} requested to take over a bot character, but the target character is not a bot!");
                GameMain.Server.SendConsoleMessage($"Permadeath: Could not take over the target character because it is not a bot.", this, Color.Red);
                return false;
            }

            if (botCharacter.Info != null)
            {
                botCharacter.Info.RenamingEnabled = true; // Grant one opportunity to rename a taken over bot
            }

            // Now that the old permanently killed character will be replaced, we can fully discard it
            var mpCampaign = GameMain.GameSession?.Campaign as MultiPlayerCampaign;
            mpCampaign?.DiscardClientCharacterData(this);
            GameMain.Server.SetClientCharacter(this, botCharacter);
            if (mpCampaign?.SetClientCharacterData(this) is CharacterCampaignData characterData)
            {
                //the bot has spawned, but the new CharacterCampaignData technically hasn't, because we just created it
                characterData.HasSpawned = true;
                mpCampaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.CharacterInfo);
            }

            SpectateOnly = false;
            return true;
        }

        public void ResetSync()
        {
            NeedsMidRoundSync = false;
            PendingPositionUpdates.Clear();
            EntityEventLastSent.Clear();
            LastSentEntityEventID = 0;
            LastRecvEntityEventID = 0;
            UnreceivedEntityEventCount = 0;
        }
    }
}
