using System;
using System.Collections.Generic;
using Content.Client.Chat.Managers;
using Content.Client.Chat.UI;
using Content.Shared.Alert;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Log;

namespace Content.Client.Alerts.UI;

public sealed class AlertsFramePresenter : IDisposable
{
    [Dependency] private readonly IEntitySystemManager _systemManager = default!;
    [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
    [Dependency] private readonly IChatManager _chatManager = default!;

    private IAlertsFrameView _alertsFrame;
    private ClientAlertsSystem? _alertsSystem;

    public AlertsFramePresenter()
    {
        // This is a lot easier than a factory
        IoCManager.InjectDependencies(this);

        _alertsFrame = new AlertsUI(_chatManager);
        _userInterfaceManager.StateRoot.AddChild((AlertsUI) _alertsFrame);

        // This is required so that if we load after the system is initialized, we can bind to it immediately
        if (_systemManager.TryGetEntitySystem<ClientAlertsSystem>(out var alertsSystem))
            SystemBindingChanged(alertsSystem);

        _systemManager.SystemLoaded += OnSystemLoaded;
        _systemManager.SystemUnloaded += OnSystemUnloaded;

        _alertsFrame.AlertPressed += OnAlertPressed;

        // initially populate the frame if system is available
        var alerts = alertsSystem?.ActiveAlerts;
        if (alerts != null)
        {
            SystemOnSyncAlerts(alertsSystem, alerts);
        }
    }

    /// <inheritdoc />
    public void Dispose()
    {
        _userInterfaceManager.StateRoot.RemoveChild((AlertsUI) _alertsFrame);
        _alertsFrame.Dispose();
        _alertsFrame = null!;

        SystemBindingChanged(null);
        _systemManager.SystemLoaded -= OnSystemLoaded;
        _systemManager.SystemUnloaded -= OnSystemUnloaded;
    }

    private void OnAlertPressed(object? sender, AlertType e)
    {
        _alertsSystem?.AlertClicked(e);
    }

    private void SystemOnClearAlerts(object? sender, EventArgs e)
    {
        _alertsFrame.ClearAllControls();
    }

    private void SystemOnSyncAlerts(object? sender, IReadOnlyDictionary<AlertKey, AlertState> e)
    {
        if (sender is ClientAlertsSystem system)
            _alertsFrame.SyncControls(system, system.AlertOrder, e);
    }

    //TODO: This system binding boilerplate seems to be duplicated between every presenter
    // prob want to pull it out into a generic object with callbacks for Onbind/OnUnbind
    #region System Binding

    private void OnSystemLoaded(object? sender, SystemChangedArgs args)
    {
        if (args.System is ClientAlertsSystem system) SystemBindingChanged(system);
    }

    private void OnSystemUnloaded(object? sender, SystemChangedArgs args)
    {
        if (args.System is ClientAlertsSystem) SystemBindingChanged(null);
    }

    private void SystemBindingChanged(ClientAlertsSystem? newSystem)
    {
        if (newSystem is null)
        {
            if (_alertsSystem is null)
                return;

            UnbindFromSystem();
        }
        else
        {
            if (_alertsSystem is null)
            {
                BindToSystem(newSystem);
                return;
            }

            UnbindFromSystem();
            BindToSystem(newSystem);
        }
    }

    private void BindToSystem(ClientAlertsSystem system)
    {
        _alertsSystem = system;
        system.SyncAlerts += SystemOnSyncAlerts;
        system.ClearAlerts += SystemOnClearAlerts;
    }

    private void UnbindFromSystem()
    {
        var system = _alertsSystem;

        if (system is null)
            throw new InvalidOperationException();

        system.SyncAlerts -= SystemOnSyncAlerts;
        system.ClearAlerts -= SystemOnClearAlerts;
    }

    #endregion
}

/// <summary>
///     This is the frame of vertical set of alerts that show up on the HUD.
/// </summary>
public interface IAlertsFrameView : IDisposable
{
    event EventHandler<AlertType>? AlertPressed;

    void SyncControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype,
        IReadOnlyDictionary<AlertKey, AlertState> alertStates);
    void ClearAllControls();
}

