using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Utility;
using HSUI.Config;
using HSUI.Enums;
using HSUI.Helpers;
using HSUI.Interface.GeneralElements;
using Dalamud.Bindings.ImGui;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Client.Game;
using LuminaStatus = Lumina.Excel.Sheets.Status;
using StatusStruct = FFXIVClientStructs.FFXIV.Client.Game.Status;

namespace HSUI.Interface.StatusEffects
{
    public class StatusEffectsListHud : ParentAnchoredDraggableHudElement, IHudElementWithActor, IHudElementWithAnchorableParent, IHudElementWithPreview, IHudElementWithMouseOver, IHudElementWithVisibilityConfig
    {
        protected StatusEffectsListConfig Config => (StatusEffectsListConfig)_config;
        public VisibilityConfig? VisibilityConfig => Config is UnitFrameStatusEffectsListConfig config ? config.VisibilityConfig : null;

        private LayoutInfo _layoutInfo;

        internal static int StatusEffectListsSize = 60;
        private StatusStruct[]? _fakeEffects = null;

        private LabelHud _durationLabel;
        private LabelHud _stacksLabel;
        public IGameObject? Actor { get; set; } = null;

        private bool _wasHovering = false;
        private bool NeedsSpecialInput => !ClipRectsHelper.Instance.Enabled || ClipRectsHelper.Instance.Mode == WindowClippingMode.Performance;

        protected override bool AnchorToParent => Config is UnitFrameStatusEffectsListConfig config ? config.AnchorToUnitFrame : false;
        protected override DrawAnchor ParentAnchor => Config is UnitFrameStatusEffectsListConfig config ? config.UnitFrameAnchor : DrawAnchor.Center;

        public StatusEffectsListHud(StatusEffectsListConfig config, string? displayName = null) : base(config, displayName)
        {
            _config.ValueChangeEvent += OnConfigPropertyChanged;

            _durationLabel = new LabelHud(config.IconConfig.DurationLabelConfig);
            _stacksLabel = new LabelHud(config.IconConfig.StacksLabelConfig);

            UpdatePreview();
        }

        ~StatusEffectsListHud()
        {
            _config.ValueChangeEvent -= OnConfigPropertyChanged;
        }

        public void StopPreview()
        {
            Config.Preview = false;
            UpdatePreview();
        }

        protected override (List<Vector2>, List<Vector2>) ChildrenPositionsAndSizes()
        {
            Vector2 pos = LayoutHelper.CalculateStartPosition(Config.Position, Config.Size, LayoutHelper.GrowthDirectionsFromIndex(Config.Directions));
            return (new List<Vector2>() { pos + Config.Size / 2f }, new List<Vector2>() { Config.Size });
        }

        public void StopMouseover()
        {
            if (_wasHovering && NeedsSpecialInput)
            {
                InputsHelper.Instance.StopHandlingInputs();
                _wasHovering = false;
            }
        }

        private uint CalculateLayout(List<StatusEffectData> list)
        {
            var effectCount = (uint)list.Count;
            var count = Config.Limit >= 0 ? Math.Min((uint)Config.Limit, effectCount) : effectCount;

            if (count <= 0)
            {
                return 0;
            }

            _layoutInfo = LayoutHelper.CalculateLayout(
                Config.Size,
                Config.IconConfig.Size,
                count,
                Config.IconPadding,
                LayoutHelper.GetFillsRowsFirst(Config.FillRowsFirst, LayoutHelper.GrowthDirectionsFromIndex(Config.Directions))
            );

            return count;
        }

        protected string GetStatusActorName(StatusStruct status)
        {
            var character = Plugin.ObjectTable.SearchById(status.SourceObject.Id);
            return character == null ? "" : character.Name.ToString();
        }

