using Barotrauma.Extensions;
using Lidgren.Network;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Barotrauma.Networking
{
    class ServerEntityEvent : NetEntityEvent
    {
        private IServerSerializable serializable;

        public bool Sent;

#if DEBUG
        public string StackTrace;
#endif

        private double createTime;
        public double CreateTime
        {
            get { return createTime; }
        }

        public ServerEntityEvent(IServerSerializable entity, UInt16 id)
            : base(entity, id)
        {
            serializable = entity;

            createTime = Timing.TotalTime;

#if DEBUG
            StackTrace = Environment.StackTrace.ToString();
#endif
        }

        public void Write(NetBuffer msg, Client recipient)
        {
            serializable.ServerWrite(msg, recipient, Data);
        }
    }

    class ServerEntityEventManager : NetEntityEventManager
    {
        private List<ServerEntityEvent> events;

        //list of unique events (i.e. !IsDuplicate) created during the round
        //used for syncing clients who join mid-round
        private List<ServerEntityEvent> uniqueEvents;

        private UInt16 lastSentToAll;

        public List<ServerEntityEvent> Events
        {
            get { return events; }
        }

        public List<ServerEntityEvent> UniqueEvents
        {
            get { return uniqueEvents; }
        }

        private class BufferedEvent
        {
            public readonly Client Sender;

            public readonly UInt16 CharacterStateID;
            public readonly NetBuffer Data;

            public readonly Character Character;

            public readonly IClientSerializable TargetEntity;

            public bool IsProcessed;

            public BufferedEvent(Client sender, Character senderCharacter, UInt16 characterStateID, IClientSerializable targetEntity, NetBuffer data)
            {
                this.Sender = sender;
                this.Character = senderCharacter;
                this.CharacterStateID = characterStateID;

                this.TargetEntity = targetEntity;

                this.Data = data;
            }
        }

        private List<BufferedEvent> bufferedEvents;

        private UInt16 ID;

        private GameServer server;
        
        public ServerEntityEventManager(GameServer server) 
        {
            events = new List<ServerEntityEvent>();

            this.server = server;

            bufferedEvents = new List<BufferedEvent>();

            uniqueEvents = new List<ServerEntityEvent>();
        }

        public void CreateEvent(IServerSerializable entity, object[] extraData = null)
        {
            if (entity == null || !(entity is Entity))
            {
                DebugConsole.ThrowError("Can't create an entity event for " + entity + "!");
                return;
            }

            if (((Entity)entity).Removed && !(entity is Level))
            {
                DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the entity has been removed.\n"+Environment.StackTrace);
                return;
            }
            if (((Entity)entity).IdFreed)
            {
                DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the ID of the entity has been freed.\n"+Environment.StackTrace);
                return;
            }

            var newEvent = new ServerEntityEvent(entity, (UInt16)(ID + 1));
            if (extraData != null) newEvent.SetData(extraData);
            
            //remove events that have been sent to all clients, they are redundant now
            //keep at least one event in the list (lastSentToAll == e.ID) so we can use it to keep track of the latest ID
            events.RemoveAll(e => NetIdUtils.IdMoreRecent(lastSentToAll, e.ID));

            if (server.ConnectedClients.Count(c => c.InGame) == 0 && events.Count > 1)
            {
                events.RemoveRange(0, events.Count - 1);
            }

            for (int i = events.Count - 1; i >= 0; i--)
            {
                //we already have an identical event that's waiting to be sent
                // -> no need to add a new one
                if (events[i].IsDuplicate(newEvent) && !events[i].Sent) return;
            }

            ID++;

            events.Add(newEvent);

            if (!uniqueEvents.Any(e => e.IsDuplicate(newEvent)))
            {
                //create a copy of the event and give it a new ID
                var uniqueEvent = new ServerEntityEvent(entity, (UInt16)(uniqueEvents.Count + 1));
                uniqueEvent.SetData(extraData);

                uniqueEvents.Add(uniqueEvent);
            }
        }

        public void Update(List<Client> clients)
        {
            foreach (BufferedEvent bufferedEvent in bufferedEvents)
            {
                if (bufferedEvent.Character == null || bufferedEvent.Character.IsDead)
                {
                    bufferedEvent.IsProcessed = true;
                    continue;
                }

                //delay reading the events until the inputs for the corresponding frame have been processed

                //UNLESS the character is unconscious, in which case we'll read the messages immediately (because further inputs will be ignored)
                //atm the "give in" command is the only thing unconscious characters can do, other types of events are ignored
                if (!bufferedEvent.Character.IsUnconscious &&
                    NetIdUtils.IdMoreRecent(bufferedEvent.CharacterStateID, bufferedEvent.Character.LastProcessedID))
                {
                    continue;
                }
                
                try
                {
                    ReadEvent(bufferedEvent.Data, bufferedEvent.TargetEntity, bufferedEvent.Sender);
                }

                catch (Exception e)
                {
                    string entityName = bufferedEvent.TargetEntity == null ? "null" : bufferedEvent.TargetEntity.ToString();
                    if (GameSettings.VerboseLogging)
                    {
                        string errorMsg = "Failed to read server event for entity \"" + entityName + "\"!";
                        GameServer.Log(errorMsg + "\n" + e.StackTrace, ServerLog.MessageType.Error);
                        DebugConsole.ThrowError(errorMsg, e);
                    }
                    GameAnalyticsManager.AddErrorEventOnce("ServerEntityEventManager.Read:ReadFailed" + entityName,
                        GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
                        "Failed to read server event for entity \"" + entityName + "\"!\n" + e.StackTrace);
                }

                bufferedEvent.IsProcessed = true;
            }
            
            var inGameClients = clients.FindAll(c => c.InGame && !c.NeedsMidRoundSync);
            if (inGameClients.Count > 0)
            {
                lastSentToAll = inGameClients[0].LastRecvEntityEventID;
                if (server.OwnerConnection != null)
                {
                    var owner = clients.Find(c => c.Connection == server.OwnerConnection);
                    if (owner != null)
                    {
                        lastSentToAll = owner.LastRecvEntityEventID;
                    }
                }
                inGameClients.ForEach(c => { if (NetIdUtils.IdMoreRecent(lastSentToAll, c.LastRecvEntityEventID)) lastSentToAll = c.LastRecvEntityEventID; });

                clients.Where(c => c.NeedsMidRoundSync).ForEach(c => { if (NetIdUtils.IdMoreRecent(lastSentToAll, c.FirstNewEventID)) lastSentToAll = (ushort)(c.FirstNewEventID - 1); });

                ServerEntityEvent firstEventToResend = events.Find(e => e.ID == (ushort)(lastSentToAll + 1));
                if (firstEventToResend != null && (Timing.TotalTime - firstEventToResend.CreateTime) > 10.0f)
                {
                    //it's been 10 seconds since this event was created, kick everyone that hasn't received it yet, this is way too old
                    //  UNLESS the event was created when the client was still midround syncing, in which case we'll wait until the timeout
                    //  runs out before kicking the client
                    List<Client> toKick = inGameClients.FindAll(c => 
                        NetIdUtils.IdMoreRecent((UInt16)(lastSentToAll + 1), c.LastRecvEntityEventID) && 
                        (firstEventToResend.CreateTime > c.MidRoundSyncTimeOut || Timing.TotalTime > c.MidRoundSyncTimeOut));
                    toKick.ForEach(c =>
                        {
                            DebugConsole.NewMessage(c.Name + " was kicked due to excessive desync (expected old event " + (c.LastRecvEntityEventID + 1).ToString() + ")", Color.Red);
                            GameServer.Log("Disconnecting client " + c.Name + " due to excessive desync (expected old event " 
                                + (c.LastRecvEntityEventID + 1).ToString() +
                                " (created " + (Timing.TotalTime - firstEventToResend.CreateTime).ToString("0.##") + " s ago)" +
                                " Events queued: " + events.Count + ", last sent to all: " + lastSentToAll, ServerLog.MessageType.Error);
                            server.DisconnectClient(c, "", "ServerMessage.ExcessiveDesyncOldEvent");
                        }
                    );
                }

                if (events.Count > 0)
                {
                    //the client is waiting for an event that we don't have anymore
                    //(the ID they're expecting is smaller than the ID of the first event in our list)
                    List<Client> toKick = inGameClients.FindAll(c => NetIdUtils.IdMoreRecent(events[0].ID, (UInt16)(c.LastRecvEntityEventID + 1)));
                    toKick.ForEach(c =>
                    {
                        DebugConsole.NewMessage(c.Name + " was kicked due to excessive desync (expected removed event " + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", Color.Red);
                        GameServer.Log("Disconnecting client " + c.Name + " due to excessive desync (expected removed event " + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", ServerLog.MessageType.Error);
                        server.DisconnectClient(c, "", "ServerMessage.ExcessiveDesyncRemovedEvent");
                    });
                }
            }
            
            var timedOutClients = clients.FindAll(c => c.InGame && c.NeedsMidRoundSync && Timing.TotalTime > c.MidRoundSyncTimeOut);
            foreach (Client timedOutClient in timedOutClients)
            {
                GameServer.Log("Disconnecting client " + timedOutClient.Name + ". Syncing the client with the server took too long.", ServerLog.MessageType.Error);
                GameMain.Server.DisconnectClient(timedOutClient, "", "ServerMessage.SyncTimeout");
            }
            
            bufferedEvents.RemoveAll(b => b.IsProcessed);           
        }

        private void BufferEvent(BufferedEvent bufferedEvent)
        {
            if (bufferedEvents.Count > 512)
            {
                //should normally never happen

                //a client could potentially spam events with a much higher character state ID 
                //than the state of their character and/or stop sending character inputs,
                //so we'll drop some events to make sure no-one blows up our buffer
                DebugConsole.Log("Excessive amount of events in a client's event buffer. The client may be spamming events or their event IDs might be out of sync. Dropping events...");
                bufferedEvents.RemoveRange(0, 256);
            }

            bufferedEvents.Add(bufferedEvent);
        }


        /// <summary>
        /// Writes all the events that the client hasn't received yet into the outgoing message
        /// </summary>
        public void Write(Client client, NetOutgoingMessage msg)
        {
            Write(client, msg, out _);
        }

        /// <summary>
        /// Writes all the events that the client hasn't received yet into the outgoing message
        /// </summary>
        public void Write(Client client, NetOutgoingMessage msg, out List<NetEntityEvent> sentEvents)
        {
            List<NetEntityEvent> eventsToSync = null;
            if (client.NeedsMidRoundSync)
            {
                eventsToSync = GetEventsToSync(client, uniqueEvents);
            }
            else
            {
                eventsToSync = GetEventsToSync(client, events);
            }

            if (eventsToSync.Count == 0)
            {
                sentEvents = eventsToSync;
                return;
            }

            //too many events for one packet
            if (eventsToSync.Count > 200)
            {
                if (eventsToSync.Count > 200 && !client.NeedsMidRoundSync)
                {
                    Color color = eventsToSync.Count > 500 ? Color.Red : Color.Orange;
                    if (eventsToSync.Count < 300) { color = Color.Yellow; }
                    string warningMsg = "WARNING: event count very high: " + eventsToSync.Count;

                    var sortedEvents = eventsToSync.GroupBy(e => e.Entity.ToString())
                        .Select(e => new { Value = e.Key, Count = e.Count() })
                        .OrderByDescending(e => e.Count);

                    int count = 1;
                    foreach (var sortedEvent in sortedEvents)
                    {
                        warningMsg += "\n" + count + ". " + (sortedEvent.Value?.ToString() ?? "null") + " x" + sortedEvent.Count;
                        count++;
                        if (count > 3) { break; }
                    }
                    if (GameSettings.VerboseLogging)
                    {
                        GameServer.Log(warningMsg, ServerLog.MessageType.Error);
                    }
                    DebugConsole.NewMessage(warningMsg, color);
                }
            }

            if (client.NeedsMidRoundSync)
            {
                msg.Write((byte)ServerNetObject.ENTITY_EVENT_INITIAL);                
                msg.Write(client.UnreceivedEntityEventCount);
                msg.Write(client.FirstNewEventID);

                Write(msg, eventsToSync, out sentEvents, client);
            }
            else
            {
                msg.Write((byte)ServerNetObject.ENTITY_EVENT);
                Write(msg, eventsToSync, out sentEvents, client);
            }

            foreach (NetEntityEvent entityEvent in sentEvents)
            {
                (entityEvent as ServerEntityEvent).Sent = true;
                client.EntityEventLastSent[entityEvent.ID] = NetTime.Now;
            }
        }

        /// <summary>
        /// Returns a list of events that should be sent to the client from the eventList 
        /// </summary>
        /// <param name="client"></param>
        /// <param name="eventList"></param>
        /// <returns></returns>
        private List<NetEntityEvent> GetEventsToSync(Client client, List<ServerEntityEvent> eventList)
        {
            List<NetEntityEvent> eventsToSync = new List<NetEntityEvent>();
            if (eventList.Count == 0) return eventsToSync;

            //find the index of the first event the client hasn't received
            int startIndex = eventList.Count;
            while (startIndex > 0 &&
                NetIdUtils.IdMoreRecent(eventList[startIndex - 1].ID, client.LastRecvEntityEventID))
            {
                startIndex--;
            }

            for (int i = startIndex; i < eventList.Count; i++)
            {
                //find the first event that hasn't been sent in roundtriptime or at all
                client.EntityEventLastSent.TryGetValue(eventList[i].ID, out double lastSent);

                float minInterval = Math.Max(client.Connection.AverageRoundtripTime, (float)server.UpdateInterval.TotalSeconds * 2);

                if (lastSent > NetTime.Now - Math.Min(minInterval, 0.5f))
                {
                    continue;
                }

                eventsToSync.AddRange(eventList.GetRange(i, eventList.Count - i));
                break;
            }

            return eventsToSync;
        }

        public void InitClientMidRoundSync(Client client)
        {
            //no need for midround syncing if no events have been created,
            //or if the first created unique event is still in the event list
            if (uniqueEvents.Count == 0 || (events.Count > 0 && events[0].ID == uniqueEvents[0].ID))
            {
                client.UnreceivedEntityEventCount = 0;
                client.FirstNewEventID = 0;
                client.NeedsMidRoundSync = false;
            }
            else
            {
                double midRoundSyncTimeOut = uniqueEvents.Count / 100 * server.UpdateInterval.TotalSeconds;
                midRoundSyncTimeOut = Math.Max(10.0f, midRoundSyncTimeOut * 10.0f);

                client.UnreceivedEntityEventCount = (UInt16)uniqueEvents.Count;
                client.NeedsMidRoundSync = true;
                client.MidRoundSyncTimeOut = Timing.TotalTime + midRoundSyncTimeOut;

                //how many (unique) events the clients had missed before joining
                client.UnreceivedEntityEventCount = (UInt16)uniqueEvents.Count;
                //ID of the first event sent after the client joined 
                //(after the client has been synced they'll switch their lastReceivedID 
                //to the one before this, and the eventmanagers will start to function "normally")
                client.FirstNewEventID = events.Count == 0 ? (UInt16)0 : events[events.Count - 1].ID;
            }
        }

        /// <summary>
        /// Read the events from the message, ignoring ones we've already received
        /// </summary>
        public void Read(NetIncomingMessage msg, Client sender = null)
        {
            UInt16 firstEventID = msg.ReadUInt16();
            int eventCount = msg.ReadByte();

            for (int i = 0; i < eventCount; i++)
            {
                UInt16 thisEventID = (UInt16)(firstEventID + (UInt16)i);
                UInt16 entityID = msg.ReadUInt16();

                if (entityID == Entity.NullEntityID)
                {
                    msg.ReadPadBits();
                    if (thisEventID == (UInt16)(sender.LastSentEntityEventID + 1)) sender.LastSentEntityEventID++;
                    continue;
                }

                byte msgLength = msg.ReadByte();

                IClientSerializable entity = Entity.FindEntityByID(entityID) as IClientSerializable;

                //skip the event if we've already received it
                if (thisEventID != (UInt16)(sender.LastSentEntityEventID + 1))
                {
                    if (GameSettings.VerboseLogging)
                    {
                        DebugConsole.NewMessage("Received msg " + thisEventID, Color.Red);
                    }
                    msg.Position += msgLength * 8;
                }
                else if (entity == null)
                {
                    //entity not found -> consider the even read and skip over it
                    //(can happen, for example, when a client uses a medical item repeatedly 
                    //and creates an event for it before receiving the event about it being removed)
                    if (GameSettings.VerboseLogging)
                    {
                        DebugConsole.NewMessage(
                            "Received msg " + thisEventID + ", entity " + entityID + " not found",
                            Microsoft.Xna.Framework.Color.Orange);
                    }
                    sender.LastSentEntityEventID++;
                    msg.Position += msgLength * 8;
                }
                else
                {
                    if (GameSettings.VerboseLogging)
                    {
                        DebugConsole.NewMessage("Received msg " + thisEventID, Microsoft.Xna.Framework.Color.Green);
                    }

                    UInt16 characterStateID = msg.ReadUInt16();

                    NetBuffer buffer = new NetBuffer();
                    buffer.Write(msg.ReadBytes(msgLength - 2));
                    BufferEvent(new BufferedEvent(sender, sender.Character, characterStateID, entity, buffer));

                    sender.LastSentEntityEventID++;
                }
                msg.ReadPadBits();
            }
        }

        protected override void WriteEvent(NetBuffer buffer, NetEntityEvent entityEvent, Client recipient = null)
        {
            var serverEvent = entityEvent as ServerEntityEvent;
            if (serverEvent == null) return;

            serverEvent.Write(buffer, recipient);
        }

        protected void ReadEvent(NetBuffer buffer, INetSerializable entity, Client sender = null)
        {
            var clientEntity = entity as IClientSerializable;
            if (clientEntity == null) return;
            
            clientEntity.ServerRead(ClientNetObject.ENTITY_STATE, buffer, sender);
        }
        
        public void Clear()
        {
            ID = 0;
            events.Clear();

            bufferedEvents.Clear();

            lastSentToAll = 0;

            uniqueEvents.Clear();

            foreach (Client c in server.ConnectedClients)
            {
                c.EntityEventLastSent.Clear();
                c.LastRecvEntityEventID = 0;
                c.LastSentEntityEventID = 0;
            }
        }
    }
}
