using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using JetBrains.Annotations;
using Robust.Shared.Collections;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
using Robust.Shared.Utility;

namespace Robust.Shared.GameObjects;

public abstract partial class SharedUserInterfaceSystem : EntitySystem
{
    [Dependency] private IDynamicTypeFactory _factory = default!;
    [Dependency] private IGameTiming _timing = default!;
    [Dependency] private INetManager _netManager = default!;
    [Dependency] private IParallelManager _parallel = default!;
    [Dependency] protected IPrototypeManager ProtoManager = default!;
    [Dependency] private IReflectionManager _reflection = default!;
    [Dependency] protected ISharedPlayerManager Player = default!;
    [Dependency] private SharedTransformSystem _transforms = default!;

    private EntityQuery<IgnoreUIRangeComponent> _ignoreUIRangeQuery;
    private EntityQuery<TransformComponent> _xformQuery;
    protected EntityQuery<UserInterfaceComponent> UIQuery;
    protected EntityQuery<UserInterfaceUserComponent> UserQuery;

    private ActorRangeCheckJob _rangeJob;

    /// <summary>
    /// Defer BUIs during state handling so client doesn't spam a BUI constantly during prediction.
    /// </summary>
    private readonly List<(BoundUserInterface Bui, bool value)> _queuedBuis = new();

    public override void Initialize()
    {
        base.Initialize();

        EntityManager.ComponentFactory.RegisterNetworkedFields<UserInterfaceComponent>(
            nameof(UserInterfaceComponent.Actors),
            nameof(UserInterfaceComponent.Interfaces),
            nameof(UserInterfaceComponent.States));

        _ignoreUIRangeQuery = GetEntityQuery<IgnoreUIRangeComponent>();
        _xformQuery = GetEntityQuery<TransformComponent>();
        UIQuery = GetEntityQuery<UserInterfaceComponent>();
        UserQuery = GetEntityQuery<UserInterfaceUserComponent>();

        _rangeJob = new()
        {
            System = this,
            XformQuery = _xformQuery,
        };

        SubscribeAllEvent<BoundUIWrapMessage>((msg, args) =>
        {
            if (args.SenderSession.AttachedEntity is not { } player)
                return;

            OnMessageReceived(msg, player);
        });

        SubscribeLocalEvent<UserInterfaceComponent, OpenBoundInterfaceMessage>(OnUserInterfaceOpen);
        SubscribeLocalEvent<UserInterfaceComponent, CloseBoundInterfaceMessage>(OnUserInterfaceClosed);
        SubscribeLocalEvent<UserInterfaceComponent, ComponentStartup>(OnUserInterfaceStartup);
        SubscribeLocalEvent<UserInterfaceComponent, ComponentShutdown>(OnUserInterfaceShutdown);
        SubscribeLocalEvent<UserInterfaceComponent, ComponentGetState>(OnUserInterfaceGetState);
        SubscribeLocalEvent<UserInterfaceComponent, ComponentHandleState>(OnUserInterfaceHandleState);

        SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
        SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);