        protected virtual List<StatusEffectData> StatusEffectsData()
        {
            var list = StatusEffectDataList(Actor);

            // sort by duration
            if (Config.SortByDuration)
            {
                list.Sort((a, b) =>
                {
                    float aTime = a.Data.IsPermanent || a.Data.IsFcBuff ? float.MaxValue : a.Status.RemainingTime;
                    float bTime = b.Data.IsPermanent || b.Data.IsFcBuff ? float.MaxValue : b.Status.RemainingTime;

                    if (Config.DurationSortType == StatusEffectDurationSortType.Ascending)
                    {
                        return aTime.CompareTo(bTime);
                    }
                    else
                    {
                        return bTime.CompareTo(aTime);
                    }
                });
            }
            // show mine or permanent first
            else if (Config.ShowMineFirst || Config.ShowPermanentFirst)
            {
                return OrderByMineOrPermanentFirst(list);
            }

            return list;
        }

        protected unsafe List<StatusEffectData> StatusEffectDataList(IGameObject? actor)
        {
            List<StatusEffectData> list = new List<StatusEffectData>();
            IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer;
            IBattleChara? character = null;
            int count = StatusEffectListsSize;

            if (_fakeEffects == null)
            {
                if (actor == null || actor is not IBattleChara battleChara)
                {
                    return list;
                }

                if (Config.HideWhenDead && (battleChara.IsDead || battleChara.CurrentHp <= 0))
                {
                    return list;
                }

                character = (IBattleChara)actor;

                try
                {
                    count = Math.Min(count, character.StatusList.Length);
                }
                catch { }
            }
            else
            {
                count = Config.Limit == -1 ? _fakeEffects.Length : Math.Min(Config.Limit, _fakeEffects.Length);
            }

            for (int i = 0; i < count; i++)
            {
                // status
                StatusStruct* status = null;

                if (_fakeEffects != null)
                {
                    var fakeStruct = _fakeEffects![i];
                    status = &fakeStruct;
                }
                else
                {
                    try
                    {
                        status = character?.StatusList[i] == null ? null : (StatusStruct*)character.StatusList[i]!.Address;
                    }
                    catch { }
                }

                if (status == null || status->StatusId == 0)
                {
                    continue;
                }

                // data
                LuminaStatus? data = null;

                if (_fakeEffects != null)
                {
                    data = Plugin.DataManager.GetExcelSheet<LuminaStatus>()?.GetRow(status->StatusId);
                }
                else
                {
                    try
                    {
                        data = character?.StatusList[i]?.GameData.Value;
                    } catch { }
                }

                if (data == null || !data.HasValue)
                {
                    continue;
                }

                // filter "invisible" status effects
                if (data.Value.Icon == 0 || data.Value.Name.ToString().Length == 0)
                {
                    continue;
                }

                // dont filter anything on preview mode
                if (_fakeEffects != null)
                {
                    list.Add(new StatusEffectData(*status, data.Value));
                    continue;
                }

                // buffs
                if (!Config.ShowBuffs && data.Value.StatusCategory == 1)
                {
                    continue;
                }

                // debuffs
                if (!Config.ShowDebuffs && data.Value.StatusCategory != 1)
                {
                    continue;
                }

                // permanent
                if (!Config.ShowPermanentEffects && data.Value.IsPermanent)
                {
                    continue;
                }

                // only mine
                var mine = player?.GameObjectId == status->SourceObject.Id;

                if (Config.IncludePetAsOwn)
                {
                    mine = player?.GameObjectId == status->SourceObject.Id || IsStatusFromPlayerPet(*status);
                }

                if (Config.ShowOnlyMine && !mine)
                {
                    continue;
                }

                // blacklist
                if (Config.BlacklistConfig.Enabled && !Config.BlacklistConfig.StatusAllowed(data.Value))
                {
                    continue;
                }

                list.Add(new StatusEffectData(*status, data.Value));
            }

            return list;
        }

        protected bool IsStatusFromPlayerPet(StatusStruct status)
        {
            var buddy = Plugin.BuddyList.PetBuddy;

            if (buddy == null)
            {
                return false;
            }

            return buddy.EntityId == status.SourceObject.Id;
        }

