using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.NativeWrapper;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator;
using LightlessSync.UI.Services;
using Microsoft.Extensions.Logging;
using System.Numerics;
using static LightlessSync.UI.DtrEntry;
using LSeStringBuilder = Lumina.Text.SeStringBuilder;

namespace LightlessSync.Services;

/// <summary>
/// NameplateService is used for coloring our nameplates based on the settings of the user.
/// </summary>
public unsafe class NameplateService : DisposableMediatorSubscriberBase
{
    private readonly ILogger<NameplateService> _logger;
    private readonly LightlessConfigService _configService;
    private readonly IClientState _clientState;
    private readonly IGameGui _gameGui;
    private readonly IObjectTable _objectTable;
    private readonly PairUiService _pairUiService;
    private readonly NameplateUpdateHookService _nameplateUpdateHookService;

    public NameplateService(ILogger<NameplateService> logger,
        LightlessConfigService configService,
        IClientState clientState,
        IGameGui gameGui,
        IObjectTable objectTable,
        LightlessMediator lightlessMediator,
        PairUiService pairUiService,
        NameplateUpdateHookService nameplateUpdateHookService) : base(logger, lightlessMediator)
    {
        _logger = logger;
        _configService = configService;
        _clientState = clientState;
        _gameGui = gameGui;
        _objectTable = objectTable;
        _pairUiService = pairUiService;
        _nameplateUpdateHookService = nameplateUpdateHookService;

        _nameplateUpdateHookService.NameplateUpdated += OnNameplateUpdated;
        Refresh();

        Mediator.Subscribe<VisibilityChange>(this, (_) => Refresh());
    }

    /// <summary>
    /// Nameplate update handler, triggered by the signature hook service.
    /// </summary>
    private void OnNameplateUpdated(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex)
    {
        try
        {
            SetNameplate(namePlateInfo, battleChara);
        }
        catch (Exception e)
        {
            _logger.LogError(e, "Error in NameplateService OnNameplateUpdated");
        }
    }

    /// <summary>
    /// Determine if the player should be colored based on conditions (isFriend, IsInParty)
    /// </summary>
    /// <param name="playerCharacter">Player character that will be checked</param>
    /// <param name="visibleUserIds">All visible users in the current object table</param>
    /// <returns>PLayer should or shouldnt be colored based on the result. True means colored</returns>
    private bool ShouldColorPlayer(IPlayerCharacter playerCharacter, HashSet<ulong> visibleUserIds)
    {
        if (!visibleUserIds.Contains(playerCharacter.GameObjectId))
            return false;

        var isInParty = playerCharacter.StatusFlags.HasFlag(StatusFlags.PartyMember);
        var isFriend = playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend);

        bool partyColorAllowed = _configService.Current.overridePartyColor && isInParty;
        bool friendColorAllowed = _configService.Current.overrideFriendColor && isFriend;

        if ((isInParty && !partyColorAllowed) || (isFriend && !friendColorAllowed))
            return false;

        return true;
    }

    /// <summary>
    /// Setting up the nameplate of the user to be colored
    /// </summary>
    /// <param name="namePlateInfo">Information given from the Signature to be updated</param>
    /// <param name="battleChara">Character from FF</param>
    private void SetNameplate(RaptureAtkModule.NamePlateInfo* namePlateInfo, BattleChara* battleChara)
    {
        if (!_configService.Current.IsNameplateColorsEnabled || _clientState.IsPvPExcludingDen)
            return;
        if (namePlateInfo == null || battleChara == null)
            return;

        var obj = _objectTable.FirstOrDefault(o => o.Address == (nint)battleChara);
        if (obj is not IPlayerCharacter player)
            return;

        var snapshot = _pairUiService.GetSnapshot();
        var visibleUsersIds = snapshot.PairsByUid.Values
                    .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
                    .Select(u => (ulong)u.PlayerCharacterId)
                    .ToHashSet();

        //Check if player should be colored
        if (!ShouldColorPlayer(player, visibleUsersIds))
            return;

        var originalName = player.Name.ToString();

        //Check if not null of the name
        if (string.IsNullOrEmpty(originalName))
            return;

        //Check if any characters/symbols are forbidden
        if (HasForbiddenSeStringChars(originalName))
            return;

        //Swap color channels as we store them in BGR format as FF loves that
        var cfgColors = SwapColorChannels(_configService.Current.NameplateColors);
        var coloredName = WrapStringInColor(originalName, cfgColors.Glow, cfgColors.Foreground);

        //Replace string of nameplate with our colored one
        namePlateInfo->Name.SetString(coloredName.EncodeWithNullTerminator());
    }

    /// <summary>
    /// Converts Uint code to Vector4 as we store Colors in Uint in our config, needed for lumina
    /// </summary>
    /// <param name="rgb">Color code</param>
    /// <returns>Vector4 Color</returns>
    private static Vector4 RgbUintToVector4(uint rgb)
    {
        float r = ((rgb >> 16) & 0xFF) / 255f;
        float g = ((rgb >> 8) & 0xFF) / 255f;
        float b = (rgb & 0xFF) / 255f;
        return new Vector4(r, g, b, 1f);
    }

    /// <summary>
    /// Checks if the string has any forbidden characters/symbols as the string builder wouldnt append.
    /// </summary>
    /// <param name="s">String that has to be checked</param>
    /// <returns>Contains forbidden characters/symbols or not</returns>
    private static bool HasForbiddenSeStringChars(string s)
    {
        if (string.IsNullOrEmpty(s))
            return false;

        foreach (var ch in s)
        {
            if (ch == '\0' || ch == '\u0002')
                return true;
        }

        return false;
    }

    /// <summary>
    /// Wraps the given string with the given edge and text color.
    /// </summary>
    /// <param name="text">String that has to be wrapped</param>
    /// <param name="edgeColor">Edge(border) color</param>
    /// <param name="textColor">Text color</param>
    /// <returns>Color wrapped SeString</returns>
    public static SeString WrapStringInColor(string text, uint? edgeColor = null, uint? textColor = null)
    {
        if (string.IsNullOrEmpty(text))
            return SeString.Empty;

        var builder = new LSeStringBuilder();

        if (textColor is uint tc)
            builder.PushColorRgba(RgbUintToVector4(tc));

        if (edgeColor is uint ec)
            builder.PushEdgeColorRgba(RgbUintToVector4(ec));

        builder.Append(text);

        if (edgeColor != null)
            builder.PopEdgeColor();

        if (textColor != null)
            builder.PopColor();

        return builder.ToReadOnlySeString().ToDalamudString();
    }

    /// <summary>
    /// Request redraw of nameplates
    /// </summary>
    public void RequestRedraw()
    {
        Refresh();
    }

    /// <summary>
    /// Toggles the refresh of the Nameplate addon
    /// </summary>
    protected void Refresh()
    {
        AtkUnitBasePtr namePlateAddon = _gameGui.GetAddonByName("NamePlate");

        if (namePlateAddon.IsNull)
        {
            _logger.LogInformation("NamePlate addon is null, cannot refresh nameplates.");
            return;
        }

        var addonNamePlate = (AddonNamePlate*)namePlateAddon.Address;

        if (addonNamePlate == null)
        {
            _logger.LogInformation("addonNamePlate addon is null, cannot refresh nameplates.");
            return;
        }
            
        addonNamePlate->DoFullUpdate = 1;
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _nameplateUpdateHookService.NameplateUpdated -= OnNameplateUpdated;
        }

        base.Dispose(disposing);
    }
}