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

namespace Barotrauma
{
    partial class Character
    {
        private Address ownerClientAddress;
        private Option<AccountId> ownerClientAccountId;

        public bool ClientDisconnected;
        public float KillDisconnectedTimer;

        private bool networkUpdateSent;

        private double LastInputTime;

        public bool HealthUpdatePending;

        public void SetOwnerClient(Client client)
        {
            if (client == null)
            {
                ownerClientAddress = null;
                ownerClientAccountId = Option<AccountId>.None();
                IsRemotePlayer = false;
            }
            else
            {
                ownerClientAddress = client.Connection.Endpoint.Address;
                ownerClientAccountId = client.AccountId;
                IsRemotePlayer = true;
            }
        }

        public bool IsClientOwner(Client client)
        {
            if (ownerClientAccountId.TryUnwrap(out var accountId)
                && client.AccountId.TryUnwrap(out var clientId))
            {
                return accountId == clientId;
            }
            else
            {
                return ownerClientAddress == client.Connection.Endpoint.Address;
            }            
        }

        public float GetPositionUpdateInterval(Client recipient)
        {
            if (!Enabled) { return 1000.0f; }

            Vector2 comparePosition = recipient.SpectatePos ?? recipient.Character.WorldPosition;

            float distance = Vector2.Distance(comparePosition, WorldPosition);
            if (recipient.Character?.ViewTarget != null)
            {
                distance = Math.Min(distance, Vector2.Distance(recipient.Character.ViewTarget.WorldPosition, WorldPosition));
            }
            if (ViewTarget != null && ViewTarget != this)
            {
                distance = Math.Min(distance, Vector2.Distance(comparePosition, ViewTarget.WorldPosition));
            }

            float priority = 1.0f - MathUtils.InverseLerp(
                NetConfig.HighPrioCharacterPositionUpdateDistance, 
                NetConfig.LowPrioCharacterPositionUpdateDistance,
                distance);

            float interval = MathHelper.Lerp(
                NetConfig.LowPrioCharacterPositionUpdateInterval, 
                NetConfig.HighPrioCharacterPositionUpdateInterval,
                priority);

            if (IsDead && !AnimController.IsDraggedWithRope)
            {
                interval = Math.Max(interval * 2, 0.1f);
            }

            return interval;
        }