        SubscribeLocalEvent<UserInterfaceUserComponent, ComponentShutdown>(OnActorShutdown);
    }

    private void AddQueued(BoundUserInterface bui, bool value)
    {
        _queuedBuis.Add((bui, value));
    }

    /// <summary>
    /// Validates the received message, and then pass it onto systems/components
    /// </summary>
    private void OnMessageReceived(BoundUIWrapMessage msg, EntityUid sender)
    {
        // This is more or less the main BUI method that handles all messages.

        var uid = GetEntity(msg.Entity);

        if (!UIQuery.TryComp(uid, out var uiComp))
        {
            return;
        }

        if (!uiComp.Interfaces.TryGetValue(msg.UiKey, out var ui))
        {
            Log.Debug($"Got BoundInterfaceMessageWrapMessage for unknown UI key: {msg.UiKey}");
            return;
        }

        // If it's not an open message check we're even a subscriber.
        if (msg.Message is not OpenBoundInterfaceMessage &&
            (!uiComp.Actors.TryGetValue(msg.UiKey, out var actors) ||
             !actors.Contains(sender)))
        {
            Log.Debug($"UI {msg.UiKey} got BoundInterfaceMessageWrapMessage from a client who was not subscribed: {ToPrettyString(sender)}");
            return;
        }

        // verify that the user is allowed to press buttons on this UI:
        // If it's a close message something else might try to cancel it but we want to force it.
        if (msg.Message is not CloseBoundInterfaceMessage && ui.RequireInputValidation)
        {
            var attempt = new BoundUserInterfaceMessageAttempt(sender, uid, msg.UiKey, msg.Message);

            RaiseLocalEvent(attempt);
            if (attempt.Cancelled)
                return;

            RaiseLocalEvent(uid, attempt);
            if (attempt.Cancelled)
                return;
        }

        // get the wrapped message and populate it with the sender & UI key information.
        var message = msg.Message;
        message.Actor = sender;
        message.Entity = msg.Entity;
        message.UiKey = msg.UiKey;

        if (uiComp.ClientOpenInterfaces.TryGetValue(msg.UiKey, out var cBui))
        {
            cBui.ReceiveMessage(message);
        }

        // Raise as object so the correct type is used.
        RaiseLocalEvent(uid, (object)message, true);
    }

    #region User

    private void OnActorShutdown(Entity<UserInterfaceUserComponent> ent, ref ComponentShutdown args)
    {
        CloseUserUis((ent.Owner, ent.Comp));
    }

    #endregion

    private void OnPlayerAttached(PlayerAttachedEvent ev)
    {
        if (!UserQuery.TryGetComponent(ev.Entity, out var actor))
            return;

        // Open BUIs upon attachment
        foreach (var (uid, keys) in actor.OpenInterfaces)
        {
            if (!UIQuery.TryGetComponent(uid, out var uiComp))
                continue;

            // Player can now receive information about open UIs
            Dirty(uid, uiComp);

            foreach (var key in keys)
            {
                if (!uiComp.Interfaces.TryGetValue(key, out var data))
                    continue;

                EnsureClientBui((uid, uiComp), key, data);
            }
        }
    }

    private void OnPlayerDetached(PlayerDetachedEvent ev)
    {
        if (!UserQuery.TryGetComponent(ev.Entity, out var actor))
            return;

        // Close BUIs open detachment.
        foreach (var (uid, keys) in actor.OpenInterfaces)
        {
            if (!UIQuery.TryGetComponent(uid, out var uiComp))
                continue;

            // Player can no longer receive information about open UIs
            Dirty(uid, uiComp);

            foreach (var key in keys)
            {
                if (!uiComp.ClientOpenInterfaces.Remove(key, out var cBui))
                    continue;

                cBui.Dispose();
            }
        }
    }

    private void OnUserInterfaceClosed(Entity<UserInterfaceComponent> ent, ref CloseBoundInterfaceMessage args)
    {
        CloseUiInternal(ent!, args.UiKey, args.Actor);
    }

    private void CloseUiInternal(Entity<UserInterfaceComponent?> ent, Enum key, EntityUid actor)
    {
        if (!UIQuery.Resolve(ent.Owner, ref ent.Comp, false))
            return;

        if (!ent.Comp.Actors.TryGetValue(key, out var actors))
            return;

        actors.Remove(actor);
        if (actors.Count == 0)
            ent.Comp.Actors.Remove(key);

        DirtyField(ent, nameof(UserInterfaceComponent.Actors));

        // If the actor is also deleting then don't worry about updating what they have open.
        if (!TerminatingOrDeleted(actor)
            && UserQuery.TryComp(actor, out var actorComp)
            && actorComp.OpenInterfaces.TryGetValue(ent.Owner, out var keys))
        {
            keys.Remove(key);
            if (keys.Count == 0)
            {
                actorComp.OpenInterfaces.Remove(ent.Owner);
                if (actorComp.OpenInterfaces.Count == 0)
                    RemCompDeferred<UserInterfaceUserComponent>(actor);
            }
        }

        if (ent.Comp.ClientOpenInterfaces.TryGetValue(key, out var cBui))
        {
            AddQueued(cBui, false);
        }

        if (ent.Comp.Actors.Count == 0)
            RemCompDeferred<ActiveUserInterfaceComponent>(ent.Owner);

        var ev = new BoundUIClosedEvent(key, ent.Owner, actor);
        RaiseLocalEvent(ent.Owner, ev);
    }

    private void OnUserInterfaceOpen(Entity<UserInterfaceComponent> ent, ref OpenBoundInterfaceMessage args)
    {
        OpenUiInternal(ent!, args.UiKey, args.Actor);
    }

    private void OpenUiInternal(Entity<UserInterfaceComponent?> ent, Enum key, EntityUid actor)
    {
        if (!UIQuery.Resolve(ent.Owner, ref ent.Comp, false))
            return;

        // Similar to the close method this handles actually opening a UI, it just gets relayed here
        EnsureComp<ActiveUserInterfaceComponent>(ent.Owner);

        var actorComp = EnsureComp<UserInterfaceUserComponent>(actor);

        // Let state handling open the UI clientside.
        actorComp.OpenInterfaces.GetOrNew(ent.Owner).Add(key);
        ent.Comp.Actors.GetOrNew(key).Add(actor);
        DirtyField(ent, nameof(UserInterfaceComponent.Actors));

        var ev = new BoundUIOpenedEvent(key, ent.Owner, actor);
        RaiseLocalEvent(ent.Owner, ev);

        // If we're client we want this handled immediately.
        EnsureClientBui(ent!, key, ent.Comp.Interfaces[key]);
    }

    private void OnUserInterfaceStartup(Entity<UserInterfaceComponent> ent, ref ComponentStartup args)
    {
        // PlayerAttachedEvent will catch some of these.
        foreach (var (key, bui) in ent.Comp.ClientOpenInterfaces)
        {
            AddQueued(bui, true);
        }
    }

    protected void OnUserInterfaceShutdown(Entity<UserInterfaceComponent> ent, ref ComponentShutdown args)
    {
        var ents = new ValueList<EntityUid>();
        foreach (var (key, acts) in ent.Comp.Actors)
        {
            ents.Clear();
            ents.AddRange(acts);
            foreach (var actor in ents)
            {
                CloseUiInternal(ent!, key, actor);
                DebugTools.Assert(!acts.Contains(actor));
            }

            DebugTools.Assert(!ent.Comp.Actors.ContainsKey(key));
        }

        DebugTools.Assert(ent.Comp.ClientOpenInterfaces.Values.All(x => _queuedBuis.Contains((x, false))));
    }

    private void OnUserInterfaceGetState(Entity<UserInterfaceComponent> ent, ref ComponentGetState args)
    {
        if (args.FromTick > ent.Comp.CreationTick && ent.Comp.LastFieldUpdate >= args.FromTick)
        {
            var fields = EntityManager.GetModifiedFields(ent.Comp, args.FromTick);

            switch (fields)
            {
                case 1 << 0:
                {
                    var state = new UserInterfaceActorsDeltaState();
                    AddActors(ent, state.Actors, ref args);

                    args.State = state;
                    return;
                }
                case 1 << 2:
                {
                    var states = ent.Comp.States;

                    // TODO Game State
                    // Force the client to serialize & de-serialize implicitly generated component states.
                    if (_netManager.IsClient)
                        states = new(states);

                    args.State = new UserInterfaceStatesDeltaState {States = states};
                    return;
                }
            }
        }

        var actors = new Dictionary<Enum, List<NetEntity>>();
        var dataCopy = new Dictionary<Enum, InterfaceData>(ent.Comp.Interfaces.Count);

        // TODO Game State
        // Force the client to serialize & de-serialize implicitly generated component states.
        foreach (var (weh, a) in ent.Comp.Interfaces)
        {
            dataCopy[weh] = new InterfaceData(a);
        }

        args.State = new UserInterfaceComponentState(actors, new(ent.Comp.States), dataCopy);

        // Ensure that only the player that currently has the UI open gets to know what they have it open.
        AddActors(ent, actors, ref args);
    }

    private void AddActors(Entity<UserInterfaceComponent> ent, Dictionary<Enum, List<NetEntity>> actors, ref ComponentGetState args)
    {
        // Ensure that only the player that currently has the UI open gets to know what they have it open.
        if (args.ReplayState)
        {
            foreach (var (key, acts) in ent.Comp.Actors)
            {
                actors[key] = GetNetEntityList(acts);
            }
        }
        else if (args.Player.AttachedEntity is { } player)
        {
            var netPlayer = new List<NetEntity> { GetNetEntity(player) };
            foreach (var (key, acts) in ent.Comp.Actors)
            {
                if (acts.Contains(player))
                    actors[key] = netPlayer;
            }
        }
    }

    private void OnUserInterfaceHandleState(Entity<UserInterfaceComponent> ent, ref ComponentHandleState args)
    {
        Dictionary<Enum, List<NetEntity>>? stateActors = null;
        Dictionary<Enum, InterfaceData>? stateData = null;
        Dictionary<Enum, BoundUserInterfaceState>? stateStates = null;

        if (args.Current is UserInterfaceComponentState state)
        {
            stateActors = state.Actors;
            stateData = state.Data;
            stateStates = state.States;
        }
        else if (args.Current is UserInterfaceActorsDeltaState actorDelta)
        {
            stateActors = actorDelta.Actors;
        }
        else if (args.Current is UserInterfaceStatesDeltaState stateDelta)
        {
            stateStates = stateDelta.States;
        }
        else
        {
            return;
        }

        // Interfaces
        if (stateData != null)
        {
            ent.Comp.Interfaces.Clear();

            foreach (var data in stateData)
            {
                ent.Comp.Interfaces[data.Key] = new(data.Value);
            }
        }

        var attachedEnt = Player.LocalEntity;

        // Actors
        if (stateActors != null)
        {
            foreach (var key in ent.Comp.Actors.Keys)
            {
                if (!stateActors.ContainsKey(key))
                    CloseUi(ent!, key);
            }

            var toRemoveActors = new ValueList<EntityUid>();
            var newSet = new HashSet<EntityUid>();
            foreach (var (key, acts) in stateActors)
            {
                var actors = ent.Comp.Actors.GetOrNew(key);

                newSet.Clear();
                foreach (var netEntity in acts)
                {
                    var uid = EnsureEntity<UserInterfaceComponent>(netEntity, ent.Owner);
                    if (uid.IsValid())
                        newSet.Add(uid);
                }

                foreach (var actor in newSet)
                {
                    if (!actors.Contains(actor))
                        OpenUiInternal(ent!, key,  actor);
                }

                foreach (var actor in actors)
                {
                    if (!newSet.Contains(actor))
                        toRemoveActors.Add(actor);
                }

                foreach (var actor in toRemoveActors)
                {
                    CloseUiInternal(ent!, key, actor);
                }
            }

            var clientBuis = new ValueList<Enum>(ent.Comp.ClientOpenInterfaces.Keys);

            // Check if the UI is open by us, otherwise dispose of it.
            foreach (var key in clientBuis)
            {
                if (ent.Comp.Actors.TryGetValue(key, out var actors) &&
                    (attachedEnt == null || actors.Contains(attachedEnt.Value)))
                {
                    continue;
                }

                var bui = ent.Comp.ClientOpenInterfaces[key];
                AddQueued(bui, false);
            }
        }

        // States
        if (stateStates != null)
        {
            foreach (var key in ent.Comp.States.Keys)
            {
                if (!stateStates.ContainsKey(key))
                    ent.Comp.States.Remove(key);
            }

            // update any states we have open
            foreach (var (key, buiState) in stateStates)
            {
                if (ent.Comp.States.TryGetValue(key, out var existing) &&
                    existing.Equals(buiState))
                {
                    continue;
                }

                ent.Comp.States[key] = buiState;

                if (!ent.Comp.ClientOpenInterfaces.TryGetValue(key, out var cBui) || !cBui.IsOpened)
                    continue;

                cBui.State = buiState;
                cBui.UpdateState(buiState);
                cBui.Update();
            }
        }

        // If UI not open then open it
        // If we get the first state for an ent coming in then don't open BUIs yet, just defer it until later.
        var open = ent.Comp.LifeStage > ComponentLifeStage.Added;

        if (attachedEnt != null && stateActors != null)
        {
            foreach (var (key, value) in ent.Comp.Interfaces)
            {
                EnsureClientBui(ent, key, value, open);
            }
        }
    }

    /// <summary>
    /// Opens a client's BUI if not already open and applies the state to it.
    /// </summary>
    private void EnsureClientBui(Entity<UserInterfaceComponent> entity, Enum key, InterfaceData data, bool open = true)
    {
        // If it's out BUI open it up and apply the state, otherwise do nothing.
        var player = Player.LocalEntity;

        // Existing BUI just keep it.
        if (entity.Comp.ClientOpenInterfaces.TryGetValue(key, out var existing))
        {
            _queuedBuis.Remove((existing, false));
            return;
        }

        if (player == null ||
            !entity.Comp.Actors.TryGetValue(key, out var actors) ||
            !actors.Contains(player.Value))
        {
            return;
        }

        DebugTools.Assert(_netManager.IsClient);

        // Try-catch to try prevent error loops / bricked clients that constantly throw exceptions while applying game
        // states. E.g., stripping UI used to throw NREs in some instances while fetching the identity of unknown
        // entities.
        var type = _reflection.LooseGetType(data.ClientType);
        // No dependency injection because the BUI constructor will handle it.
        var boundUserInterface = (BoundUserInterface) _factory.CreateInstance(type, [entity.Owner, key], inject: false);
        entity.Comp.ClientOpenInterfaces[key] = boundUserInterface;

        // This is just so we don't open while applying UI states.
        if (!open)
            return;

        AddQueued(boundUserInterface, true);
    }

    /// <summary>
    /// Yields all the entities + keys currently open by this entity.
    /// </summary>
    public IEnumerable<(EntityUid Entity, Enum Key)> GetActorUis(Entity<UserInterfaceUserComponent?> entity)
    {
        if (!UserQuery.Resolve(entity.Owner, ref entity.Comp, false))
            yield break;

        foreach (var berry in entity.Comp.OpenInterfaces)
        {
            foreach (var key in berry.Value)
            {
                yield return (berry.Key, key);
            }
        }
    }

    /// <summary>
    /// Gets the actors that have the specified key attached to this entity open.
    /// </summary>
    public IEnumerable<EntityUid> GetActors(Entity<UserInterfaceComponent?> entity, Enum key)
    {
        if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false) || !entity.Comp.Actors.TryGetValue(key, out var actors))
            yield break;

        foreach (var actorUid in actors)
        {
            yield return actorUid;
        }
    }

    /// <summary>
    /// Closes the attached UI for all entities.
    /// </summary>
    public void CloseUi(Entity<UserInterfaceComponent?> entity, Enum key)
    {
        if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false))
            return;

        if (!entity.Comp.Actors.TryGetValue(key, out var actorSet))
            return;

        var actors = actorSet.ToArray();
        foreach (var actor in actors)
        {
            CloseUiInternal(entity, key, actor);
        }
    }

    /// <summary>
    /// Closes the attached UI only for the specified actor.
    /// </summary>
    public void CloseUi(Entity<UserInterfaceComponent?> entity, Enum key, ICommonSession? actor, bool predicted = false)
    {
        var actorEnt = actor?.AttachedEntity;

        if (actorEnt == null)
            return;

        CloseUi(entity, key, actorEnt.Value, predicted);
    }

    /// <summary>
    /// Closes the attached Ui only for the specified actor.
    /// </summary>
    public void CloseUi(Entity<UserInterfaceComponent?> entity, Enum key, EntityUid? actor, bool predicted = false)
    {
        if (actor == null)
            return;

        if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false))
            return;

        // Short-circuit if no UI.
        if (!entity.Comp.Interfaces.ContainsKey(key))
            return;

        if (!entity.Comp.Actors.TryGetValue(key, out var actors) || !actors.Contains(actor.Value))
            return;

        // Rely upon the client telling us.
        if (!predicted)
        {
            CloseUiInternal(entity, key, actor.Value);
            return;
        }

        if (!_timing.IsFirstTimePredicted)
            return;

        RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new CloseBoundInterfaceMessage(), key));
    }

    /// <summary>
    /// Tries to call OpenUi and return false if it isn't open.
    /// </summary>
    public bool TryOpenUi(Entity<UserInterfaceComponent?> entity, Enum key, EntityUid actor, bool predicted = false)
    {
        if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false))
            return false;

        OpenUi(entity, key, actor, predicted);

        // Due to the event actually handling the UI open / closed we can't
        if (!entity.Comp.Actors.TryGetValue(key, out var actors) ||
            !actors.Contains(actor))
        {
            return false;
        }

        return true;
    }

    /// <summary>
    /// Opens the UI for the local client. Does nothing on server.
    /// </summary>
    public virtual void OpenUi(Entity<UserInterfaceComponent?> entity, Enum key, bool predicted = false)
    {

    }

    public void OpenUi(Entity<UserInterfaceComponent?> entity, Enum key, EntityUid? actor, bool predicted = false)
    {
        if (actor == null || !UIQuery.Resolve(entity.Owner, ref entity.Comp, false))
            return;

        // No implementation for that UI key on this ent so short-circuit.
        if (!entity.Comp.Interfaces.ContainsKey(key))
            return;

        if (entity.Comp.Actors.TryGetValue(key, out var actors) && actors.Contains(actor.Value))
            return;

        if (predicted)
        {
            if (_timing.IsFirstTimePredicted)
            {
                // Not guaranteed to open so rely upon the event handling it.
                // Also lets client request it to be opened remotely too.
                RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new OpenBoundInterfaceMessage(), key));
            }
        }
        else
        {
            OnMessageReceived(new BoundUIWrapMessage(GetNetEntity(entity.Owner), new OpenBoundInterfaceMessage(), key), actor.Value);
        }
    }

    public void OpenUi(Entity<UserInterfaceComponent?> entity, Enum key, ICommonSession actor, bool predicted = false)
    {
        var actorEnt = actor.AttachedEntity;

        if (actorEnt == null)
            return;

        OpenUi(entity, key, actorEnt.Value, predicted);
    }

    /// <summary>
    /// Tries to return the saved position of a user interface.
    /// </summary>
    public virtual bool TryGetPosition(Entity<UserInterfaceComponent?> entity, Enum key, out Vector2 position)
    {
        position = Vector2.Zero;
        return false;
    }

    /// <summary>
    /// Saves a position for the BUI.
    /// </summary>
    protected virtual void SavePosition(BoundUserInterface bui)
    {

    }

    /// <summary>
    /// Sets a BUI state and networks it to all clients.
    /// </summary>
    public void SetUiState(Entity<UserInterfaceComponent?> entity, Enum key, BoundUserInterfaceState? state)
    {
        if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false))
            return;

        if (!entity.Comp.Interfaces.ContainsKey(key))
            return;

        // Null state
        if (state == null)
        {
            if (!entity.Comp.States.Remove(key))
                return;

            DirtyField(entity, nameof(UserInterfaceComponent.States));
        }
        // Non-null state, check if it matches existing.
        else
        {
            ref var stateRef = ref CollectionsMarshal.GetValueRefOrAddDefault(entity.Comp.States, key, out var exists);

            if (exists && stateRef?.Equals(state) == true)
                return;

            stateRef = state;
        }

        // Predict the change on client
        if (state != null && _netManager.IsClient && entity.Comp.ClientOpenInterfaces.TryGetValue(key, out var bui))
        {
            if (bui.State?.Equals(state) != true)
            {
                bui.UpdateState(state);
                bui.Update();
            }
        }

        DirtyField(entity, nameof(UserInterfaceComponent.States));
    }

    /// <summary>
    /// Returns true if this entity has the specified Ui key available, even if not currently open.
    /// </summary>
    public bool HasUi(EntityUid uid, Enum uiKey, UserInterfaceComponent? ui = null)
    {
        if (!Resolve(uid, ref ui, false))
            return false;

        return ui.Interfaces.ContainsKey(uiKey);
    }

    /// <summary>
    /// Returns true if the specified UI key is open for this entity by anyone.
    /// </summary>
    public bool IsUiOpen(Entity<UserInterfaceComponent?> entity, Enum uiKey)
    {
        if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false))
            return false;

        if (!entity.Comp.Actors.TryGetValue(uiKey, out var actors))
            return false;

        DebugTools.Assert(actors.Count > 0);
        return actors.Count > 0;
    }

    public bool IsUiOpen(Entity<UserInterfaceComponent?> entity, Enum uiKey, EntityUid actor)
    {
        if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false))
            return false;

        if (!entity.Comp.Actors.TryGetValue(uiKey, out var actors))
            return false;

        return actors.Contains(actor);
    }

    /// <summary>
    /// Returns true if any of the specified UI keys are open for this entity by anyone.
    /// </summary>
    /// <param name="entity">The entity to check.</param>
    /// <param name="uiKeys">The UI keys to check.</param>
    /// <returns>True if any UI is open, false otherwise.</returns>
    [PublicAPI]
    public bool IsUiOpen(Entity<UserInterfaceComponent?> entity, IEnumerable<Enum> uiKeys)
    {
        if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false))
            return false;

        foreach (var key in uiKeys)
        {
            if (entity.Comp.Actors.ContainsKey(key))
                return true;
        }

        return false;
    }

    /// <summary>
    /// Returns true if any UI is open for this entity by anyone.
    /// </summary>
    /// <param name="entity">The entity to check.</param>
    /// <returns>True if any UI is open, false otherwise.</returns>
    [PublicAPI]
    public bool IsAnyUiOpen(Entity<UserInterfaceComponent?> entity)
    {
        if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false))
            return false;

        return entity.Comp.Actors.Count > 0;
    }

    /// <summary>
    /// Raises a BUI message locally (on client or server) without networking it.
    /// </summary>
    [PublicAPI]
    public void RaiseUiMessage(Entity<UserInterfaceComponent?> entity, Enum key, BoundUserInterfaceMessage message)
    {
        if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false))
            return;

        if (!entity.Comp.Actors.TryGetValue(key, out var actors))
            return;

        OnMessageReceived(new BoundUIWrapMessage(GetNetEntity(entity.Owner), message, key), message.Actor);
    }

    #region Server messages

    /// <summary>
    /// Sends a BUI message to any actors who have the specified Ui key open.
    /// </summary>
    public void ServerSendUiMessage(Entity<UserInterfaceComponent?> entity, Enum key, BoundUserInterfaceMessage message)
    {
        if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false))
            return;

        if (!entity.Comp.Actors.TryGetValue(key, out var actors))
            return;

        var filter = Filter.Entities(actors.ToArray());
        RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), message, key), filter);
    }

    /// <summary>
    /// Sends a Bui message to the specified actor only.
    /// </summary>
    public void ServerSendUiMessage(Entity<UserInterfaceComponent?> entity, Enum key, BoundUserInterfaceMessage message, EntityUid actor)
    {
        if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false))
            return;

        if (!entity.Comp.Actors.TryGetValue(key, out var actors) || !actors.Contains(actor))
            return;

        RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), message, key), actor);
    }

    /// <summary>
    /// Sends a Bui message to the specified actor only.
    /// </summary>
    public void ServerSendUiMessage(Entity<UserInterfaceComponent?> entity, Enum key, BoundUserInterfaceMessage message, ICommonSession actor)
    {
        if (!_netManager.IsClient)
            return;

        if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false) || actor.AttachedEntity is not { } attachedEntity)
            return;

        if (!entity.Comp.Actors.TryGetValue(key, out var actors) || !actors.Contains(attachedEntity))
            return;

        RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), message, key), actor);
    }

    #endregion

    /// <summary>
    /// Raises a BUI message from the client to the server.
    /// </summary>
    public void ClientSendUiMessage(Entity<UserInterfaceComponent?> entity, Enum key, BoundUserInterfaceMessage message)
    {
        var player = Player.LocalEntity;

        // Don't send it if we're not a valid actor for it just in case.
        if (player == null ||
            !UIQuery.Resolve(entity.Owner, ref entity.Comp, false) ||
            !entity.Comp.Actors.TryGetValue(key, out var actors) ||
            !actors.Contains(player.Value))
        {
            return;
        }

        RaiseNetworkEvent(new BoundUIWrapMessage(GetNetEntity(entity.Owner), message, key));
    }

    /// <summary>
    /// Closes the user's UIs that match the specified key.
    /// </summary>
    public void CloseUserUis<T>(Entity<UserInterfaceUserComponent?> actor) where T: Enum
    {
        if (!UserQuery.Resolve(actor.Owner, ref actor.Comp, false))
            return;

        if (actor.Comp.OpenInterfaces.Count == 0)
            return;

        var keys = new ValueList<Enum>();
        foreach (var (uid, enums) in actor.Comp.OpenInterfaces)
        {
            keys.Clear();
            keys.AddRange(enums);

            foreach (var weh in keys)
            {
                if (weh is not T)
                    continue;

                CloseUiInternal(uid, weh, actor.Owner);
            }
        }
    }

    /// <summary>
    /// Closes all Uis for the actor.
    /// </summary>
    public void CloseUserUis(Entity<UserInterfaceUserComponent?> actor)
    {
        if (!UserQuery.Resolve(actor.Owner, ref actor.Comp, false))
            return;

        if (actor.Comp.OpenInterfaces.Count == 0)
            return;

        var keys = new ValueList<Enum>();

        foreach (var (uid, enums) in actor.Comp.OpenInterfaces)
        {
            keys.Clear();
            keys.AddRange(enums);

            foreach (var key in keys)
            {
                CloseUiInternal(uid, key, actor.Owner);
            }
        }
    }

    /// <summary>
    /// Closes all Uis for the entity.
    /// </summary>
    public void CloseUis(Entity<UserInterfaceComponent?> entity)
    {
        if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false))
            return;

        var toClose = new ValueList<EntityUid>();
        foreach (var (key, actors) in entity.Comp.Actors)
        {
            toClose.Clear();
            toClose.AddRange(actors);
            foreach (var actor in toClose)
            {
                CloseUiInternal(entity, key, actor);
            }
        }
    }

    /// <summary>
    /// Closes all Uis for the entity that the specified actor has open.
    /// </summary>
    public void CloseUis(Entity<UserInterfaceComponent?> entity, EntityUid actor)
    {
        if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false))
            return;

        foreach (var key in entity.Comp.Interfaces.Keys)
        {
            CloseUiInternal(entity, key, actor);
        }
    }

    /// <summary>
    /// Closes all Uis for the entity that the specified actor has open.
    /// </summary>
    public void CloseUis(Entity<UserInterfaceComponent?> entity, ICommonSession actor)
    {
        if (actor.AttachedEntity is not { } attachedEnt || !UIQuery.Resolve(entity.Owner, ref entity.Comp, false))
            return;

        CloseUis(entity, attachedEnt);
    }

    /// <summary>
    /// Tries to get the BUI if it is currently open.
    /// </summary>
    public bool TryGetOpenUi(Entity<UserInterfaceComponent?> entity, Enum uiKey, [NotNullWhen(true)] out BoundUserInterface? bui)
    {
        bui = null;

        return UIQuery.Resolve(entity.Owner, ref entity.Comp, false) && entity.Comp.ClientOpenInterfaces.TryGetValue(uiKey, out bui);
    }

    /// <summary>
    /// Tries to get the BUI if it is currently open.
    /// </summary>
    public bool TryGetOpenUi<T>(Entity<UserInterfaceComponent?> entity, Enum uiKey, [NotNullWhen(true)] out T? bui) where T : BoundUserInterface
    {
        if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false) || !entity.Comp.ClientOpenInterfaces.TryGetValue(uiKey, out var cBui))
        {
            bui = null;
            return false;
        }

        bui = (T)cBui;
        return true;
    }

    public bool TryToggleUi(Entity<UserInterfaceComponent?> entity, Enum uiKey, ICommonSession actor)
    {
        if (actor.AttachedEntity is not { } attachedEntity)
            return false;

        return TryToggleUi(entity, uiKey, attachedEntity);
    }

    /// <summary>
    /// Switches between closed and open for a specific client.
    /// </summary>
    public bool TryToggleUi(Entity<UserInterfaceComponent?> entity, Enum uiKey, EntityUid actor)
    {
        if (!UIQuery.Resolve(entity.Owner, ref entity.Comp, false) ||
            !entity.Comp.Interfaces.ContainsKey(uiKey))
        {
            return false;
        }

        if (entity.Comp.Actors.TryGetValue(uiKey, out var actors) && actors.Contains(actor))
        {
            CloseUi(entity, uiKey, actor);
        }
        else
        {
            OpenUi(entity, uiKey, actor);
        }

        return true;
    }

    /// <summary>
    /// Raised by client-side UIs to send predicted messages to server.
    /// </summary>
    public void SendPredictedUiMessage(BoundUserInterface bui, BoundUserInterfaceMessage msg)
    {
        RaisePredictiveEvent(new BoundUIWrapMessage(GetNetEntity(bui.Owner), msg, bui.UiKey));
    }

    public bool TryGetInterfaceData(Entity<UserInterfaceComponent?> entity,
        Enum key,
        [NotNullWhen(true)] out InterfaceData? data)
    {
        data = null;
        return Resolve(entity, ref entity.Comp, false) && entity.Comp.Interfaces.TryGetValue(key, out data);
    }

    public float GetUiRange(Entity<UserInterfaceComponent?> entity, Enum key)
    {
        TryGetInterfaceData(entity, key, out var data);
        return data?.InteractionRange ?? 0;
    }

    /// <inheritdoc />
    public override void Update(float frameTime)
    {
        if (_timing.IsFirstTimePredicted)
        {
            foreach (var (bui, open) in _queuedBuis)
            {
                if (open)
                {
#if EXCEPTION_TOLERANCE
                    try
                    {
#endif
                    bui.Open();

                    if (UIQuery.TryComp(bui.Owner, out var uiComp))
                    {
                        if (uiComp.States.TryGetValue(bui.UiKey, out var buiState))
                        {
                            bui.State = buiState;
                            bui.UpdateState(buiState);
                            bui.Update();
                        }
                    }
#if EXCEPTION_TOLERANCE
                    }
                    catch (Exception e)
                    {
                        Log.Error(
                            $"Caught exception while attempting to create a BUI {bui.UiKey} with type {bui.GetType()} on entity {ToPrettyString(bui.Owner)}. Exception: {e}");
                    }
#endif
                }
                // Close BUI
                else
                {
                    if (UIQuery.TryComp(bui.Owner, out var uiComp))
                    {
                        uiComp.ClientOpenInterfaces.Remove(bui.UiKey);
                    }

#if EXCEPTION_TOLERANCE
                    try
                    {
#endif
                    if (!TerminatingOrDeleted(bui.Owner))
                    {
                        SavePosition(bui);
                    }

                    bui.Dispose();
#if EXCEPTION_TOLERANCE
                    }
                    catch (Exception e)
                    {
                        Log.Error(
                            $"Caught exception while attempting to dispose of a BUI {bui.UiKey} with type {bui.GetType()} on entity {ToPrettyString(bui.Owner)}. Exception: {e}");
                    }
#endif
                }
            }

            _queuedBuis.Clear();
        }

        var query = AllEntityQuery<ActiveUserInterfaceComponent, UserInterfaceComponent>();
        // Run these in parallel because it's expensive.
        _rangeJob.ActorRanges.Clear();

        // Handles closing the BUI if actors move out of range of them.
        // TODO iterate over BUI users, not BUI entities.
        // I.e., a user may have more than one BUI open, but its rare for a bui to be open by more than one user.
        // This means we won't have to fetch the user's transform as frequently.
        while (query.MoveNext(out var uid, out _, out var uiComp))
        {
            foreach (var (key, actors) in uiComp.Actors)
            {
                DebugTools.Assert(actors.Count > 0);
                var data = uiComp.Interfaces[key];

                // Short-circuit
                if (data.InteractionRange <= 0f)
                    continue;

                foreach (var actor in actors)
                {
                    if (_netManager.IsClient && !actor.IsValid())
                        continue; // Client might not have received the entity. Server should log errors.

                    _rangeJob.ActorRanges.Add((uid, key, data, actor, false));
                }
            }
        }

        _parallel.ProcessNow(_rangeJob, _rangeJob.ActorRanges.Count);

        foreach (var data in _rangeJob.ActorRanges)
        {
            var uid = data.Ui;
            var actor = data.Actor;
            var key = data.Key;

            if (data.Result || Deleted(uid) || Deleted(actor) || !UIQuery.TryComp(uid, out var uiComp))
                continue;

            CloseUi((uid, uiComp), key, actor);
        }
    }

    /// <summary>
    /// Set a UI after an entity has been created.
    /// </summary>
    public void SetUi(Entity<UserInterfaceComponent?> ent, Enum key, InterfaceData data)
    {
        if (!Resolve(ent, ref ent.Comp, false))
            ent.Comp = AddComp<UserInterfaceComponent>(ent);

        ent.Comp.Interfaces[key] = data;
        DirtyField(ent, nameof(UserInterfaceComponent.Interfaces));
    }

    public bool TryGetUiState<T>(Entity<UserInterfaceComponent?> ent, Enum key, [NotNullWhen(true)] out T? state) where T : BoundUserInterfaceState
    {
        if (!Resolve(ent, ref ent.Comp, false) || !ent.Comp.States.TryGetValue(key, out var stateComp))
        {
            state = null;
            return false;
        }

        state = (T)stateComp;
        return true;
    }

    /// <summary>
    ///     Verify that the subscribed clients are still in range of the interface.
    /// </summary>
    private bool CheckRange(
        Entity<TransformComponent> UiEnt,
        Enum key,
        InterfaceData data,
        Entity<TransformComponent> actor)
    {
        if (actor.Comp.MapID != UiEnt.Comp.MapID)
            return false;

        // Handle pluggable BoundUserInterfaceCheckRangeEvent
        var checkRangeEvent = new BoundUserInterfaceCheckRangeEvent(UiEnt, key, data, actor!);
        RaiseLocalEvent(UiEnt.Owner, ref checkRangeEvent, true);

        if (checkRangeEvent.Result == BoundUserInterfaceRangeResult.Pass)
            return true;

        // We only check if the range check should be ignored if it did not pass.
        // The majority of the time the check will be passing and users generally do not have this component.
        if (_ignoreUIRangeQuery.HasComponent(actor))
            return true;

        if (checkRangeEvent.Result == BoundUserInterfaceRangeResult.Fail)
            return false;

        DebugTools.Assert(checkRangeEvent.Result == BoundUserInterfaceRangeResult.Default);

        return _transforms.InRange(UiEnt!, (actor.Owner, actor.Comp), data.InteractionRange);
    }

    /// <summary>
    /// Used for running UI raycast checks in parallel.
    /// </summary>
    private record struct ActorRangeCheckJob() : IParallelRobustJob
    {
        public required EntityQuery<TransformComponent> XformQuery;
        public required SharedUserInterfaceSystem System;
        public readonly List<(EntityUid Ui, Enum Key, InterfaceData Data, EntityUid Actor, bool Result)> ActorRanges = new();

        public void Execute(int index)
        {
            var data = ActorRanges[index];

            if (!XformQuery.TryComp(data.Ui, out var uiXform) ||
                !XformQuery.TryComp(data.Actor, out var actorXform))
            {
                data.Result = false;
            }
            else
            {
                data.Result = System.CheckRange((data.Ui, uiXform), data.Key, data.Data, (data.Actor, actorXform));
            }

            ActorRanges[index] = data;
        }
    }
}

