using Dalamud.Interface;
using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using ImGuiNET;
using Lumina.Data.Files;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Services;
using Penumbra.UI.Classes;
using ApiChangedItemIcon = Penumbra.Api.Enums.ChangedItemIcon;

namespace Penumbra.UI;

public class ChangedItemDrawer : IDisposable, IUiService
{
    [Flags]
    public enum ChangedItemIcon : uint
    {
        Head          = 0x00_00_01,
        Body          = 0x00_00_02,
        Hands         = 0x00_00_04,
        Legs          = 0x00_00_08,
        Feet          = 0x00_00_10,
        Ears          = 0x00_00_20,
        Neck          = 0x00_00_40,
        Wrists        = 0x00_00_80,
        Finger        = 0x00_01_00,
        Monster       = 0x00_02_00,
        Demihuman     = 0x00_04_00,
        Customization = 0x00_08_00,
        Action        = 0x00_10_00,
        Mainhand      = 0x00_20_00,
        Offhand       = 0x00_40_00,
        Unknown       = 0x00_80_00,
        Emote         = 0x01_00_00,
    }

    private static readonly ChangedItemIcon[] Order =
    [
        ChangedItemIcon.Head,
        ChangedItemIcon.Body,
        ChangedItemIcon.Hands,
        ChangedItemIcon.Legs,
        ChangedItemIcon.Feet,
        ChangedItemIcon.Ears,
        ChangedItemIcon.Neck,
        ChangedItemIcon.Wrists,
        ChangedItemIcon.Finger,
        ChangedItemIcon.Mainhand,
        ChangedItemIcon.Offhand,
        ChangedItemIcon.Customization,
        ChangedItemIcon.Action,
        ChangedItemIcon.Emote,
        ChangedItemIcon.Monster,
        ChangedItemIcon.Demihuman,
        ChangedItemIcon.Unknown,
    ];

    private static readonly string[] LowerNames = Order.Select(f => ToDescription(f).ToLowerInvariant()).ToArray();

    public static bool TryParseIndex(ReadOnlySpan<char> input, out ChangedItemIcon slot)
    {
        // Handle numeric cases before TryParse because numbers
        // are not logical otherwise.
        if (int.TryParse(input, out var idx))
        {
            // We assume users will use 1-based index, but if they enter 0, just use the first.
            if (idx == 0)
            {
                slot = Order[0];
                return true;
            }

            // Use 1-based index.
            --idx;
            if (idx >= 0 && idx < Order.Length)
            {
                slot = Order[idx];
                return true;
            }
        }

        slot = 0;
        return false;
    }

    public static bool TryParsePartial(string lowerInput, out ChangedItemIcon slot)
    {
        if (TryParseIndex(lowerInput, out slot))
            return true;

        slot = 0;
        foreach (var (item, flag) in LowerNames.Zip(Order))
        {
            if (item.Contains(lowerInput, StringComparison.Ordinal))
                slot |= flag;
        }

        return slot != 0;
    }

    public const           ChangedItemIcon AllFlags      = (ChangedItemIcon)0x01FFFF;
    public static readonly int             NumCategories = Order.Length;
    public const           ChangedItemIcon DefaultFlags  = AllFlags & ~ChangedItemIcon.Offhand;

    private readonly Configuration                                    _config;
    private readonly ExcelSheet<Item>                                 _items;
    private readonly CommunicatorService                              _communicator;
    private readonly Dictionary<ChangedItemIcon, IDalamudTextureWrap> _icons = new(16);
    private          float                                            _smallestIconWidth;

    public static Vector2 TypeFilterIconSize
        => new(2 * ImGui.GetTextLineHeight());

    public ChangedItemDrawer(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator,
        Configuration config)
    {
        _items = gameData.GetExcelSheet<Item>()!;
        uiBuilder.RunWhenUiPrepared(() => CreateEquipSlotIcons(uiBuilder, gameData, textureProvider), true);
        _communicator = communicator;
        _config       = config;
    }

    public void Dispose()
    {
        foreach (var wrap in _icons.Values.Distinct())
            wrap.Dispose();
        _icons.Clear();
    }

    /// <summary> Check if a changed item should be drawn based on its category. </summary>
    public bool FilterChangedItem(string name, object? data, LowerString filter)
        => (_config.Ephemeral.ChangedItemFilter == AllFlags || _config.Ephemeral.ChangedItemFilter.HasFlag(GetCategoryIcon(name, data)))
         && (filter.IsEmpty || filter.IsContained(ChangedItemFilterName(name, data)));

    /// <summary> Draw the icon corresponding to the category of a changed item. </summary>
    public void DrawCategoryIcon(string name, object? data)
        => DrawCategoryIcon(GetCategoryIcon(name, data));