        partial void UpdateNetInput()
        {
            //non-ai character (a character that was previously controlled by a player) or a remote player (which can be an AI character controlled by a player)
            if (this is not AICharacter || IsRemotePlayer)
            {
                if (!CanMove)
                {
                    AnimController.Frozen = false;
                    if (memInput.Count > 0)
                    {
                        prevDequeuedInput = dequeuedInput;
                        dequeuedInput = memInput[memInput.Count - 1].states & InputNetFlags.Ragdoll;
                        memInput.RemoveAt(memInput.Count - 1);
                    }
                }
                else if (memInput.Count == 0)
                {
                    AnimController.Frozen = true;
                    if (Timing.TotalTime > LastInputTime + 0.5)
                    {
                        //no inputs have been received in 0.5 seconds, reset input
                        //(if there's a temporary network hiccup that prevents us from receiving inputs, we assume the inputs haven't changed,
                        //but if it takes too long, for example due to a client crashing/disconnecting, we don't want to keep the character
                        //firing a welding tool or whatever else they were doing until the kill disconnect timer kicks in)
                        prevDequeuedInput = dequeuedInput =
                            dequeuedInput.HasFlag(InputNetFlags.FacingLeft) ? InputNetFlags.FacingLeft : InputNetFlags.None;
                    }
                }
                else
                {
                    AnimController.Frozen = false;
                    prevDequeuedInput = dequeuedInput;

                    LastProcessedID = memInput[memInput.Count - 1].networkUpdateID;
                    dequeuedInput = memInput[memInput.Count - 1].states;

                    double aimAngle = ((double)memInput[memInput.Count - 1].intAim / 65535.0) * 2.0 * Math.PI;
                    cursorPosition = AimRefPosition + new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 500.0f;

                    //reset focus when attempting to use/select something
                    if (memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Use) ||
                        memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Select) ||
                        memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Deselect) ||
                        memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Health) ||
                        memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Grab))
                    {
                        focusedItem = null;
                        FocusedCharacter = null;
                    }
                    var closestEntity = FindEntityByID(memInput[memInput.Count - 1].interact);
                    if (closestEntity is Item item)
                    {
                        if (CanInteractWith(item))
                        {
                            focusedItem = item;
                            FocusedCharacter = null;
                        }
                        else
                        {
                            //failed to interact with the item 
                            // -> correct the position and the state of the Holdable component (in case the item was deattached client-side)
                            item.PositionUpdateInterval = 0.0f;
                            var holdable = item.GetComponent<Items.Components.Holdable>();
                            holdable?.Item?.CreateServerEvent(holdable);
                        }
                    }
                    else if (closestEntity is Character character)
                    {
                        if (CanInteractWith(character, maxDist: 250.0f))
                        {
                            FocusedCharacter = character;
                            focusedItem = null;
                        }
                    }

                    memInput.RemoveAt(memInput.Count - 1);

                    if ((dequeuedInput == InputNetFlags.None || dequeuedInput == InputNetFlags.FacingLeft) && Math.Abs(AnimController.Collider.LinearVelocity.X) < 0.005f && Math.Abs(AnimController.Collider.LinearVelocity.Y) < 0.2f)
                    {
                        while (memInput.Count > 5 && memInput[memInput.Count - 1].states == dequeuedInput)
                        {
                            //remove inputs where the player is not moving at all
                            //helps the server catch up, shouldn't affect final position
                            LastProcessedID = memInput[memInput.Count - 1].networkUpdateID;
                            memInput.RemoveAt(memInput.Count - 1);
                        }
                    }
                }
            }
            AnimController.Frozen = false;

            if (networkUpdateSent)
            {
                foreach (Key key in keys)
                {
                    key.DequeueHit();
                    key.DequeueHeld();
                }

                networkUpdateSent = false;
            }
        }

        public void ServerReadInput(IReadMessage msg, Client c)
        {
            if (c.Character != this)
            {
#if DEBUG
                DebugConsole.Log("Received a character update message from a client who's not controlling the character");
#endif
                return;
            }

            UInt16 networkUpdateID = msg.ReadUInt16();
            byte inputCount = msg.ReadByte();

            // Doesn't seem to work consistently (at least with simulated long loading time 120), because sometimes there's some stun on the character. Anyway, can't see why we'd have to check AllowInput here.
            //if (AllowInput) { Enabled = true; }
            Enabled = true;

            for (int i = 0; i < inputCount; i++)
            {
                InputNetFlags newInput = (InputNetFlags)msg.ReadRangedInteger(0, (int)InputNetFlags.MaxVal);
                UInt16 newAim = 0;
                UInt16 newInteract = 0;

                if (newInput != InputNetFlags.None && newInput != InputNetFlags.FacingLeft)
                {
                    c.KickAFKTimer = 0.0f;
                }
                else if (AnimController.Dir < 0.0f != newInput.HasFlag(InputNetFlags.FacingLeft))
                {
                    //character changed the direction they're facing
                    c.KickAFKTimer = 0.0f;
                }

                newAim = msg.ReadUInt16();
                if (newInput.HasFlag(InputNetFlags.Select) ||
                    newInput.HasFlag(InputNetFlags.Deselect) ||
                    newInput.HasFlag(InputNetFlags.Use) ||
                    newInput.HasFlag(InputNetFlags.Health) ||
                    newInput.HasFlag(InputNetFlags.Grab))
                {
                    newInteract = msg.ReadUInt16();
                }

                if (NetIdUtils.IdMoreRecent((ushort)(networkUpdateID - i), LastNetworkUpdateID) && (i < 60))
                {
                    if ((i > 0 && memInput[i - 1].intAim != newAim))
                    {
                        c.KickAFKTimer = 0.0f;
                    }
                    NetInputMem newMem = new NetInputMem
                    {
                        states = newInput,
                        intAim = newAim,
                        interact = newInteract,
                        networkUpdateID = (ushort)(networkUpdateID - i)
                    };
                    memInput.Insert(i, newMem);
                    LastInputTime = Timing.TotalTime;
                }
            }

            if (NetIdUtils.IdMoreRecent(networkUpdateID, LastNetworkUpdateID))
            {
                LastNetworkUpdateID = networkUpdateID;
            }
            else if (NetIdUtils.Difference(networkUpdateID, LastNetworkUpdateID) > 500)
            {
#if DEBUG || UNSTABLE
                DebugConsole.AddWarning($"Large discrepancy between a client character's network update ID server-side and client-side (client: {networkUpdateID}, server: {LastNetworkUpdateID}). Resetting the ID.");
#endif
                LastNetworkUpdateID = LastProcessedID = networkUpdateID;
            }
            if (memInput.Count > 60)
            {
                //deleting inputs from the queue here means the server is way behind and data needs to be dropped
                //we'll make the server drop down to 30 inputs for good measure
                memInput.RemoveRange(30, memInput.Count - 30);
            }
        }

        public virtual void ServerEventRead(IReadMessage msg, Client c)
        {
            EventType eventType = (EventType)msg.ReadRangedInteger((int)EventType.MinValue, (int)EventType.MaxValue);
            switch (eventType)
            {
                case EventType.InventoryState:
                    Inventory.ServerEventRead(msg, c);
                    break;
                case EventType.Treatment:
                    bool doingCPR = msg.ReadBoolean();
                    if (c.Character != this)
                    {
#if DEBUG
                        DebugConsole.Log("Received a character update message from a client who's not controlling the character");
#endif
                        return;
                    }

                    AnimController.Anim = doingCPR ? AnimController.Animation.CPR : AnimController.Animation.None;
                    break;
                case EventType.Status:
                    if (c.Character != this)
                    {
#if DEBUG
                        DebugConsole.Log("Received a character update message from a client who's not controlling the character");
#endif
                        return;
                    }

                    if (IsIncapacitated)
                    {
                        var causeOfDeath = CharacterHealth.GetCauseOfDeath();
                        Kill(causeOfDeath.type, causeOfDeath.affliction);
                    }
                    break;
                case EventType.ConfirmTalentRefund:
                    if (!CanManageTalents(c)) { return; }
                    Info?.RefundTalents();
                    break;
                case EventType.UpdateTalents:
                    if (!CanManageTalents(c)) { return; }

                    // get the full list of talents from the player, only give the ones
                    // that are not already given (or otherwise not viable)
                    ushort talentCount = msg.ReadUInt16();
                    List<Identifier> talentSelection = new List<Identifier>();
                    for (int i = 0; i < talentCount; i++)
                    {
                        UInt32 talentIdentifier = msg.ReadUInt32();
                        var prefab = TalentPrefab.TalentPrefabs.Find(p => p.UintIdentifier == talentIdentifier);
                        if (prefab == null) { continue; }

                        if (TalentTree.IsViableTalentForCharacter(this, prefab.Identifier, talentSelection))
                        {
                            bool? should = GameMain.LuaCs.Hook.Call<bool?>("character.updateTalent", this, prefab, c);
                            if (should == null)
                            {
                                GiveTalent(prefab.Identifier);
                            }
                            talentSelection.Add(prefab.Identifier);
                        }
                    }
                    if (talentSelection.Count != talentCount)
                    {
                        DebugConsole.AddWarning($"Failed to unlock talents: the amount of unlocked talents doesn't match (client: {talentCount}, server: {talentSelection.Count})");
                    }
                    break;
            }

            bool CanManageTalents(Client client)
            {
                if (client.Character != this)
                {
                    if (client.TeamID != TeamID || !IsBot || !client.HasPermission(ClientPermissions.ManageBotTalents) || client.Spectating)
                    {
#if DEBUG
                        DebugConsole.Log("A client tried to manage talents of a character they don't control or have permission to manage");
#endif
                        return false;
                    }
                }

                return true;
            }
        }

        public void ServerWritePosition(ReadWriteMessage tempBuffer, Client c)
        {
            if (this == c.Character)
            {
                tempBuffer.WriteBoolean(true);
                if (LastNetworkUpdateID < memInput.Count + 1)
                {
                    tempBuffer.WriteUInt16((UInt16)0);
                }
                else
                {
                    tempBuffer.WriteUInt16((UInt16)(LastNetworkUpdateID - memInput.Count - 1));
                }
            }
            else
            {
                tempBuffer.WriteBoolean(false);

                bool aiming = false;
                bool use = false;
                bool attack = false;
                bool shoot = false;

                if (IsRemotePlayer)
                {
                    aiming  = dequeuedInput.HasFlag(InputNetFlags.Aim);
                    use     = dequeuedInput.HasFlag(InputNetFlags.Use);
                    attack  = dequeuedInput.HasFlag(InputNetFlags.Attack);
                    shoot   = dequeuedInput.HasFlag(InputNetFlags.Shoot);
                }
                else if (keys != null)
                {
                    aiming  = keys[(int)InputType.Aim].GetHeldQueue;
                    use     = keys[(int)InputType.Use].GetHeldQueue;
                    attack  = keys[(int)InputType.Attack].GetHeldQueue;
                    shoot   = keys[(int)InputType.Shoot].GetHeldQueue;
                    networkUpdateSent = true;
                }

                tempBuffer.WriteBoolean(aiming);
                tempBuffer.WriteBoolean(shoot);
                tempBuffer.WriteBoolean(use);

                if (AnimController is HumanoidAnimController humanAnim)
                {
                    tempBuffer.WriteBoolean(humanAnim.Crouching);
                }
                else if (AnimController is FishAnimController fishAnim)
                {
                    tempBuffer.WriteBoolean(fishAnim.Reverse);
                }

                tempBuffer.WriteBoolean(attack);

                Vector2 relativeCursorPos = cursorPosition - AimRefPosition;
                tempBuffer.WriteUInt16((UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI)));

                tempBuffer.WriteBoolean(IsRagdolled || Stun > 0.0f || IsDead || IsIncapacitated);

                tempBuffer.WriteBoolean(AnimController.Dir > 0.0f);
            }

            if (SelectedCharacter != null || HasSelectedAnyItem)
            {
                tempBuffer.WriteBoolean(true);
                tempBuffer.WriteUInt16(SelectedCharacter != null ? SelectedCharacter.ID : NullEntityID);
                tempBuffer.WriteUInt16(SelectedItem != null ? SelectedItem.ID : NullEntityID);
                tempBuffer.WriteUInt16(SelectedSecondaryItem != null ? SelectedSecondaryItem.ID : NullEntityID);
                if (SelectedCharacter != null)
                {
                    tempBuffer.WriteBoolean(AnimController.Anim == AnimController.Animation.CPR);
                }
            }
            else
            {
                tempBuffer.WriteBoolean(false);
            }

            tempBuffer.WriteSingle(SimPosition.X);
            tempBuffer.WriteSingle(SimPosition.Y);
            float MaxVel = NetConfig.MaxPhysicsBodyVelocity;
            AnimController.Collider.LinearVelocity = new Vector2(
                NetConfig.Quantize(AnimController.Collider.LinearVelocity.X, -MaxVel, MaxVel, 12),
                NetConfig.Quantize(AnimController.Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12));
            tempBuffer.WriteRangedSingle(AnimController.Collider.LinearVelocity.X, -MaxVel, MaxVel, 12);
            tempBuffer.WriteRangedSingle(AnimController.Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12);

            AnimController.TargetMovement = new Vector2(
                NetConfig.Quantize(AnimController.TargetMovement.X, -Ragdoll.MAX_SPEED, Ragdoll.MAX_SPEED, 12),
                NetConfig.Quantize(AnimController.TargetMovement.Y, -Ragdoll.MAX_SPEED, Ragdoll.MAX_SPEED, 12));
            tempBuffer.WriteRangedSingle(AnimController.TargetMovement.X, -Ragdoll.MAX_SPEED, Ragdoll.MAX_SPEED, 12);
            tempBuffer.WriteRangedSingle(AnimController.TargetMovement.Y, -Ragdoll.MAX_SPEED, Ragdoll.MAX_SPEED, 12);

            bool fixedRotation = AnimController.Collider.FarseerBody.FixedRotation;
            tempBuffer.WriteBoolean(fixedRotation);
            if (!fixedRotation)
            {
                tempBuffer.WriteSingle(AnimController.Collider.Rotation);
                tempBuffer.WriteSingle(AnimController.Collider.AngularVelocity);
            }


            tempBuffer.WriteBoolean(AnimController.IgnorePlatforms);

            bool writeStatus = healthUpdateTimer <= 0.0f;
            tempBuffer.WriteBoolean(writeStatus);
            if (writeStatus)
            {
                WriteStatus(tempBuffer);
                tempBuffer.WriteBoolean(AIController is EnemyAIController);
                if (AIController is EnemyAIController enemyAi)
                {
                    tempBuffer.WriteByte((byte)enemyAi.State);
                    tempBuffer.WriteBoolean(enemyAi.PetBehavior is PetBehavior);
                    if (enemyAi.PetBehavior is PetBehavior petBehavior)
                    {
                        tempBuffer.WriteByte((byte)((petBehavior.Happiness / petBehavior.MaxHappiness) * byte.MaxValue));
                        tempBuffer.WriteByte((byte)((petBehavior.Hunger / petBehavior.MaxHunger) * byte.MaxValue));
                    }
                }
                HealthUpdatePending = false;
            }
        }

        public virtual void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
        {
            if (extraData is not IEventData eventData) { throw new Exception($"Malformed character event: expected {nameof(Character)}.{nameof(IEventData)}, got {extraData?.GetType().Name ?? "[NULL]"}"); }

            msg.WriteRangedInteger((int)eventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue);
            switch (eventData)
            {
                case InventoryStateEventData inventoryData:
                    msg.WriteUInt16(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0);
                    Inventory.ServerEventWrite(msg, c, inventoryData);
                    break;
                case ControlEventData controlEventData:
                    Client owner = controlEventData.Owner;
                    msg.WriteBoolean(owner == c && owner.Character == this);
                    msg.WriteByte(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.SessionId : (byte)0);
                    msg.WriteBoolean(info is { RenamingEnabled: true });
                    break;
                case CharacterStatusEventData statusEventData:
                    WriteStatus(msg, statusEventData.ForceAfflictionData);
                    msg.WriteBoolean(GodMode);
                    break;
                case UpdateSkillsEventData updateSkillsData:
                    if (Info?.Job is { } job)
                    {
                        msg.WriteIdentifier(updateSkillsData.SkillIdentifier);
                        msg.WriteBoolean(updateSkillsData.ForceNotification);
                        //don't use Character.GetSkillLevel here, because it applies all the temporary boosts from items and afflictions on the skill level
                        msg.WriteSingle(job.GetSkillLevel(updateSkillsData.SkillIdentifier));
                    }
                    else
                    {
                        msg.WriteIdentifier(Identifier.Empty);
                    }
                    break;
                case IAttackEventData attackEventData:
                    {
                        int attackLimbIndex = Removed ? -1 : Array.IndexOf(AnimController.Limbs, attackEventData.AttackLimb);
                        ushort targetEntityId = NullEntityID;
                        int targetLimbIndex = -1;
                        if (attackEventData.TargetEntity is Entity { Removed: false } targetEntity)
                        {
                            targetEntityId = targetEntity.ID;
                            if (targetEntity is Character { AnimController.Limbs: var targetLimbsArray })
                            {
                                targetLimbIndex = targetLimbsArray.IndexOf(attackEventData.TargetLimb);
                            }
                        }
                        msg.WriteByte((byte)(attackLimbIndex < 0 ? 255 : attackLimbIndex));
                        msg.WriteUInt16((ushort)targetEntityId);
                        msg.WriteByte((byte)(targetLimbIndex < 0 ? 255 : targetLimbIndex));
                        msg.WriteSingle(attackEventData.TargetSimPos.X);
                        msg.WriteSingle(attackEventData.TargetSimPos.Y);
                    }
                    break;
                case AssignCampaignInteractionEventData _:

                    bool canClientInteract = true;
                    if (CampaignInteractionType == CampaignMode.InteractionType.Talk &&
                        ActiveConversation != null)
                    {
                        canClientInteract = ActiveConversation.CanClientStartConversation(c);
                    }
                    msg.WriteByte((byte)(canClientInteract ? CampaignInteractionType : CampaignMode.InteractionType.None));
                    msg.WriteBoolean(RequireConsciousnessForCustomInteract);
                    break;
                case ObjectiveManagerStateEventData objectiveManagerStateEventData:
                    AIObjectiveManager.ObjectiveType type = objectiveManagerStateEventData.ObjectiveType;
                    msg.WriteRangedInteger((int)type, (int)AIObjectiveManager.ObjectiveType.MinValue, (int)AIObjectiveManager.ObjectiveType.MaxValue);
                    if (!(AIController is HumanAIController controller))
                    {
                        msg.WriteBoolean(false);
                        break;
                    }
                    if (type == AIObjectiveManager.ObjectiveType.Order)
                    {
                        var currentOrderInfo = controller.ObjectiveManager.GetCurrentOrderInfo();
                        bool validOrder = currentOrderInfo != null;
                        msg.WriteBoolean(validOrder);
                        if (!validOrder) { break; }
                        var orderPrefab = currentOrderInfo.Prefab;
                        msg.WriteUInt32(orderPrefab.UintIdentifier);
                        if (!orderPrefab.HasOptions) { break; }
                        int optionIndex = orderPrefab.AllOptions.IndexOf(currentOrderInfo.Option);
                        if (optionIndex == -1)
                        {
                            DebugConsole.AddWarning($"Error while writing order data. Order option \"{currentOrderInfo.Option}\" not found in the order prefab \"{orderPrefab.Name}\".");
                        }
                        msg.WriteRangedInteger(optionIndex, -1, orderPrefab.AllOptions.Length);
                    }
                    else if (type == AIObjectiveManager.ObjectiveType.Objective)
                    {
                        var objective = controller.ObjectiveManager.CurrentObjective;
                        bool validObjective = objective?.Identifier is { IsEmpty: false };
                        msg.WriteBoolean(validObjective);
                        if (!validObjective) { break; }
                        msg.WriteIdentifier(objective.Identifier);
                        msg.WriteIdentifier(objective.Option);
                        UInt16 targetEntityId = 0;
                        if (objective is AIObjectiveOperateItem operateObjective && operateObjective.OperateTarget != null)
                        {
                            targetEntityId = operateObjective.OperateTarget.ID;
                        }
                        msg.WriteUInt16(targetEntityId);
                    }
                    break;
                case TeamChangeEventData _:
                    msg.WriteByte((byte)TeamID);
                    break;
                case AddToCrewEventData addToCrewEventData:
                    msg.WriteNetSerializableStruct(addToCrewEventData.ItemTeamChange);
                    break;
                case RemoveFromCrewEventData removeFromCrewEventData:
                    msg.WriteNetSerializableStruct(removeFromCrewEventData.ItemTeamChange);
                    break;
                case UpdateExperienceEventData _:
                    msg.WriteInt32(Info.ExperiencePoints);
                    msg.WriteInt32(info.AdditionalTalentPoints);
                    break;
                case UpdateTalentsEventData _:
                    msg.WriteUInt16((ushort)characterTalents.Count);
                    foreach (var unlockedTalent in characterTalents)
                    {
                        msg.WriteBoolean(unlockedTalent.AddedThisRound);
                        msg.WriteUInt32(unlockedTalent.Prefab.UintIdentifier);
                    }
                    break;
                case UpdateMoneyEventData _:
                    msg.WriteInt32(Wallet?.Balance ?? 0);
                    break;
                case UpdateRefundPointsEventData when Info is { } i:
                    msg.WriteInt32(i.TalentRefundPoints);
                    break;
                case ConfirmRefundEventData:
                    // No data
                    break;
                case UpdatePermanentStatsEventData updatePermanentStatsEventData:

                    StatTypes statType = updatePermanentStatsEventData.StatType;
                    if (Info == null)
                    {
                        msg.WriteByte((byte)0);
                        msg.WriteByte((byte)0);
                    }
                    else if (!Info.SavedStatValues.ContainsKey(statType))
                    {
                        msg.WriteByte((byte)0);
                        msg.WriteByte((byte)statType);
                    }
                    else
                    {
                        msg.WriteByte((byte)Info.SavedStatValues[statType].Count);
                        msg.WriteByte((byte)statType);
                        foreach (var savedStatValue in Info.SavedStatValues[statType])
                        {
                            msg.WriteIdentifier(savedStatValue.StatIdentifier);
                            msg.WriteSingle(savedStatValue.StatValue);
                            msg.WriteBoolean(savedStatValue.RemoveOnDeath);
                        }
                    }
                    break;
                case LatchedOntoTargetEventData latchedOntoTargetEventData:
                    msg.WriteBoolean(latchedOntoTargetEventData.IsLatched);
                    if (latchedOntoTargetEventData.IsLatched)
                    {
                        msg.WriteSingle(SimPosition.X);
                        msg.WriteSingle(SimPosition.Y);
                        msg.WriteSingle(latchedOntoTargetEventData.AttachSurfaceNormal.X);
                        msg.WriteSingle(latchedOntoTargetEventData.AttachSurfaceNormal.Y);
                        msg.WriteSingle(latchedOntoTargetEventData.AttachPos.X);
                        msg.WriteSingle(latchedOntoTargetEventData.AttachPos.Y);
                        msg.WriteInt32(latchedOntoTargetEventData.TargetLevelWallIndex);
                        if (latchedOntoTargetEventData.TargetStructureID != NullEntityID)
                        {
                            msg.WriteUInt16(latchedOntoTargetEventData.TargetStructureID);
                        }
                        else if (latchedOntoTargetEventData.TargetCharacterID != NullEntityID)
                        {
                            msg.WriteUInt16(latchedOntoTargetEventData.TargetCharacterID);
                        }
                        else
                        {
                            msg.WriteUInt16(NullEntityID);
                        }
                    }
                    break;
                default:
                    throw new Exception($"Malformed character event: did not expect {eventData.GetType().Name}");
            }
        }

        private readonly List<int> severedJointIndices = new List<int>();

        /// <param name="forceAfflictionData">Normally full affliction data is not written for dead characters, this can be used to force them to be written</param>
        private void WriteStatus(IWriteMessage msg, bool forceAfflictionData = false)
        {
            msg.WriteBoolean(IsDead);
            if (IsDead)
            {
                msg.WriteRangedInteger((int)CauseOfDeath.Type, 0, Enum.GetValues(typeof(CauseOfDeathType)).Length - 1);
                if (CauseOfDeath.Type == CauseOfDeathType.Affliction)
                {
                    msg.WriteUInt32(CauseOfDeath.Affliction.UintIdentifier);
                }
                msg.WriteUInt16(CauseOfDeath.Killer?.ID ?? NullEntityID);
                msg.WriteBoolean(forceAfflictionData);
                if (forceAfflictionData)
                {
                    CharacterHealth.ServerWrite(msg);
                }
            }
            else
            {
                CharacterHealth.ServerWrite(msg);
            }

            if (AnimController?.LimbJoints == null)
            {
                //0 limbs severed
                msg.WriteByte((byte)0);
            }
            else
            {
                severedJointIndices.Clear();
                for (int i = 0; i < AnimController.LimbJoints.Length; i++)
                {
                    if (AnimController.LimbJoints[i] != null && AnimController.LimbJoints[i].IsSevered)
                    {
                        severedJointIndices.Add(i);
                    }
                }
                msg.WriteByte((byte)severedJointIndices.Count);
                foreach (int jointIndex in severedJointIndices)
                {
                    msg.WriteByte((byte)jointIndex);
                }
            }
        }

        public void WriteSpawnData(IWriteMessage msg, UInt16 entityId, bool restrictMessageSize)
        {
            if (GameMain.Server == null) { return; }
            
            int initialMsgLength = msg.LengthBytes;

            msg.WriteBoolean(Info == null);
            msg.WriteUInt16(entityId);
            msg.WriteIdentifier(SpeciesName);
            msg.WriteString(Seed);

            if (Removed)
            {
                msg.WriteSingle(0.0f);
                msg.WriteSingle(0.0f);
            }
            else
            {
                msg.WriteSingle(WorldPosition.X);
                msg.WriteSingle(WorldPosition.Y);
            }

            msg.WriteBoolean(Enabled);
            msg.WriteBoolean(DisabledByEvent);

            //character with no characterinfo (e.g. some monster)
            if (Info == null)
            {
                TryWriteStatus(msg);
                return;
            }

            Client ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this && (!c.SpectateOnly || !GameMain.Server.ServerSettings.AllowSpectating));
            if (ownerClient != null)
            {
                msg.WriteBoolean(true);
                msg.WriteByte(ownerClient.SessionId);
            }
            else if (GameMain.Server.Character == this)
            {
                msg.WriteBoolean(true);
                msg.WriteByte((byte)0);
            }
            else
            {
                msg.WriteBoolean(false);
            }
            msg.WriteSingle(HumanPrefabHealthMultiplier);
            msg.WriteInt32(Wallet.Balance);
            msg.WriteRangedInteger(Wallet.RewardDistribution, 0, 100);
            msg.WriteByte((byte)TeamID);
            msg.WriteBoolean(this is AICharacter);
            msg.WriteIdentifier(info.SpeciesName);
            int msgLengthBeforeInfo = msg.LengthBytes;
            info.ServerWrite(msg);
            int infoLength = msg.LengthBytes - msgLengthBeforeInfo;

            msg.WriteByte((byte)CampaignInteractionType);
            if (CampaignInteractionType == CampaignMode.InteractionType.Store)
            {
                msg.WriteIdentifier(MerchantIdentifier);
            }
            msg.WriteIdentifier(Faction);

            int msgLengthBeforeOrders = msg.LengthBytes;
            // Current orders
            msg.WriteByte((byte)info.CurrentOrders.Count(o => o != null));
            foreach (var orderInfo in info.CurrentOrders)
            {
                if (orderInfo == null) { continue; }
                msg.WriteUInt32(orderInfo.Prefab.UintIdentifier);
                msg.WriteUInt16(orderInfo.TargetEntity == null ? (UInt16)0 : orderInfo.TargetEntity.ID);
                var hasOrderGiver = orderInfo.OrderGiver != null;
                msg.WriteBoolean(hasOrderGiver);
                if (hasOrderGiver) { msg.WriteUInt16(orderInfo.OrderGiver.ID); }
                msg.WriteByte((byte)(orderInfo.Option == Identifier.Empty ? 0 : orderInfo.Prefab.Options.IndexOf(orderInfo.Option)));
                msg.WriteByte((byte)orderInfo.ManualPriority);
                var hasTargetPosition = orderInfo.TargetPosition != null;
                msg.WriteBoolean(hasTargetPosition);
                if (hasTargetPosition)
                {
                    msg.WriteSingle(orderInfo.TargetPosition.Position.X);
                    msg.WriteSingle(orderInfo.TargetPosition.Position.Y);
                    msg.WriteUInt16(orderInfo.TargetPosition.Hull == null ? (UInt16)0 : orderInfo.TargetPosition.Hull.ID);
                }
            }
            int ordersLength = msg.LengthBytes - msgLengthBeforeOrders;

            if (msg.LengthBytes - initialMsgLength >= 255 && restrictMessageSize)
            {
                DebugConsole.AddWarning($"Character spawn data for \"{Name}\" exceeded 255 bytes (info: {infoLength}, orders: {ordersLength}, total: {msg.LengthBytes - initialMsgLength})");
            }

            TryWriteStatus(msg);

            void TryWriteStatus(IWriteMessage msg)
            {
                int msgLengthBeforeStatus = msg.LengthBytes - initialMsgLength;

                var tempBuffer = new ReadWriteMessage();
                WriteStatus(tempBuffer, forceAfflictionData: true);
                if (msgLengthBeforeStatus + tempBuffer.LengthBytes >= 255 && restrictMessageSize && GameMain.LuaCs.Networking.RestrictMessageSize)
                { 
                    msg.WriteBoolean(false);
                    if (msgLengthBeforeStatus < 255)
                    {
                        DebugConsole.ThrowError($"Character spawn data for \"{Name}\" caused the length of the message to exceed 255 bytes ({msgLengthBeforeStatus} + {tempBuffer.LengthBytes})");
                    }
                }
                else
                {
                    msg.WriteBoolean(true);
                    WriteStatus(msg, forceAfflictionData: true);
                }
            }

            DebugConsole.Log("Character spawn message length: " + (msg.LengthBytes - initialMsgLength));
        }
    }
}