/// <summary>
///     The status effects display on the right side of the screen.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class AlertsUI : Control, IAlertsFrameView
{
    // also known as Control.Children?
    private readonly Dictionary<AlertKey, AlertControl> _alertControls = new();

    public AlertsUI(IChatManager chatManager)
    {
        _chatManager = chatManager;
        RobustXamlLoader.Load(this);

        LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.Begin);
        LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.End);
        LayoutContainer.SetAnchorTop(this, 0f);
        LayoutContainer.SetAnchorRight(this, 1f);
        LayoutContainer.SetAnchorBottom(this, 1f);
        LayoutContainer.SetMarginBottom(this, -180);
        LayoutContainer.SetMarginTop(this, 250);
        LayoutContainer.SetMarginRight(this, -10);
    }

    public void SyncControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype,
        IReadOnlyDictionary<AlertKey, AlertState> alertStates)
    {
        // remove any controls with keys no longer present
        if (SyncRemoveControls(alertStates)) return;

        // now we know that alertControls contains alerts that should still exist but
        // may need to updated,
        // also there may be some new alerts we need to show.
        // further, we need to ensure they are ordered w.r.t their configured order
        SyncUpdateControls(alertsSystem, alertOrderPrototype, alertStates);
    }

    public void ClearAllControls()
    {
        foreach (var alertControl in _alertControls.Values)
        {
            alertControl.OnPressed -= AlertControlPressed;
            alertControl.Dispose();
        }

        _alertControls.Clear();
    }

    public event EventHandler<AlertType>? AlertPressed;

    //TODO: This control caring about it's layout relative to other controls in the tree is terrible
    // the presenters or gamescreen should be dealing with this
    // probably want to tackle this after chatbox gets MVP'd
    #region Spaghetti

    public const float ChatSeparation = 38f;
    private readonly IChatManager _chatManager;

    protected override void EnteredTree()
    {
        base.EnteredTree();
        _chatManager.OnChatBoxResized += OnChatResized;
        OnChatResized(new ChatResizedEventArgs(HudChatBox.InitialChatBottom));
    }

    protected override void ExitedTree()
    {
        base.ExitedTree();
        _chatManager.OnChatBoxResized -= OnChatResized;
    }

    private void OnChatResized(ChatResizedEventArgs chatResizedEventArgs)
    {
        // resize us to fit just below the chat box
        if (_chatManager.CurrentChatBox != null)
            LayoutContainer.SetMarginTop(this, chatResizedEventArgs.NewBottom + ChatSeparation);
        else
            LayoutContainer.SetMarginTop(this, 250);
    }

    #endregion

    // This makes no sense but I'm leaving it in place in case I break anything by removing it.
    protected override void Resized()
    {
        // TODO: Can rework this once https://github.com/space-wizards/RobustToolbox/issues/1392 is done,
        // this is here because there isn't currently a good way to allow the grid to adjust its height based
        // on constraints, otherwise we would use anchors to lay it out
        base.Resized();
        AlertContainer.MaxGridHeight = Height;
    }

    protected override void UIScaleChanged()
    {
        AlertContainer.MaxGridHeight = Height;
        base.UIScaleChanged();
    }

    private bool SyncRemoveControls(IReadOnlyDictionary<AlertKey, AlertState> alertStates)
    {
        var toRemove = new List<AlertKey>();
        foreach (var existingKey in _alertControls.Keys)
        {
            if (!alertStates.ContainsKey(existingKey)) toRemove.Add(existingKey);
        }

        foreach (var alertKeyToRemove in toRemove)
        {
            _alertControls.Remove(alertKeyToRemove, out var control);
            if (control == null) return true;
            AlertContainer.Children.Remove(control);
        }

        return false;
    }

    private void SyncUpdateControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype,
        IReadOnlyDictionary<AlertKey, AlertState> alertStates)
    {
        foreach (var (alertKey, alertState) in alertStates)
        {
            if (!alertKey.AlertType.HasValue)
            {
                Logger.WarningS("alert", "found alertkey without alerttype," +
                                         " alert keys should never be stored without an alerttype set: {0}", alertKey);
                continue;
            }

            var alertType = alertKey.AlertType.Value;
            if (!alertsSystem.TryGet(alertType, out var newAlert))
            {
                Logger.ErrorS("alert", "Unrecognized alertType {0}", alertType);
                continue;
            }

            if (_alertControls.TryGetValue(newAlert.AlertKey, out var existingAlertControl) &&
                existingAlertControl.Alert.AlertType == newAlert.AlertType)
            {
                // key is the same, simply update the existing control severity / cooldown
                existingAlertControl.SetSeverity(alertState.Severity);
                existingAlertControl.Cooldown = alertState.Cooldown;
            }
            else
            {
                if (existingAlertControl != null) AlertContainer.Children.Remove(existingAlertControl);

                // this is a new alert + alert key or just a different alert with the same
                // key, create the control and add it in the appropriate order
                var newAlertControl = CreateAlertControl(newAlert, alertState);

                //TODO: Can the presenter sort the states before giving it to us?
                if (alertOrderPrototype != null)
                {
                    var added = false;
                    foreach (var alertControl in AlertContainer.Children)
                    {
                        if (alertOrderPrototype.Compare(newAlert, ((AlertControl) alertControl).Alert) >= 0)
                            continue;

                        var idx = alertControl.GetPositionInParent();
                        AlertContainer.Children.Add(newAlertControl);
                        newAlertControl.SetPositionInParent(idx);
                        added = true;
                        break;
                    }

                    if (!added) AlertContainer.Children.Add(newAlertControl);
                }
                else
                    AlertContainer.Children.Add(newAlertControl);

                _alertControls[newAlert.AlertKey] = newAlertControl;
            }
        }
    }

    private AlertControl CreateAlertControl(AlertPrototype alert, AlertState alertState)
    {
        var alertControl = new AlertControl(alert, alertState.Severity)
        {
            Cooldown = alertState.Cooldown
        };
        alertControl.OnPressed += AlertControlPressed;
        return alertControl;
    }

    private void AlertControlPressed(BaseButton.ButtonEventArgs args)
    {
        if (args.Button is not AlertControl control)
            return;

        if (args.Event.Function != EngineKeyFunctions.UIClick)
            return;

        AlertPressed?.Invoke(this, control.Alert.AlertType);
    }
}