        protected List<StatusEffectData> OrderByMineOrPermanentFirst(List<StatusEffectData> list)
        {
            var player = Plugin.ObjectTable.LocalPlayer;
            if (player == null)
            {
                return list;
            }

            if (Config.ShowMineFirst && Config.ShowPermanentFirst)
            {
                return list.OrderByDescending(x => x.Status.SourceObject.Id == player.GameObjectId && x.Data.IsPermanent || x.Data.IsFcBuff)
                    .ThenByDescending(x => x.Status.SourceObject.Id == player.GameObjectId)
                    .ThenByDescending(x => x.Data.IsPermanent)
                    .ThenByDescending(x => x.Data.IsFcBuff)
                    .ToList();
            }
            else if (Config.ShowMineFirst && !Config.ShowPermanentFirst)
            {
                return list.OrderByDescending(x => x.Status.SourceObject.Id == player.GameObjectId)
                    .ToList();
            }
            else if (!Config.ShowMineFirst && Config.ShowPermanentFirst)
            {
                return list.OrderByDescending(x => x.Data.IsPermanent)
                    .ThenByDescending(x => x.Data.IsFcBuff)
                    .ToList();
            }

            return list;
        }

        public override void DrawChildren(Vector2 origin)
        {
            if (!Config.Enabled)
            {
                return;
            }

            if (_fakeEffects == null && (Actor == null || Actor.ObjectKind != ObjectKind.Player && Actor.ObjectKind != ObjectKind.BattleNpc))
            {
                return;
            }

            // calculate layout
            List<StatusEffectData> list = StatusEffectsData();

            // area
            GrowthDirections growthDirections = LayoutHelper.GrowthDirectionsFromIndex(Config.Directions);
            Vector2 position = origin + GetAnchoredPosition(Config.Position, Config.Size, DrawAnchor.TopLeft);
            Vector2 areaPos = LayoutHelper.CalculateStartPosition(position, Config.Size, growthDirections);
            Vector2 margin = new Vector2(14, 10);

            ImDrawListPtr drawList = ImGui.GetWindowDrawList();

            // no need to do anything else if there are no effects
            if (list.Count == 0)
            {
                if (_wasHovering && NeedsSpecialInput)
                {
                    _wasHovering = false;
                    InputsHelper.Instance.StopHandlingInputs();
                }

                return;
            }

            // calculate icon positions
            uint count = CalculateLayout(list);
            var (iconPositions, minPos, maxPos) = LayoutHelper.CalculateIconPositions(
                growthDirections,
                count,
                position,
                Config.Size,
                Config.IconConfig.Size,
                Config.IconPadding,
                LayoutHelper.GetFillsRowsFirst(Config.FillRowsFirst, growthDirections),
                _layoutInfo
            );

            // window
            // imgui clips the left and right borders inside windows for some reason
            // we make the window bigger so the actual drawable size is the expected one
            Vector2 windowPos = minPos - margin;
            Vector2 windowSize = maxPos - minPos;

            AddDrawAction(Config.StrataLevel, () =>
            {
                DrawHelper.DrawInWindow(ID, windowPos, windowSize + margin * 2, !Config.DisableInteraction, (drawList) =>
                {
                    // area
                    if (Config.Preview)
                    {
                        drawList.AddRectFilled(areaPos, areaPos + Config.Size, 0x88000000);
                    }

                    for (var i = 0; i < count; i++)
                    {
                        Vector2 iconPos = iconPositions[i];
                        var statusEffectData = list[i];

                        // shadow
                        if (Config.IconConfig.ShadowConfig! != null && Config.IconConfig.ShadowConfig.Enabled)
                        {
                            // Right Side
                            drawList.AddRectFilled(iconPos + new Vector2(Config.IconConfig.Size.X, Config.IconConfig.ShadowConfig.Offset), iconPos + Config.IconConfig.Size + new Vector2(Config.IconConfig.ShadowConfig.Offset, Config.IconConfig.ShadowConfig.Offset) + new Vector2(Config.IconConfig.ShadowConfig.Thickness - 1, Config.IconConfig.ShadowConfig.Thickness - 1), Config.IconConfig.ShadowConfig.Color.Base);

                            // Bottom Size
                            drawList.AddRectFilled(iconPos + new Vector2(Config.IconConfig.ShadowConfig.Offset, Config.IconConfig.Size.Y), iconPos + Config.IconConfig.Size + new Vector2(Config.IconConfig.ShadowConfig.Offset, Config.IconConfig.ShadowConfig.Offset) + new Vector2(Config.IconConfig.ShadowConfig.Thickness - 1, Config.IconConfig.ShadowConfig.Thickness - 1), Config.IconConfig.ShadowConfig.Color.Base);
                        }

                        // icon
                        var cropIcon = Config.IconConfig.CropIcon;
                        int stackCount = cropIcon ? 1 : statusEffectData.Data.MaxStacks > 0 ? statusEffectData.Status.Param : 0;
                        DrawHelper.DrawIcon<LuminaStatus>(drawList, statusEffectData.Data, iconPos, Config.IconConfig.Size, false, cropIcon, stackCount);

                        // border
                        var borderConfig = GetBorderConfig(statusEffectData);
                        if (borderConfig != null && cropIcon)
                        {
                            drawList.AddRect(iconPos, iconPos + Config.IconConfig.Size, borderConfig.Color.Base, 0, ImDrawFlags.None, borderConfig.Thickness);
                        }

                        // Draw dispell indicator above dispellable status effect on uncropped icons
                        if (borderConfig != null && !cropIcon && statusEffectData.Data.CanDispel)
                        {
                            var dispellIndicatorColor = new Vector4(141f / 255f, 206f / 255f, 229f / 255f, 100f / 100f);
                            // 24x32
                            drawList.AddRectFilled(
                                           iconPos + new Vector2(Config.IconConfig.Size.X * .07f, Config.IconConfig.Size.Y * .07f),
                                           iconPos + new Vector2(Config.IconConfig.Size.X * .93f, Config.IconConfig.Size.Y * .14f),
                                           ImGui.ColorConvertFloat4ToU32(dispellIndicatorColor),
                                           8f
                                       );
                        }
                    }
                });
            });

            StatusEffectData? hoveringData = null;
            IGameObject? character = Actor;

            // labels need to be drawn separated since they have their own window for clipping
            for (var i = 0; i < count; i++)
            {
                Vector2 iconPos = iconPositions[i];
                StatusEffectData statusEffectData = list[i];

                // duration
                if (Config.IconConfig.DurationLabelConfig.Enabled &&
                    !statusEffectData.Data.IsPermanent &&
                    !statusEffectData.Data.IsFcBuff)
                {
                    AddDrawAction(Config.IconConfig.DurationLabelConfig.StrataLevel, () =>
                    {
                        double duration = Math.Round(Math.Abs(statusEffectData.Status.RemainingTime));
                        Config.IconConfig.DurationLabelConfig.SetText(Utils.DurationToString(duration));
                        _durationLabel.Draw(iconPos, Config.IconConfig.Size, character);
                    });
                }

                // stacks
                if (Config.IconConfig.StacksLabelConfig.Enabled &&
                    statusEffectData.Data.MaxStacks > 0 &&
                    statusEffectData.Status.Param > 0 &&
                    !statusEffectData.Data.IsFcBuff)
                {
                    AddDrawAction(Config.IconConfig.StacksLabelConfig.StrataLevel, () =>
                    {
                        Config.IconConfig.StacksLabelConfig.SetText($"{statusEffectData.Status.Param}");
                        _stacksLabel.Draw(iconPos, Config.IconConfig.Size, character);
                    });
                }

                // tooltips / interaction
                if (ImGui.IsMouseHoveringRect(iconPos, iconPos + Config.IconConfig.Size))
                {
                    hoveringData = statusEffectData;
                }
            }

            if (hoveringData.HasValue)
            {
                StatusEffectData data = hoveringData.Value;

                if (NeedsSpecialInput)
                {
                    _wasHovering = true;
                    InputsHelper.Instance.StartHandlingInputs();
                }

                // tooltip
                if (Config.ShowTooltips)
                {
                    uint? iconId = data.Data.Icon > 0 ? data.Data.Icon : null;
                    TooltipsHelper.Instance.ShowTooltipOnCursor(
                        EncryptedStringsHelper.GetString(data.Data.Description.ToDalamudString().ToString()),
                        EncryptedStringsHelper.GetString(data.Data.Name.ToString()),
                        data.Status.StatusId,
                        GetStatusActorName(data.Status),
                        iconId,
                        HSUI.Helpers.TooltipIdKind.Status
                    );
                }

                bool leftClick = InputsHelper.Instance.HandlingMouseInputs ? InputsHelper.Instance.LeftButtonClicked : ImGui.GetIO().MouseClicked[0];
                bool rightClick = InputsHelper.Instance.HandlingMouseInputs ? InputsHelper.Instance.RightButtonClicked : ImGui.GetIO().MouseClicked[1];

                // remove buff on right click
                bool isFromPlayer = data.Status.SourceObject.Id == Plugin.ObjectTable.LocalPlayer?.GameObjectId;
                bool isTheEcho = data.Status.SourceObject.Id is 42 or 239;

                if (data.Data.StatusCategory == 1 && (isFromPlayer || isTheEcho) && rightClick)
                {
                    StatusManager.ExecuteStatusOff(data.Status.StatusId, data.Status.SourceObject.ObjectId);

                    if (NeedsSpecialInput)
                    {
                        _wasHovering = false;
                        InputsHelper.Instance.StopHandlingInputs();
                    }
                }

                // automatic add to black list with ctrl+alt+shift click
                if (Config.BlacklistConfig.Enabled &&
                    ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyAlt && ImGui.GetIO().KeyShift && leftClick)
                {
                    Config.BlacklistConfig.AddNewEntry(data.Data);
                    ConfigurationManager.Instance.ForceNeedsSave();

                    if (NeedsSpecialInput)
                    {
                        _wasHovering = false;
                        InputsHelper.Instance.StopHandlingInputs();
                    }
                }
            }
            else if (_wasHovering && NeedsSpecialInput)
            {
                _wasHovering = false;
                InputsHelper.Instance.StopHandlingInputs();
            }
        }