/// <summary>
/// Raised by <see cref="UserInterfaceSystem"/> to check whether an interface is still accessible by its user.
/// The event is raised directed at the entity that owns the interface.
/// </summary>
[ByRefEvent]
[PublicAPI]
public struct BoundUserInterfaceCheckRangeEvent(
    Entity<TransformComponent> target,
    Enum uiKey,
    InterfaceData data,
    Entity<TransformComponent> actor)
{
    /// <summary>
    /// The entity owning the UI being checked for.
    /// </summary>
    public readonly EntityUid Target = target;

    /// <summary>
    /// The UI itself.
    /// </summary>
    /// <returns></returns>
    public readonly Enum UiKey = uiKey;

    public readonly InterfaceData Data = data;

    /// <summary>
    /// The player for which the UI is being checked.
    /// </summary>
    public readonly Entity<TransformComponent> Actor = actor;

    /// <summary>
    /// The result of the range check.
    /// </summary>
    public BoundUserInterfaceRangeResult Result;
}

/// <summary>
/// Possible results for a <see cref="BoundUserInterfaceCheckRangeEvent"/>.
/// </summary>
public enum BoundUserInterfaceRangeResult : byte
{
    /// <summary>
    /// Run built-in range check.
    /// </summary>
    Default,

    /// <summary>
    /// Range check passed, UI is accessible.
    /// </summary>
    Pass,

    /// <summary>
    /// Range check failed, UI is inaccessible.
    /// </summary>
    Fail
}
