using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using Prometheus;
using Robust.Shared.Console;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Profiling;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Timing;
using Robust.Shared.Utility;

namespace Robust.Shared.GameObjects
{
    public delegate void EntityUidQueryCallback(EntityUid uid);

    public delegate void ComponentQueryCallback<T>(EntityUid uid, T component) where T : IComponent;

    /// <inheritdoc />
    [Virtual]
    public abstract partial class EntityManager : IEntityManager
    {
        #region Dependencies

        [IoC.Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
        [IoC.Dependency] protected readonly ILogManager LogManager = default!;
        [IoC.Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
        [IoC.Dependency] private readonly IMapManager _mapManager = default!;
        [IoC.Dependency] private readonly IGameTiming _gameTiming = default!;
        [IoC.Dependency] private readonly ISerializationManager _serManager = default!;
        [IoC.Dependency] private readonly ProfManager _prof = default!;
        [IoC.Dependency] private readonly INetManager _netMan = default!;
        [IoC.Dependency] private readonly IReflectionManager _reflection = default!;
        [IoC.Dependency] private readonly EntityConsoleHost _entityConsoleHost = default!;

        // I feel like PJB might shed me for putting a system dependency here, but its required for setting entity
        // positions on spawn....
        protected SharedTransformSystem _xforms = default!;
        private SharedContainerSystem _containers = default!;

        public EntityQuery<MetaDataComponent> MetaQuery;
        public EntityQuery<TransformComponent> TransformQuery;
        private EntityQuery<PhysicsComponent> _physicsQuery;
        private EntityQuery<ActorComponent> _actorQuery;

        #endregion Dependencies

        /// <inheritdoc />
        public GameTick CurrentTick => _gameTiming.CurTick;

        public static readonly MapInitEvent MapInitEventInstance = new();

        IComponentFactory IEntityManager.ComponentFactory => ComponentFactory;

        /// <inheritdoc />
        public IEntitySystemManager EntitySysManager => _entitySystemManager;

        /// <inheritdoc />
        public abstract IEntityNetworkManager EntityNetManager { get; }

        protected readonly Queue<EntityUid> QueuedDeletions = new();
        protected readonly HashSet<EntityUid> QueuedDeletionsSet = new();

        private EntityDiffContext _context = new();

        /// <summary>
        ///     All entities currently stored in the manager.
        /// </summary>
        protected readonly HashSet<EntityUid> Entities = new();

        internal EntityEventBus EventBusInternal = null!;

        protected int NextEntityUid = (int) EntityUid.FirstUid;

        protected int NextNetworkId = (int) NetEntity.First;

        /// <inheritdoc />
        public IEventBus EventBus => EventBusInternal;

        public event Action<Entity<MetaDataComponent>>? EntityAdded;
        public event Action<Entity<MetaDataComponent>>? EntityInitialized;
        public event Action<Entity<MetaDataComponent>>? EntityDeleted;

        /// <summary>
        /// Internal termination event handlers. This is mainly for exception tolerance, we want to ensure that PVS,
        /// and other important engine systems can get updated before some content code throws an exception.
        /// </summary>
        internal event TerminatingEventHandler? BeforeEntityTerminating;
        public delegate void TerminatingEventHandler(ref EntityTerminatingEvent ev);

        public event Action? BeforeEntityFlush;
        public event Action? AfterEntityFlush;

        /// <summary>
        /// Raised when an entity is queued for deletion. Not raised if an entity is deleted.
        /// </summary>
        public event Action<EntityUid>? EntityQueueDeleted;
        public event Action<Entity<MetaDataComponent>>? EntityDirtied;

        private string _xformName = string.Empty;

        private ComponentRegistration _metaReg = default!;
        private ComponentRegistration _xformReg = default!;

        private SharedMapSystem _mapSystem = default!;

        private ISawmill _sawmill = default!;
        internal ISawmill ResolveSawmill = default!;

        public bool Started { get; protected set; }

        public bool ShuttingDown { get; protected set; }

        public bool Initialized { get; protected set; }

#if DEBUG
        private int _mainThreadId;
#endif

        /// <summary>
        /// Constructs a new instance of <see cref="EntityManager"/>.
        /// </summary>
        public EntityManager()
        {
        }

        public virtual void Initialize()
        {
            if (Initialized)
                throw new InvalidOperationException("Initialize() called multiple times");

            EventBusInternal = new EntityEventBus(this, _reflection);

            InitializeComponents();
            _metaReg = _componentFactory.GetRegistration(typeof(MetaDataComponent));
            _xformReg = _componentFactory.GetRegistration(typeof(TransformComponent));
            _xformName = _xformReg.Name;
            _sawmill = LogManager.GetSawmill("entity");
            ResolveSawmill = LogManager.GetSawmill("resolve");

#if DEBUG
            _mainThreadId = Environment.CurrentManagedThreadId;
#endif

            Initialized = true;
        }

        /// <summary>
        /// Returns true if the entity's data (apart from transform) is default.
        /// </summary>
        public bool IsDefault(EntityUid uid, ICollection<string>? ignoredComps = null)
        {
            if (!MetaQuery.TryGetComponent(uid, out var metadata) || metadata.EntityPrototype == null)
                return false;

            var prototype = metadata.EntityPrototype;

            // Check if entity name / description match
            if (metadata.EntityName != prototype.Name ||
                metadata.EntityDescription != prototype.Description)
            {
                return false;
            }

            var protoData = PrototypeManager.GetPrototypeData(prototype);
            var comps = _entCompIndex[uid];

            // Fast check if the component counts match.
            // Note that transform and metadata are not included in the prototype data.
            if (protoData.Count + 2 != comps.Count)
                return false;

            foreach (var component in comps)
            {
                if (component.Deleted)
                    return false;

                var compType = component.GetType();

                if (compType == typeof(TransformComponent) || compType == typeof(MetaDataComponent))
                    continue;

                var compName = _componentFactory.GetComponentName(compType);

                if (ignoredComps?.Contains(compName) == true)
                    continue;

                // If the component isn't on the prototype then it's custom.
                if (!protoData.TryGetValue(compName, out var protoMapping))
                    return false;

                MappingDataNode compMapping;
                try
                {
                    compMapping = _serManager.WriteValueAs<MappingDataNode>(compType, component, alwaysWrite: true, context: _context);
                }
                catch (Exception e)
                {
                    _sawmill.Error($"Failed to serialize {compName} component of entity prototype {prototype.ID}. Exception: {e.Message}");
#if !EXCEPTION_TOLERANCE
                    throw;
#else
                    return false;
#endif
                }

                if (compMapping.AnyExcept(protoMapping))
                    return false;
            }

            return true;
        }

        public virtual void Startup()
        {
            if(!Initialized)
                throw new InvalidOperationException("Startup() called without Initialized");
            if (Started)
                throw new InvalidOperationException("Startup() called multiple times");

            // TODO: Probably better to call this on its own given it's so infrequent.
            _entitySystemManager.Initialize();
            Started = true;
            EventBusInternal.LockSubscriptions();
            _mapSystem = System<SharedMapSystem>();
            _xforms = System<SharedTransformSystem>();
            _containers = System<SharedContainerSystem>();
            MetaQuery = GetEntityQuery<MetaDataComponent>();
            TransformQuery = GetEntityQuery<TransformComponent>();
            _physicsQuery = GetEntityQuery<PhysicsComponent>();
            _actorQuery = GetEntityQuery<ActorComponent>();
            _entityConsoleHost.Startup();
        }

        public virtual void Shutdown()
        {
            ShuttingDown = true;
            FlushEntities();
            EventBusInternal.ClearSubscriptions();
            _entitySystemManager.Shutdown();
            ClearComponents();
            ShuttingDown = false;
            Started = false;
            _entityConsoleHost.Shutdown();
        }

        public virtual void Cleanup()
        {
            _componentFactory.ComponentsAdded -= OnComponentsAdded;
            ShuttingDown = true;
            FlushEntities();
            _entitySystemManager.Clear();
            EventBusInternal.Dispose();
            EventBusInternal = null!;
            ClearComponents();

            ShuttingDown = false;
            Initialized = false;
            Started = false;
        }

        public virtual void TickUpdate(float frameTime, bool noPredictions, Histogram? histogram)
        {
            using (histogram?.WithLabels("EntitySystems").NewTimer())
            using (_prof.Group("Systems"))
            {
                _entitySystemManager.TickUpdate(frameTime, noPredictions);
            }

            using (histogram?.WithLabels("EntityEventBus").NewTimer())
            using (_prof.Group("Events"))
            {
                EventBusInternal.ProcessEventQueue();
            }

            using (histogram?.WithLabels("QueuedDeletion").NewTimer())
            using (_prof.Group("QueueDel"))
            {
                ProcessQueueudDeletions();
            }

            using (histogram?.WithLabels("ComponentCull").NewTimer())
            using (_prof.Group("ComponentCull"))
            {
                CullRemovedComponents();
            }
        }

        internal virtual void ProcessQueueudDeletions()
        {
            while (QueuedDeletions.TryDequeue(out var uid))
            {
                DeleteEntity(uid);
            }

            QueuedDeletionsSet.Clear();
        }

        public virtual void FrameUpdate(float frameTime)
        {
            _entitySystemManager.FrameUpdate(frameTime);
        }

        #region Entity Management

        /// <inheritdoc />
        public EntityUid CreateEntityUninitialized(string? prototypeName, EntityUid euid, ComponentRegistry? overrides = null)
        {
            return CreateEntity(prototypeName, out _, overrides);
        }

        /// <inheritdoc />
        public EntityUid CreateEntityUninitialized(string? prototypeName, ComponentRegistry? overrides = null)
        {
            return CreateEntity(prototypeName, out _, overrides);
        }

        public EntityUid CreateEntityUninitialized(string? prototypeName, out MetaDataComponent meta, ComponentRegistry? overrides = null)
        {
            return CreateEntity(prototypeName, out meta, overrides);
        }

        /// <inheritdoc />
        public virtual EntityUid CreateEntityUninitialized(string? prototypeName, EntityCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default)
        {
            var newEntity = CreateEntity(prototypeName, out _, overrides);

            var xformComp = TransformQuery.GetComponent(newEntity);
            _xforms.SetCoordinates(newEntity, xformComp, coordinates, rotation: rotation, unanchor: false);
            return newEntity;
        }

        /// <inheritdoc />
        public virtual EntityUid CreateEntityUninitialized(string? prototypeName, MapCoordinates coordinates, ComponentRegistry? overrides = null, Angle rotation = default!)
        {
            var newEntity = CreateEntity(prototypeName, out _, overrides);
            var transform = TransformQuery.GetComponent(newEntity);

            if (coordinates.MapId == MapId.Nullspace)
            {
                transform._parent = EntityUid.Invalid;
                transform.Anchored = false;
                return newEntity;
            }

            var mapEnt = _mapSystem.GetMap(coordinates.MapId);
            if (!TryGetComponent(mapEnt, out TransformComponent? mapXform))
                throw new ArgumentException($"Attempted to spawn entity on an invalid map. Coordinates: {coordinates}");

            EntityCoordinates coords;
            if (_mapManager.TryFindGridAt(coordinates, out var gridUid, out var grid)
                && MetaQuery.TryGetComponentInternal(gridUid, out var meta)
                && meta.EntityLifeStage < EntityLifeStage.Terminating)
            {
                coords = new EntityCoordinates(gridUid, _mapSystem.WorldToLocal(gridUid, grid, coordinates.Position));
                _xforms.SetCoordinates(newEntity, transform, coords, rotation, unanchor: false);
            }
            else
            {
                coords = new EntityCoordinates(mapEnt, coordinates.Position);
                _xforms.SetCoordinates(newEntity, transform, coords, rotation, newParent: mapXform);
            }

            return newEntity;
        }

        /// <inheritdoc />
        public int EntityCount => Entities.Count;

        /// <inheritdoc />
        public IEnumerable<EntityUid> GetEntities() => Entities;

        /// <inheritdoc />
        public virtual void DirtyEntity(EntityUid uid, MetaDataComponent? metadata = null)
        {
            // We want to retrieve MetaDataComponent even if its Deleted flag is set.
            if (!MetaQuery.ResolveInternal(uid, ref metadata))
                return;

            if (metadata.EntityLastModifiedTick == _gameTiming.CurTick)
                return;

            metadata.EntityLastModifiedTick = _gameTiming.CurTick;

            if (metadata.EntityLifeStage > EntityLifeStage.Initializing)
            {
                EntityDirtied?.Invoke((uid, metadata));
            }
        }

        /// <inheritdoc />
        [Obsolete("use override with an EntityUid or Entity<T>")]
        public void Dirty(IComponent component, MetaDataComponent? meta = null)
        {
            Dirty(component.Owner, component, meta);
        }

        /// <inheritdoc />
        public virtual void Dirty(EntityUid uid, IComponent component, MetaDataComponent? meta = null)
        {
            DebugTools.Assert(component.GetType().HasCustomAttribute<NetworkedComponentAttribute>(),
                $"Attempted to dirty a non-networked component: {component.GetType()}");
            DebugTools.AssertOwner(uid, component);

            if (component.LifeStage >= ComponentLifeStage.Removing || !component.NetSyncEnabled)
                return;

            if (component.LastModifiedTick == CurrentTick)
                return;

            DirtyEntity(uid, meta);
            component.LastModifiedTick = CurrentTick;
        }

        /// <inheritdoc />
        public virtual void Dirty<T>(Entity<T> ent, MetaDataComponent? meta = null) where T : IComponent
        {
            DebugTools.Assert(ent.Comp.GetType().HasCustomAttribute<NetworkedComponentAttribute>(),
                $"Attempted to dirty a non-networked component: {ent.Comp.GetType()}");

            if (ent.Comp.LifeStage >= ComponentLifeStage.Removing || !ent.Comp.NetSyncEnabled)
                return;

            if (ent.Comp.LastModifiedTick == CurrentTick)
                return;

            DirtyEntity(ent, meta);
            ent.Comp.LastModifiedTick = CurrentTick;
        }

        /// <inheritdoc />
        public virtual void Dirty<T1, T2>(Entity<T1, T2> ent, MetaDataComponent? meta = null)
            where T1 : IComponent
            where T2 : IComponent
        {
            DebugTools.Assert(ent.Comp1.GetType().HasCustomAttribute<NetworkedComponentAttribute>(),
                $"Attempted to dirty a non-networked component: {ent.Comp1.GetType()}");
            DebugTools.Assert(ent.Comp2.GetType().HasCustomAttribute<NetworkedComponentAttribute>(),
                $"Attempted to dirty a non-networked component: {ent.Comp2.GetType()}");

            // We're not gonna bother checking ent.Comp.NetSyncEnabled
            // chances are at least one of these components didn't get net-sync disabled.
            DirtyEntity(ent, meta);
            ent.Comp1.LastModifiedTick = CurrentTick;
            ent.Comp2.LastModifiedTick = CurrentTick;
        }

        /// <inheritdoc />
        public virtual void Dirty<T1, T2, T3>(Entity<T1, T2, T3> ent, MetaDataComponent? meta = null)
            where T1 : IComponent
            where T2 : IComponent
            where T3 : IComponent
        {
            DebugTools.Assert(ent.Comp1.GetType().HasCustomAttribute<NetworkedComponentAttribute>(),
                $"Attempted to dirty a non-networked component: {ent.Comp1.GetType()}");
            DebugTools.Assert(ent.Comp2.GetType().HasCustomAttribute<NetworkedComponentAttribute>(),
                $"Attempted to dirty a non-networked component: {ent.Comp2.GetType()}");
            DebugTools.Assert(ent.Comp3.GetType().HasCustomAttribute<NetworkedComponentAttribute>(),
                $"Attempted to dirty a non-networked component: {ent.Comp3.GetType()}");

            // We're not gonna bother checking ent.Comp.NetSyncEnabled
            // chances are at least one of these components didn't get net-sync disabled.
            DirtyEntity(ent, meta);
            ent.Comp1.LastModifiedTick = CurrentTick;
            ent.Comp2.LastModifiedTick = CurrentTick;
            ent.Comp3.LastModifiedTick = CurrentTick;
        }

        /// <inheritdoc />
        public virtual void Dirty<T1, T2, T3, T4>(Entity<T1, T2, T3, T4> ent, MetaDataComponent? meta = null)
            where T1 : IComponent
            where T2 : IComponent
            where T3 : IComponent
            where T4 : IComponent
        {
            DebugTools.Assert(ent.Comp1.GetType().HasCustomAttribute<NetworkedComponentAttribute>(),
                $"Attempted to dirty a non-networked component: {ent.Comp1.GetType()}");
            DebugTools.Assert(ent.Comp2.GetType().HasCustomAttribute<NetworkedComponentAttribute>(),
                $"Attempted to dirty a non-networked component: {ent.Comp2.GetType()}");
            DebugTools.Assert(ent.Comp3.GetType().HasCustomAttribute<NetworkedComponentAttribute>(),
                $"Attempted to dirty a non-networked component: {ent.Comp3.GetType()}");
            DebugTools.Assert(ent.Comp4.GetType().HasCustomAttribute<NetworkedComponentAttribute>(),
                $"Attempted to dirty a non-networked component: {ent.Comp4.GetType()}");

            // We're not gonna bother checking ent.Comp.NetSyncEnabled
            // chances are at least one of these components didn't get net-sync disabled.
            DirtyEntity(ent, meta);
            ent.Comp1.LastModifiedTick = CurrentTick;
            ent.Comp2.LastModifiedTick = CurrentTick;
            ent.Comp3.LastModifiedTick = CurrentTick;
            ent.Comp4.LastModifiedTick = CurrentTick;
        }

        public bool TryQueueDeleteEntity(EntityUid? uid)
        {
            if (uid == null)
                return false;

            if (Deleted(uid.Value))
                return false;

            if (QueuedDeletionsSet.Contains(uid.Value))
                return false;

            QueueDeleteEntity(uid);
            return true;
        }

        /// <summary>
        /// Shuts-down and removes given Entity. This is also broadcast to all clients.
        /// </summary>
        public virtual void DeleteEntity(EntityUid? uid)
        {
            if (uid == null)
                return;

            // Some UIs get disposed after entity-manager has shut down and already deleted all entities.
            if (!Started)
                return;

            // Networking blindly spams entities at this function, they can already be
            // deleted from being a child of a previously deleted entity
            // TODO: Why does networking need to send deletes for child entities?
            if (MetaQuery.TryGetComponent(uid.Value, out var meta))
                DeleteEntity(uid.Value, meta, TransformQuery.GetComponent(uid.Value));
        }

        /// <summary>
        /// Shuts-down and removes given Entity. This is also broadcast to all clients.
        /// </summary>
        public void DeleteEntity(EntityUid e, MetaDataComponent meta, TransformComponent xform)
        {
            // Some UIs get disposed after entity-manager has shut down and already deleted all entities.
            if (!Started)
                return;

            ThreadCheck();

            if (meta.EntityLifeStage >= EntityLifeStage.Deleted)
                return;

            if (meta.EntityLifeStage == EntityLifeStage.Terminating)
            {
                var msg = $"Called Delete on an entity already being deleted. Entity: {ToPrettyString(e)}";
#if !EXCEPTION_TOLERANCE
                throw new InvalidOperationException(msg);
#else
                _sawmill.Error($"{msg}. Trace: {Environment.StackTrace}");
#endif
            }

            // Notify all entities they are being terminated prior to detaching & deleting
            RecursiveFlagEntityTermination(e, meta, xform);

            TransformComponent? parentXform = null;
            if (xform.ParentUid.IsValid())
            {
                if (xform.LifeStage < ComponentLifeStage.Initialized)
                {
                    // Entity is being deleted before initialization ever finished.
                    // The entity will not yet have been added to the parent's transform component.
                    // This is seemingly pretty error prone ATM, and I'm not even sure if it should be supported?

                    // Just in case it HAS somehow been added, make sure we remove it.
                    if (TransformQuery.TryComp(xform.ParentUid, out parentXform) && parentXform._children.Remove(e))
                        DebugTools.Assert($"Child entity {ToPrettyString(e)} was added to the parent's child set prior to being initialized?");

                    parentXform = null;
                    xform._parent = EntityUid.Invalid;
                    xform._anchored = false;
                }
                else
                {
                    // Use resolve for automatic error logging.
                    // ReSharper disable once ReturnValueOfPureMethodIsNotUsed
                    TransformQuery.Resolve(xform.ParentUid, ref parentXform);
                }
            }

            // Then actually delete them
            RecursiveDeleteEntity(e, meta, xform, parentXform);
        }

        private void RecursiveFlagEntityTermination(EntityUid uid,
            MetaDataComponent metadata,
            TransformComponent xform)
        {
            DebugTools.Assert(metadata.EntityLifeStage < EntityLifeStage.Terminating);
            SetLifeStage(metadata, EntityLifeStage.Terminating);

            try
            {
                var ev = new EntityTerminatingEvent((uid, metadata));
                BeforeEntityTerminating?.Invoke(ref ev);
                EventBusInternal.RaiseLocalEvent(uid, ref ev, true);
            }
            catch (Exception e)
            {
                _sawmill.Error($"Caught exception while raising event {nameof(EntityTerminatingEvent)} on entity {ToPrettyString(uid, metadata)}\n{e}");
#if !EXCEPTION_TOLERANCE
                throw;
#endif
            }

            foreach (var child in xform._children)
            {
                if (!MetaQuery.TryGetComponent(child, out var childMeta) || childMeta.EntityDeleted)
                {
                    _sawmill.Error($"A deleted entity was still the transform child of another entity. Parent: {ToPrettyString(uid, metadata)}.");
                    xform._children.Remove(child);
                    continue;
                }

                RecursiveFlagEntityTermination(child, childMeta, TransformQuery.GetComponent(child));
            }
        }

        private void RecursiveDeleteEntity(
            EntityUid uid,
            MetaDataComponent metadata,
            TransformComponent transform,
            TransformComponent? parentXform)
        {
            DebugTools.Assert(transform.ParentUid.IsValid() == (parentXform != null));
            DebugTools.Assert(parentXform == null || parentXform._children.Contains(uid));

            // Note about this method: #if EXCEPTION_TOLERANCE is not used here because we're gonna it in the future...

            // Detach the base entity to null before iterating over children
            // This also ensures that the entity-lookup updates don't have to be re-run for every child (which recurses up the transform hierarchy).
            _xforms.DetachEntity(uid, transform, metadata, parentXform, true);

            foreach (var child in transform._children)
            {
                try
                {
                    var childMeta = MetaQuery.GetComponent(child);
                    var childXform = TransformQuery.GetComponent(child);
                    DebugTools.AssertEqual(childXform.ParentUid, uid);
                    RecursiveDeleteEntity(child, childMeta, childXform, transform);
                }
                catch(Exception e)
                {
                    _sawmill.Error($"Caught exception while trying to recursively delete child entity '{ToPrettyString(child)}' of '{ToPrettyString(uid, metadata)}'\n{e}");
#if !EXCEPTION_TOLERANCE
                    throw;
#endif
                }
            }

            if (transform._children.Count != 0)
                _sawmill.Error($"Failed to delete all children of entity: {ToPrettyString(uid)}");

            // Shut down all components.
            foreach (var component in InSafeOrder(_entCompIndex[uid]))
            {
                if (component.Running)
                {
                    try
                    {
                        LifeShutdown(uid, component, _componentFactory.GetIndex(component.GetType()));
                    }
                    catch (Exception e)
                    {
                        _sawmill.Error($"Caught exception while trying to call shutdown on component of entity '{ToPrettyString(uid, metadata)}'\n{e}");
#if !EXCEPTION_TOLERANCE
                        throw;
#endif
                    }
                }
            }

            // Dispose all my components, in a safe order so transform is available
            DisposeComponents(uid, metadata);
            SetLifeStage(metadata, EntityLifeStage.Deleted);

            try
            {
                EntityDeleted?.Invoke((uid, metadata));
            }
            catch (Exception e)
            {
                _sawmill.Error($"Caught exception while invoking event {nameof(EntityDeleted)} on '{ToPrettyString(uid, metadata)}'\n{e}");
#if !EXCEPTION_TOLERANCE
                throw;
#endif
            }

            EventBusInternal.OnEntityDeleted(uid);
            Entities.Remove(uid);
            // Need to get the ID above before MetadataComponent shutdown but only remove it after everything else is done.
            NetEntityLookup.Remove(metadata.NetEntity);
        }

        public virtual void QueueDeleteEntity(EntityUid? uid)
        {
            if (uid == null)
                return;

            if (!QueuedDeletionsSet.Add(uid.Value))
                return;

            QueuedDeletions.Enqueue(uid.Value);
            EntityQueueDeleted?.Invoke(uid.Value);
        }

        public virtual bool IsQueuedForDeletion(EntityUid uid) => QueuedDeletionsSet.Contains(uid);

        /// <inheritdoc />
        public virtual void PredictedDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
        {
            DeleteEntity(ent.Owner);
        }

        /// <inheritdoc />
        public void PredictedDeleteEntity(Entity<MetaDataComponent?, TransformComponent?>? ent)
        {
            if (ent == null)
                return;

            PredictedDeleteEntity(ent.Value);
        }

        /// <inheritdoc />
        public virtual void PredictedQueueDeleteEntity(Entity<MetaDataComponent?> ent)
        {
            QueueDeleteEntity(ent);
        }

        /// <inheritdoc />
        public void PredictedQueueDeleteEntity(Entity<MetaDataComponent?>? ent)
        {
            if (ent != null)
                PredictedQueueDeleteEntity(ent.Value);
        }

        /// <inheritdoc />
        [Obsolete("use variant without TransformComponent")]
        public void PredictedQueueDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
        {
            PredictedQueueDeleteEntity(new Entity<MetaDataComponent?>(ent.Owner, ent.Comp1));
        }

        /// <inheritdoc />
        [Obsolete("use variant without TransformComponent")]
        public void PredictedQueueDeleteEntity(Entity<MetaDataComponent?, TransformComponent?>? ent)
        {
            if (ent != null)
                PredictedQueueDeleteEntity(new Entity<MetaDataComponent?>(ent.Value.Owner, ent.Value.Comp1));
        }

        /// <inheritdoc />
        public void PredictedQueueDeleteEntity(EntityUid uid)
        {
            PredictedQueueDeleteEntity(new Entity<MetaDataComponent?>(uid, null));
        }

        /// <inheritdoc />
        public void PredictedQueueDeleteEntity(EntityUid? uid)
        {
            if (uid != null)
                PredictedQueueDeleteEntity(new Entity<MetaDataComponent?>(uid.Value, null));
        }

        public bool EntityExists(EntityUid uid)
        {
            return MetaQuery.HasComponentInternal(uid);
        }

        public bool EntityExists(EntityUid? uid)
        {
            return uid.HasValue && EntityExists(uid.Value);
        }

        /// <inheritdoc />
        public bool IsPaused(EntityUid? uid, MetaDataComponent? metadata = null)
        {
            if (uid == null)
                return false;

            return MetaQuery.Resolve(uid.Value, ref metadata) && metadata.EntityPaused;
        }

        public bool Deleted(EntityUid uid)
        {
            return !MetaQuery.TryGetComponentInternal(uid, out var comp) || comp.EntityDeleted;
        }

        public bool Deleted([NotNullWhen(false)] EntityUid? uid)
        {
            return !uid.HasValue || !MetaQuery.TryGetComponentInternal(uid.Value, out var comp) || comp.EntityDeleted;
        }

        /// <summary>
        /// Disposes all entities and clears all lists.
        /// </summary>
        public virtual void FlushEntities()
        {
            _sawmill.Info($"Flushing entities. Entity count: {Entities.Count}");
            BeforeEntityFlush?.Invoke();
            FlushEntitiesInternal();

            if (Entities.Count != 0)
            {
                _sawmill.Error($"Failed to flush all entities. Entity count: {Entities.Count}");
                // Dump entity info, but avoid dumping ~50k errors if for whatever reason we failed to delete almost all entities.
                // Using 512 as the limit, in case the problem entities are related to player counts on high-pop servers.
                if (Entities.Count < 512)
                {
                    foreach (var uid in Entities)
                    {
                        _sawmill.Error($"Entity exists after flush: {ToPrettyString(uid)}");
                    }
                }
            }

#if EXCEPTION_TOLERANCE
            // Attempt to flush entities a second time, just in case something somehow caused an entity to be spawned
            // while flushing entities
            FlushEntitiesInternal();
#endif

            if (Entities.Count != 0)
                throw new Exception($"Failed to flush all entities. Entity count: {Entities.Count}");

            AfterEntityFlush?.Invoke();
        }

        private void FlushEntitiesInternal()
        {
            QueuedDeletions.Clear();
            QueuedDeletionsSet.Clear();

            // First, we directly delete all maps. This will delete most entities while reducing the number of component
            // lookups

            var maps = _entTraitDict[typeof(MapComponent)].Keys.ToArray();
            foreach (var map in maps)
            {
                try
                {
                    DeleteEntity(map);
                }
                catch (Exception e)
                {
                    _sawmill.Log(LogLevel.Error, e,
                        $"Caught exception while trying to delete map entity {ToPrettyString(map)}, this might corrupt the game state...");
#if !EXCEPTION_TOLERANCE
                    throw;
#endif
                }
            }

            // Then delete all other entities.
            var ents = _entTraitDict[typeof(MetaDataComponent)].ToArray();
            DebugTools.Assert(ents.Length == Entities.Count);
            foreach (var (uid, comp) in ents)
            {
                var meta = (MetaDataComponent) comp;
                if (meta.EntityLifeStage >= EntityLifeStage.Terminating)
                    continue;

                try
                {
                    DeleteEntity(uid, meta, TransformQuery.GetComponent(uid));
                }
                catch (Exception e)
                {
                    _sawmill.Log(LogLevel.Error, e,
                        $"Caught exception while trying to delete entity {ToPrettyString(uid, meta)}, this might corrupt the game state...");
#if !EXCEPTION_TOLERANCE
                    throw;
#endif
                }
            }
        }

        /// <summary>
        ///     Allocates an entity and stores it but does not load components or do initialization.
        /// </summary>
        protected internal EntityUid AllocEntity(
            EntityPrototype? prototype,
            out MetaDataComponent metadata)
        {
            var entity = AllocEntity(out metadata);
            metadata._entityPrototype = prototype;
            Dirty(entity, metadata, metadata);
            return entity;
        }

        /// <inheritdoc cref="AllocEntity(Robust.Shared.Prototypes.EntityPrototype?,out Robust.Shared.GameObjects.MetaDataComponent)"/>
        internal EntityUid AllocEntity(EntityPrototype? prototype) => AllocEntity(prototype, out _);

        /// <summary>
        ///     Allocates an entity and stores it but does not load components or do initialization.
        /// </summary>
        private EntityUid AllocEntity(out MetaDataComponent metadata)
        {
            ThreadCheck();

            var uid = GenerateEntityUid();

#if DEBUG
            if (EntityExists(uid))
            {
                throw new InvalidOperationException($"UID already taken: {uid}");
            }
#endif

            metadata = new MetaDataComponent
            {
#pragma warning disable CS0618
                Owner = uid,
#pragma warning restore CS0618
                EntityLastModifiedTick = _gameTiming.CurTick
            };

            var netEntity = GenerateNetEntity();
            SetNetEntity(uid, netEntity, metadata);

            // we want this called before adding components
            EntityAdded?.Invoke((uid, metadata));
            EventBusInternal.OnEntityAdded(uid);

            Entities.Add(uid);
            // add the required MetaDataComponent directly.
            AddComponentInternal(uid, metadata, _metaReg, false, true, metadata);

            // allocate the required TransformComponent
            var xformComp = Unsafe.As<TransformComponent>(_componentFactory.GetComponent(_xformReg));
#pragma warning disable CS0618 // Type or member is obsolete
            xformComp.Owner = uid;
#pragma warning restore CS0618 // Type or member is obsolete
            AddComponentInternal(uid, xformComp, false, true, metadata);

            return uid;
        }

        /// <summary>
        ///     Allocates an entity and loads components but does not do initialization.
        /// </summary>
        internal virtual EntityUid CreateEntity(string? prototypeName, out MetaDataComponent metadata, IEntityLoadContext? context = null)
        {
            if (prototypeName == null)
                return AllocEntity(out metadata);

            if (!PrototypeManager.TryIndex<EntityPrototype>(prototypeName, out var prototype))
                throw new EntityCreationException($"Attempted to spawn an entity with an invalid prototype: {prototypeName}");

            return CreateEntity(prototype, out metadata, context);
        }

        /// <summary>
        ///     Allocates an entity and loads components but does not do initialization.
        /// </summary>
        private protected EntityUid CreateEntity(EntityPrototype prototype, out MetaDataComponent metadata, IEntityLoadContext? context = null)
        {
            var entity = AllocEntity(prototype, out metadata);
            try
            {
                EntityPrototype.LoadEntity((entity, metadata), ComponentFactory, this, _serManager, context);
                return entity;
            }
            catch (Exception e)
            {
                // Exception during entity loading.
                // Need to delete the entity to avoid corrupt state causing crashes later.
                DeleteEntity(entity);
                throw new EntityCreationException($"Exception inside CreateEntity with prototype {prototype.ID}", e);
            }
        }

        public void InitializeAndStartEntity(EntityUid entity, MapId? mapId = null)
        {
            var doMapInit = _mapSystem.IsInitialized(mapId ?? TransformQuery.GetComponent(entity).MapID);
            InitializeAndStartEntity(entity, doMapInit);
        }

        public void InitializeAndStartEntity(Entity<MetaDataComponent?> entity, bool doMapInit)
        {
            if (!MetaQuery.Resolve(entity.Owner, ref entity.Comp))
                return;

            try
            {
                InitializeEntity(entity.Owner, entity.Comp);
                StartEntity(entity.Owner);

                if (doMapInit)
                    RunMapInit(entity.Owner, entity.Comp);
            }
            catch (Exception e)
            {
                DeleteEntity(entity);
                throw new EntityCreationException("Exception inside InitializeAndStartEntity", e);
            }
        }

        public void InitializeEntity(EntityUid entity, MetaDataComponent? meta = null)
        {
            // Ideally, entities only ever get initialized once their parent has already been initialized.
            // Note that this doesn't guarantee that an uninitialized entity will never have initialized children.
            // In particular, for the client this might happen when applying a new game state that re-parents an
            // existing entity to a newly created entity. The new entity only gets initialiuzed & started at the end,
            // after the old/existing entity was already moved to the new parent.
            DebugTools.Assert(TransformQuery.GetComponent(entity).ParentUid is not { Valid: true } parent
                || MetaQuery.GetComponent(parent).EntityLifeStage >= EntityLifeStage.Initialized);

            DebugTools.AssertOwner(entity, meta);
            meta ??= GetComponent<MetaDataComponent>(entity);
#pragma warning disable CS0618 // Type or member is obsolete
            InitializeComponents(entity, meta);
#pragma warning restore CS0618 // Type or member is obsolete
            EntityInitialized?.Invoke((entity, meta));
        }

        public void StartEntity(EntityUid entity)
        {
            // Ideally, entities only ever get initialized once their parent has already been initialized.
            // Note that this doesn't guarantee that an uninitialized entity will never have initialized children.
            // In particular, for the client this might happen when applying a new game state that re-parents an
            // existing entity to a newly created entity. The new entity only gets initialiuzed & started at the end,
            // after the old/existing entity was already moved to the new parent.
            DebugTools.Assert(TransformQuery.GetComponent(entity).ParentUid is not { Valid: true } parent
                              || MetaQuery.GetComponent(parent).EntityLifeStage >= EntityLifeStage.Initialized);

#pragma warning disable CS0618 // Type or member is obsolete
            StartComponents(entity);
#pragma warning restore CS0618 // Type or member is obsolete
        }

        public void RunMapInit(EntityUid entity, MetaDataComponent meta)
        {
            if (meta.EntityLifeStage == EntityLifeStage.MapInitialized)
                return; // Already map initialized, do nothing.

            DebugTools.Assert(meta.EntityLifeStage == EntityLifeStage.Initialized, $"Expected entity {ToPrettyString(entity)} to be initialized, was {meta.EntityLifeStage}");
            SetLifeStage(meta, EntityLifeStage.MapInitialized);

            EventBusInternal.RaiseLocalEvent(entity, MapInitEventInstance);
        }

        /// <inheritdoc />
        [return: NotNullIfNotNull("uid")]
        public EntityStringRepresentation? ToPrettyString(EntityUid? uid, MetaDataComponent? metadata = null)
        {
            return uid == null ? null : ToPrettyString(uid.Value, metadata);
        }

        /// <inheritdoc />
        public EntityStringRepresentation ToPrettyString(EntityUid uid, MetaDataComponent? metadata)
            =>  ToPrettyString((uid, metadata));

        /// <inheritdoc />
        public EntityStringRepresentation ToPrettyString(Entity<MetaDataComponent?> entity)
        {
            if (entity.Comp == null && !MetaQuery.Resolve(entity.Owner, ref entity.Comp, false))
                return new EntityStringRepresentation(entity.Owner, default, true);

            return new EntityStringRepresentation(entity.Owner, entity.Comp, _actorQuery.CompOrNull(entity));
        }

        /// <inheritdoc />
        [return: NotNullIfNotNull("netEntity")]
        public EntityStringRepresentation? ToPrettyString(NetEntity? netEntity)
        {
            return netEntity == null ? null : ToPrettyString(netEntity.Value);
        }

        /// <inheritdoc />
        public EntityStringRepresentation ToPrettyString(NetEntity netEntity)
        {
            if (!TryGetEntityData(netEntity, out var uid, out var meta))
                return new EntityStringRepresentation(EntityUid.Invalid, netEntity, true);

            return ToPrettyString(uid.Value, meta);
        }

        #endregion Entity Management

        public virtual void RaisePredictiveEvent<T>(T msg) where T : EntityEventArgs
        {
            // Part of shared the EntityManager so that systems can have convenient proxy methods, but the
            // server should never be calling this.
            DebugTools.Assert("Why are you raising predictive events on the server?");
        }

        /// <summary>
        /// Raises an event locally on client or networked on server.
        /// </summary>
        public abstract void RaiseSharedEvent<T>(T message, EntityUid? user = null) where T : EntityEventArgs;

        /// <summary>
        /// Raises an event locally on client or networked on server.
        /// </summary>
        public abstract void RaiseSharedEvent<T>(T message, ICommonSession? user = null) where T : EntityEventArgs;

        /// <summary>
        ///     Factory for generating a new EntityUid for an entity currently being created.
        /// </summary>
        internal EntityUid GenerateEntityUid()
        {
            return new EntityUid(NextEntityUid++);
        }

        /// <summary>
        /// Generates a unique network id and increments <see cref="NextNetworkId"/>
        /// </summary>
        protected virtual NetEntity GenerateNetEntity() => new(NextNetworkId++);

        [Conditional("DEBUG")]
        protected void ThreadCheck()
        {
#if DEBUG
            DebugTools.Assert(
                Environment.CurrentManagedThreadId == _mainThreadId,
                "Environment.CurrentManagedThreadId == _mainThreadId");
#endif
        }
    }

    public enum EntityMessageType : byte
    {
        Error = 0,
        SystemMessage
    }
}