        public StatusEffectIconBorderConfig? GetBorderConfig(StatusEffectData statusEffectData)
        {
            StatusEffectIconBorderConfig? borderConfig = null;

            bool isFromPlayerPet = false;
            if (Config.IncludePetAsOwn)
            {
                isFromPlayerPet = IsStatusFromPlayerPet(statusEffectData.Status);
            }

            if (Config.IconConfig.OwnedBorderConfig.Enabled && (statusEffectData.Status.SourceObject.Id == Plugin.ObjectTable.LocalPlayer?.GameObjectId || isFromPlayerPet))
            {
                borderConfig = Config.IconConfig.OwnedBorderConfig;
            }
            else if (Config.IconConfig.DispellableBorderConfig.Enabled && statusEffectData.Data.CanDispel)
            {
                borderConfig = Config.IconConfig.DispellableBorderConfig;
            }
            else if (Config.IconConfig.BorderConfig.Enabled)
            {
                borderConfig = Config.IconConfig.BorderConfig;
            }

            return borderConfig;
        }

        private void OnConfigPropertyChanged(object? sender, OnChangeBaseArgs args)
        {
            if (args.PropertyName == "Preview")
            {
                UpdatePreview();
            }
        }

        private unsafe void UpdatePreview()
        {
            if (!Config.Preview)
            {
                _fakeEffects = null;
                return;
            }

            var RNG = new Random((int)ImGui.GetTime());
            _fakeEffects = new StatusStruct[StatusEffectListsSize];

            for (int i = 0; i < StatusEffectListsSize; i++)
            {
                var fakeStruct = new StatusStruct();

                // forcing "triplecast" buff first to always be able to test stacks
                fakeStruct.StatusId = i == 0 ? (ushort)1211 : (ushort)RNG.Next(1, 200);
                fakeStruct.RemainingTime = RNG.Next(1, 30);
                fakeStruct.Param = (byte)RNG.Next(1, 3);
                fakeStruct.SourceObject.Id = 0;

                _fakeEffects[i] = fakeStruct;
            }
        }
    }

    public struct StatusEffectData
    {
        public StatusStruct Status;
        public LuminaStatus Data;

        public StatusEffectData(StatusStruct status, LuminaStatus data)
        {
            Status = status;
            Data = data;
        }
    }
}