    public void DrawCategoryIcon(ChangedItemIcon iconType)
    {
        var height = ImGui.GetFrameHeight();
        if (!_icons.TryGetValue(iconType, out var icon))
        {
            ImGui.Dummy(new Vector2(height));
            return;
        }

        ImGui.Image(icon.ImGuiHandle, new Vector2(height));
        if (ImGui.IsItemHovered())
        {
            using var tt = ImRaii.Tooltip();
            ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth));
            ImGui.SameLine();
            ImGuiUtil.DrawTextButton(ToDescription(iconType), new Vector2(0, _smallestIconWidth), 0);
        }
    }

    /// <summary>
    /// Draw a changed item, invoking the Api-Events for clicks and tooltips.
    /// Also draw the item Id in grey if requested.
    /// </summary>
    public void DrawChangedItem(string name, object? data)
    {
        name = ChangedItemName(name, data);
        using (var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0,   0.5f))
                   .Push(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, ImGui.GetStyle().CellPadding.Y * 2)))
        {
            var ret = ImGui.Selectable(name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight()))
                ? MouseButton.Left
                : MouseButton.None;
            ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret;
            ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret;
            if (ret != MouseButton.None)
                _communicator.ChangedItemClick.Invoke(ret, Convert(data));
        }

        if (_communicator.ChangedItemHover.HasTooltip && ImGui.IsItemHovered())
        {
            // We can not be sure that any subscriber actually prints something in any case.
            // Circumvent ugly blank tooltip with less-ugly useless tooltip.
            using var tt    = ImRaii.Tooltip();
            using var group = ImRaii.Group();
            _communicator.ChangedItemHover.Invoke(Convert(data));
            group.Dispose();
            if (ImGui.GetItemRectSize() == Vector2.Zero)
                ImGui.TextUnformatted("No actions available.");
        }
    }

    /// <summary> Draw the model information, right-justified. </summary>
    public void DrawModelData(object? data)
    {
        if (!GetChangedItemObject(data, out var text))
            return;

        ImGui.SameLine(ImGui.GetContentRegionAvail().X);
        ImGui.AlignTextToFramePadding();
        ImGuiUtil.RightJustify(text, ColorId.ItemId.Value());
    }

    /// <summary> Draw a header line with the different icon types to filter them. </summary>
    public void DrawTypeFilter()
    {
        if (_config.HideChangedItemFilters)
            return;

        var typeFilter = _config.Ephemeral.ChangedItemFilter;
        if (DrawTypeFilter(ref typeFilter))
        {
            _config.Ephemeral.ChangedItemFilter = typeFilter;
            _config.Ephemeral.Save();
        }
    }

    /// <summary> Draw a header line with the different icon types to filter them. </summary>
    public bool DrawTypeFilter(ref ChangedItemIcon typeFilter)
    {
        var       ret   = false;
        using var _     = ImRaii.PushId("ChangedItemIconFilter");
        var       size  = TypeFilterIconSize;
        using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);


        bool DrawIcon(ChangedItemIcon type, ref ChangedItemIcon typeFilter)
        {
            var ret  = false;
            var icon = _icons[type];
            var flag = typeFilter.HasFlag(type);
            ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f));
            if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
            {
                typeFilter = flag ? typeFilter & ~type : typeFilter | type;
                ret        = true;
            }

            using var popup = ImRaii.ContextPopupItem(type.ToString());
            if (popup)
                if (ImGui.MenuItem("Enable Only This"))
                {
                    typeFilter = type;
                    ret        = true;
                    ImGui.CloseCurrentPopup();
                }

            if (ImGui.IsItemHovered())
            {
                using var tt = ImRaii.Tooltip();
                ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth));
                ImGui.SameLine();
                ImGuiUtil.DrawTextButton(ToDescription(type), new Vector2(0, _smallestIconWidth), 0);
            }

            return ret;
        }

        foreach (var iconType in Order)
        {
            ret |= DrawIcon(iconType, ref typeFilter);
            ImGui.SameLine();
        }

        ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X);
        ImGui.Image(_icons[AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One,
            typeFilter == 0        ? new Vector4(0.6f,  0.3f,  0.3f,  1f) :
            typeFilter == AllFlags ? new Vector4(0.75f, 0.75f, 0.75f, 1f) : new Vector4(0.5f, 0.5f, 1f, 1f));
        if (ImGui.IsItemClicked())
        {
            typeFilter = typeFilter == AllFlags ? 0 : AllFlags;
            ret        = true;
        }

        return ret;
    }

    /// <summary> Obtain the icon category corresponding to a changed item. </summary>
    internal static ChangedItemIcon GetCategoryIcon(string name, object? obj)
    {
        var iconType = ChangedItemIcon.Unknown;
        switch (obj)
        {
            case EquipItem it:
                iconType = GetCategoryIcon(it.Type.ToSlot());
                break;
            case ModelChara m:
                iconType = (CharacterBase.ModelType)m.Type switch
                {
                    CharacterBase.ModelType.DemiHuman => ChangedItemIcon.Demihuman,
                    CharacterBase.ModelType.Monster   => ChangedItemIcon.Monster,
                    _                                 => ChangedItemIcon.Unknown,
                };
                break;
            default:
            {
                if (name.StartsWith("Action: "))
                    iconType = ChangedItemIcon.Action;
                else if (name.StartsWith("Emote: "))
                    iconType = ChangedItemIcon.Emote;
                else if (name.StartsWith("Customization: "))
                    iconType = ChangedItemIcon.Customization;
                break;
            }
        }

        return iconType;
    }

    internal static ChangedItemIcon GetCategoryIcon(EquipSlot slot)
        => slot switch
        {
            EquipSlot.MainHand => ChangedItemIcon.Mainhand,
            EquipSlot.OffHand  => ChangedItemIcon.Offhand,
            EquipSlot.Head     => ChangedItemIcon.Head,
            EquipSlot.Body     => ChangedItemIcon.Body,
            EquipSlot.Hands    => ChangedItemIcon.Hands,
            EquipSlot.Legs     => ChangedItemIcon.Legs,
            EquipSlot.Feet     => ChangedItemIcon.Feet,
            EquipSlot.Ears     => ChangedItemIcon.Ears,
            EquipSlot.Neck     => ChangedItemIcon.Neck,
            EquipSlot.Wrists   => ChangedItemIcon.Wrists,
            EquipSlot.RFinger  => ChangedItemIcon.Finger,
            _                  => ChangedItemIcon.Unknown,
        };

    /// <summary> Return more detailed object information in text, if it exists. </summary>
    private static bool GetChangedItemObject(object? obj, out string text)
    {
        switch (obj)
        {
            case EquipItem it:
                text = it.ModelString;
                return true;
            case ModelChara m:
                text = $"({((CharacterBase.ModelType)m.Type).ToName()} {m.Model}-{m.Base}-{m.Variant})";
                return true;
            default:
                text = string.Empty;
                return false;
        }
    }

    /// <summary> We need to transform the internal EquipItem type to the Lumina Item type for API-events. </summary>
    private object? Convert(object? data)
    {
        if (data is EquipItem it)
            return (_items.GetRow(it.ItemId.Id), it.Type);

        return data;
    }

    private static string ToDescription(ChangedItemIcon icon)
        => icon switch
        {
            ChangedItemIcon.Head          => EquipSlot.Head.ToName(),
            ChangedItemIcon.Body          => EquipSlot.Body.ToName(),
            ChangedItemIcon.Hands         => EquipSlot.Hands.ToName(),
            ChangedItemIcon.Legs          => EquipSlot.Legs.ToName(),
            ChangedItemIcon.Feet          => EquipSlot.Feet.ToName(),
            ChangedItemIcon.Ears          => EquipSlot.Ears.ToName(),
            ChangedItemIcon.Neck          => EquipSlot.Neck.ToName(),
            ChangedItemIcon.Wrists        => EquipSlot.Wrists.ToName(),
            ChangedItemIcon.Finger        => "Ring",
            ChangedItemIcon.Monster       => "Monster",
            ChangedItemIcon.Demihuman     => "Demi-Human",
            ChangedItemIcon.Customization => "Customization",
            ChangedItemIcon.Action        => "Action",
            ChangedItemIcon.Emote         => "Emote",
            ChangedItemIcon.Mainhand      => "Weapon (Mainhand)",
            ChangedItemIcon.Offhand       => "Weapon (Offhand)",
            _                             => "Other",
        };

    internal static ApiChangedItemIcon ToApiIcon(ChangedItemIcon icon)
        => icon switch
        {
            ChangedItemIcon.Head          => ApiChangedItemIcon.Head,
            ChangedItemIcon.Body          => ApiChangedItemIcon.Body,
            ChangedItemIcon.Hands         => ApiChangedItemIcon.Hands,
            ChangedItemIcon.Legs          => ApiChangedItemIcon.Legs,
            ChangedItemIcon.Feet          => ApiChangedItemIcon.Feet,
            ChangedItemIcon.Ears          => ApiChangedItemIcon.Ears,
            ChangedItemIcon.Neck          => ApiChangedItemIcon.Neck,
            ChangedItemIcon.Wrists        => ApiChangedItemIcon.Wrists,
            ChangedItemIcon.Finger        => ApiChangedItemIcon.Finger,
            ChangedItemIcon.Monster       => ApiChangedItemIcon.Monster,
            ChangedItemIcon.Demihuman     => ApiChangedItemIcon.Demihuman,
            ChangedItemIcon.Customization => ApiChangedItemIcon.Customization,
            ChangedItemIcon.Action        => ApiChangedItemIcon.Action,
            ChangedItemIcon.Emote         => ApiChangedItemIcon.Emote,
            ChangedItemIcon.Mainhand      => ApiChangedItemIcon.Mainhand,
            ChangedItemIcon.Offhand       => ApiChangedItemIcon.Offhand,
            ChangedItemIcon.Unknown       => ApiChangedItemIcon.Unknown,
            _                             => ApiChangedItemIcon.None,
        };

    /// <summary> Apply Changed Item Counters to the Name if necessary. </summary>
    private static string ChangedItemName(string name, object? data)
        => data is int counter ? $"{counter} Files Manipulating {name}s" : name;

    /// <summary> Add filterable information to the string. </summary>
    private static string ChangedItemFilterName(string name, object? data)
        => data switch
        {
            int counter  => $"{counter} Files Manipulating {name}s",
            EquipItem it => $"{name}\0{(GetChangedItemObject(it, out var t) ? t : string.Empty)}",
            ModelChara m => $"{name}\0{(GetChangedItemObject(m,  out var t) ? t : string.Empty)}",
            _            => name,
        };

    /// <summary> Initialize the icons. </summary>
    private bool CreateEquipSlotIcons(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider)
    {
        using var equipTypeIcons = uiBuilder.LoadUld("ui/uld/ArmouryBoard.uld");

        if (!equipTypeIcons.Valid)
            return false;

        void Add(ChangedItemIcon icon, IDalamudTextureWrap? tex)
        {
            if (tex != null)
                _icons.Add(icon, tex);
        }

        Add(ChangedItemIcon.Mainhand,      equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0));
        Add(ChangedItemIcon.Head,          equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1));
        Add(ChangedItemIcon.Body,          equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2));
        Add(ChangedItemIcon.Hands,         equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3));
        Add(ChangedItemIcon.Legs,          equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5));
        Add(ChangedItemIcon.Feet,          equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6));
        Add(ChangedItemIcon.Offhand,       equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7));
        Add(ChangedItemIcon.Ears,          equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8));
        Add(ChangedItemIcon.Neck,          equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9));
        Add(ChangedItemIcon.Wrists,        equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10));
        Add(ChangedItemIcon.Finger,        equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11));
        Add(ChangedItemIcon.Monster,       textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062042_hr1.tex")!));
        Add(ChangedItemIcon.Demihuman,     textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062041_hr1.tex")!));
        Add(ChangedItemIcon.Customization, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062043_hr1.tex")!));
        Add(ChangedItemIcon.Action,        textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062001_hr1.tex")!));
        Add(ChangedItemIcon.Emote,         LoadEmoteTexture(gameData, textureProvider));
        Add(ChangedItemIcon.Unknown,       LoadUnknownTexture(gameData, textureProvider));
        Add(AllFlags,                      textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/114000/114052_hr1.tex")!));

        _smallestIconWidth = _icons.Values.Min(i => i.Width);

        return true;
    }

    private static unsafe IDalamudTextureWrap? LoadUnknownTexture(IDataManager gameData, ITextureProvider textureProvider)
    {
        var unk = gameData.GetFile<TexFile>("ui/uld/levelup2_hr1.tex");
        if (unk == null)
            return null;

        var image = unk.GetRgbaImageData();
        var bytes = new byte[unk.Header.Height * unk.Header.Height * 4];
        var diff  = 2 * (unk.Header.Height - unk.Header.Width);
        for (var y = 0; y < unk.Header.Height; ++y)
            image.AsSpan(4 * y * unk.Header.Width, 4 * unk.Header.Width).CopyTo(bytes.AsSpan(4 * y * unk.Header.Height + diff));

        return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(unk.Header.Height, unk.Header.Height), bytes, "Penumbra.UnkItemIcon");
    }

    private static unsafe IDalamudTextureWrap? LoadEmoteTexture(IDataManager gameData, ITextureProvider textureProvider)
    {
        var emote = gameData.GetFile<TexFile>("ui/icon/000000/000019_hr1.tex");
        if (emote == null)
            return null;

        var image2 = emote.GetRgbaImageData();
        fixed (byte* ptr = image2)
        {
            var color = (uint*)ptr;
            for (var i = 0; i < image2.Length / 4; ++i)
            {
                if (color[i] == 0xFF000000)
                    image2[i * 4 + 3] = 0;
            }
        }

        return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(emote.Header.Width, emote.Header.Height), image2, "Penumbra.EmoteItemIcon");
    }
}
