using System.Buffers;
using System.Numerics;
using System.Text;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.ImGuiFontChooserDialog;
using Dalamud.Interface.Style;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using HellionChat.Code;
using HellionChat.GameFunctions.Types;
using HellionChat.Resources;

namespace HellionChat.Util;

internal static class ImGuiUtil
{
    private static Plugin Plugin = null!;

    public static void Initialize(Plugin plugin)
    {
        Plugin = plugin;
    }

    private static readonly ImGuiMouseButton[] Buttons =
    [
        ImGuiMouseButton.Left,
        ImGuiMouseButton.Middle,
        ImGuiMouseButton.Right,
    ];

    private static Payload? Hovered;
    private static Payload? LastLink;
    private static readonly List<(Vector2, Vector2)> PayloadBounds = [];

    internal static void PostPayload(Chunk chunk, PayloadHandler? handler)
    {
        var payload = chunk.Link;
        if (payload != null && ImGui.IsItemHovered())
        {
            Hovered = payload;
            ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
            handler?.Hover(payload);
        }
        else if (!ReferenceEquals(Hovered, payload))
        {
            Hovered = null;
        }

        if (handler == null)
            return;

        foreach (var button in Buttons)
            if (ImGui.IsItemClicked(button))
                handler.Click(chunk, payload, button);
    }

    // Ceiling on the byte buffer for a single rendered line. UTF-8 takes at
    // most 4 bytes per char; ImGui's internal ImString limit is well below
    // this and FFXIV's chat lines top out around a few hundred chars in
    // practice. The cap prevents an unbounded ArrayPool rent if a caller
    // ever feeds in a degenerate input.
    private const int MaxLineByteCount = 16 * 1024;

    internal static void WrapText(
        string csText,
        Chunk chunk,
        PayloadHandler? handler,
        Vector4 defaultText,
        float lineWidth
    )
    {
        if (csText.Length == 0)
            return;

        foreach (var part in csText.Split(["\r\n", "\r", "\n"], StringSplitOptions.None))
        {
            if (part.Length == 0)
            {
                ImGui.TextUnformatted("");
                continue;
            }

            // Allocate against the encoder's own MaxByteCount so the buffer
            // we hand to ImGui is sized by us. The actual byte count
            // returned by GetBytes is then validated against that ceiling
            // before any pointer arithmetic touches it; CodeQL recognises
            // that comparison as a sanitiser for the
            // cs/unvalidated-local-pointer-arithmetic taint flow.
            var maxBytes = Encoding.UTF8.GetMaxByteCount(part.Length);
            if (maxBytes <= 0 || maxBytes > MaxLineByteCount)
            {
                ImGui.TextUnformatted("");
                continue;
            }

            var buffer = ArrayPool<byte>.Shared.Rent(maxBytes);
            try
            {
                var written = Encoding.UTF8.GetBytes(part, 0, part.Length, buffer, 0);
                if (written <= 0 || written > maxBytes)
                {
                    ImGui.TextUnformatted("");
                    continue;
                }

                WrapEncodedLine(buffer.AsSpan(0, written), chunk, handler, defaultText, lineWidth);
            }
            finally
            {
                ArrayPool<byte>.Shared.Return(buffer);
            }
        }
    }

    private static unsafe void WrapEncodedLine(
        ReadOnlySpan<byte> bytes,
        Chunk chunk,
        PayloadHandler? handler,
        Vector4 defaultText,
        float lineWidth
    )
    {
        var byteCount = bytes.Length;
        if (byteCount == 0)
        {
            ImGui.TextUnformatted("");
            return;
        }

        fixed (byte* basePtr = bytes)
        {
            var widthLeft = ImGui.GetContentRegionAvail().X;
            var endPrev = CalcWordWrap(basePtr, 0, byteCount, widthLeft);
            if (endPrev < 0)
                return;

            var firstSpace = FindFirstSpace(bytes, 0, byteCount);
            var properBreak = firstSpace <= endPrev;
            if (properBreak)
            {
                DrawText(basePtr, 0, endPrev, chunk, handler, defaultText);
            }
            else if (lineWidth == 0f)
            {
                ImGui.TextUnformatted("");
            }
            else
            {
                // Check whether the next chunk would wrap at or past the
                // first space. If yes, force a line break.
                var wrapPos = CalcWordWrap(basePtr, 0, firstSpace, lineWidth);
                if (wrapPos >= firstSpace)
                    ImGui.TextUnformatted("");
            }

            widthLeft = ImGui.GetContentRegionAvail().X;
            var lineStart = 0;
            while (endPrev < byteCount)
            {
                if (properBreak)
                    lineStart = endPrev;

                // Skip a leading space at the start of a wrapped line.
                if (lineStart < byteCount && bytes[lineStart] == (byte)' ')
                    lineStart++;

                var newEnd = CalcWordWrap(basePtr, lineStart, byteCount, widthLeft);
                if (properBreak && newEnd == endPrev)
                    break;

                if (newEnd < 0)
                {
                    ImGui.TextUnformatted("");
                    ImGui.TextUnformatted("");
                    break;
                }

                endPrev = newEnd;
                DrawText(basePtr, lineStart, endPrev, chunk, handler, defaultText);

                if (!properBreak)
                {
                    properBreak = true;
                    widthLeft = ImGui.GetContentRegionAvail().X;
                }
            }
        }
    }

    private static unsafe int CalcWordWrap(byte* basePtr, int start, int end, float width)
    {
        var result = ImGuiNative.CalcWordWrapPositionA(
            ImGui.GetFont().Handle,
            ImGuiHelpers.GlobalScale,
            basePtr + start,
            basePtr + end,
            width
        );
        if (result == null)
            return -1;
        return (int)(result - basePtr);
    }

    private static unsafe void DrawText(
        byte* basePtr,
        int start,
        int end,
        Chunk chunk,
        PayloadHandler? handler,
        Vector4 defaultText
    )
    {
        var oldPos = ImGui.GetCursorScreenPos();

        ImGuiNative.TextUnformatted(basePtr + start, basePtr + end);
        PostPayload(chunk, handler);

        if (!ReferenceEquals(LastLink, chunk.Link))
            PayloadBounds.Clear();

        LastLink = chunk.Link;

        if (Hovered != null && ReferenceEquals(Hovered, chunk.Link))
        {
            defaultText.W = 0.25f;
            var actualCol = ColourUtil.Vector4ToAbgr(defaultText);
            ImGui
                .GetWindowDrawList()
                .AddRectFilled(oldPos, oldPos + ImGui.GetItemRectSize(), actualCol);

            foreach (var (boundsStart, boundsSize) in PayloadBounds)
                ImGui
                    .GetWindowDrawList()
                    .AddRectFilled(boundsStart, boundsStart + boundsSize, actualCol);

            PayloadBounds.Clear();
        }

        if (Hovered == null && chunk.Link != null)
            PayloadBounds.Add((oldPos, ImGui.GetItemRectSize()));
    }

    private static int FindFirstSpace(ReadOnlySpan<byte> bytes, int start, int end)
    {
        for (var i = start; i < end; i++)
            if (char.IsWhiteSpace((char)bytes[i]))
                return i;

        return end;
    }

    // ---------------------------------------------------------------
    // Inspired by ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12).
    // Upstream dropped the width parameter (no callers there); we keep
    // it because two ChatLogWindow header buttons size themselves to
    // match the ChannelIcon button's frame. The actual bug is the
    // manual size = width - 2 * CellPadding.X subtraction: CellPadding
    // scales with HUD scale, the raw int does not, so the button
    // shrank under high HUD scales. ImGui.Button already handles its
    // own frame padding internally — pass the measured width straight
    // through.
    // ---------------------------------------------------------------
    internal static bool IconButton(
        FontAwesomeIcon icon,
        string? id = null,
        string? tooltip = null,
        int width = 0
    )
    {
        var label = icon.ToIconString();
        if (id != null)
            label += $"##{id}";

        bool ret;
        using (Plugin.FontManager.FontAwesome.Push())
        {
            var size = width > 0 ? new Vector2(width, 0f) : Vector2.Zero;
            ret = ImGui.Button(label, size);
        }

        if (
            !string.IsNullOrEmpty(tooltip)
            && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)
        )
            Tooltip(tooltip);

        return ret;
    }

    internal static bool OptionCheckbox(ref bool value, string label, string? description = null)
    {
        var ret = ImGui.Checkbox(label, ref value);
        if (!string.IsNullOrEmpty(description))
            HelpText(description);

        return ret;
    }

    internal static void HelpText(string text)
    {
        using (ImRaii.TextWrapPos(0.0f))
        using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]))
            ImGui.TextUnformatted(text);
    }

    // Hellion Chat — compact help affordance: a dimmed "(?)" glyph rendered
    // on the same line as the previous item, with the long-form description
    // tucked into a hover tooltip. Lets us keep the settings panes scannable
    // instead of stacking a wall of HelpText paragraphs under every option.
    internal static void HelpMarker(string description)
    {
        ImGui.SameLine();
        using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]))
            ImGui.TextUnformatted("(?)");

        // AllowWhenDisabled — ohne das Flag liefert IsItemHovered bei
        // ausgegrauten Settings false, der User könnte nicht mehr lesen
        // warum eine Option nicht aktiv ist. Genau dann braucht er den
        // Hover-Tooltip aber am dringendsten.
        if (!ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
            return;

        using var tooltip = ImRaii.Tooltip();
        using (ImRaii.TextWrapPos(35.0f * ImGui.GetFontSize()))
            ImGui.TextUnformatted(description);
    }

    internal static void WarningText(string text, bool wrap = true)
    {
        var style = StyleModel.GetConfiguredStyle() ?? StyleModel.GetFromCurrent();
        var dalamudOrange = style.BuiltInColors?.DalamudOrange;

        using (ImRaii.TextWrapPos(wrap ? 0.0f : ImGui.GetFontSize() * 35.0f))
        using (
            ImRaii.PushColor(ImGuiCol.Text, dalamudOrange ?? Vector4.Zero, dalamudOrange != null)
        )
            ImGui.TextUnformatted(text);
    }

    internal static ImRaii.ComboDisposable BeginComboVertical(
        string label,
        string previewValue,
        ImGuiComboFlags flags = ImGuiComboFlags.None
    )
    {
        ImGui.TextUnformatted(label);
        ImGui.SetNextItemWidth(-1);
        return ImRaii.Combo($"##{label}", previewValue, flags);
    }

    internal static bool DragFloatVertical(
        string label,
        ref float value,
        float vSpeed = 1.0f,
        float vMin = float.MinValue,
        float vMax = float.MaxValue,
        string? format = null,
        ImGuiSliderFlags flags = ImGuiSliderFlags.None
    )
    {
        ImGui.TextUnformatted(label);
        ImGui.SetNextItemWidth(-1);
        return ImGui.DragFloat($"##{label}", ref value, vSpeed, vMin, vMax, format, flags);
    }

    internal static bool DragFloatVertical(
        string label,
        string description,
        ref float value,
        float vSpeed = 1.0f,
        float vMin = float.MinValue,
        float vMax = float.MaxValue,
        string? format = null,
        ImGuiSliderFlags flags = ImGuiSliderFlags.None
    )
    {
        ImGui.TextUnformatted(label);
        ImGui.SetNextItemWidth(-1);
        var r = ImGui.DragFloat($"##{label}", ref value, vSpeed, vMin, vMax, format, flags);
        HelpText(description);

        return r;
    }

    internal static bool InputIntVertical(
        string label,
        string description,
        ref int value,
        int step = 1,
        int stepFast = 100,
        ImGuiInputTextFlags flags = ImGuiInputTextFlags.None
    )
    {
        ImGui.TextUnformatted(label);
        ImGui.SetNextItemWidth(-1);
        var r = ImGui.InputInt($"##{label}", ref value, step, stepFast, flags: flags);
        HelpText(description);

        return r;
    }

    internal static void Tooltip(string tooltip)
    {
        using (ImRaii.Tooltip())
        using (ImRaii.TextWrapPos(ImGui.GetFontSize() * 35.0f))
            ImGui.TextUnformatted(tooltip);
    }

    public static SingleFontChooserDialog? FontChooser(
        string label,
        SingleFontSpec font,
        bool checkbox,
        ref bool checkboxValue,
        Predicate<IFontFamilyId>? exclusion = null,
        string? preview = null
    )
    {
        using var id = ImRaii.PushId(label);

        ImGui.TextUnformatted(label);
        if (checkbox)
        {
            ImGui.Checkbox("##enabled", ref checkboxValue);
            ImGui.SameLine();
        }

        var fontFamily = font.FontId.Family.EnglishName;
        var fontStyle = font.FontId.EnglishName;
        fontStyle = fontStyle.Equals(fontFamily) ? "" : $" - {fontStyle}";

        var buttonText = $"{fontFamily}{fontStyle} ({font.SizePt}pt)";
        if (!ImGui.Button($"{buttonText}##{label}"))
            return null;

        var chooser = SingleFontChooserDialog.CreateAuto((UiBuilder)Plugin.Interface.UiBuilder);
        chooser.SelectedFont = font;
        if (exclusion is not null)
            chooser.FontFamilyExcludeFilter = exclusion;
        if (preview is not null)
            chooser.PreviewText = preview;

        return chooser;
    }

    public static void FontSizeCombo(string label, ref float currentSize)
    {
        ImGui.TextUnformatted(label);
        ImGui.SetNextItemWidth(-1);
        using var combo = ImRaii.Combo($"##{label}", $"{currentSize:###.##}pt");
        if (!combo.Success)
            return;

        foreach (var size in FontManager.AxisFontSizeList)
            if (ImGui.Selectable($"{size:###.##}pt", currentSize.Equals(size)))
                currentSize = size;
    }

    public static bool Button(string id, FontAwesomeIcon icon, bool disabled)
    {
        using (ImRaii.Disabled(disabled))
            return ImGuiComponents.IconButton(id, icon);
    }

    internal static bool CtrlShiftButton(string label, string tooltip = "")
    {
        var ctrlShiftHeld = ImGui.GetIO() is { KeyCtrl: true, KeyShift: true };

        bool ret;
        using (ImRaii.Disabled(!ctrlShiftHeld))
            ret = ImGui.Button(label) && ctrlShiftHeld;

        if (tooltip.Length != 0 && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
            Tooltip(tooltip);

        return ret;
    }

    internal static void KeybindInput(string id, ref ConfigKeyBind? keybind)
    {
        var idUint = ImGui.GetID(id);

        using var pushedId = ImRaii.PushId(id);
        if (ImGui.GetStateStorage().GetBool(idUint))
        {
            var io = ImGui.GetIO();
            var currentMods = ModifierFlag.None;
            var modString = "";
            if (io.KeyCtrl)
            {
                currentMods |= ModifierFlag.Ctrl;
                modString += Language.Keybind_Modifier_Ctrl + " + ";
            }
            if (io.KeyShift)
            {
                currentMods |= ModifierFlag.Shift;
                modString += Language.Keybind_Modifier_Shift + " + ";
            }
            if (io.KeyAlt)
            {
                currentMods |= ModifierFlag.Alt;
                modString += Language.Keybind_Modifier_Alt + " + ";
            }

            var text = $"{modString}... ({Language.Keybind_EscToClear})";
            using (ImRaii.PushColor(ImGuiCol.TextSelectedBg, Vector4.Zero))
            {
                ImGui.SetKeyboardFocusHere();
                ImGui.InputText(id + "##keybind", ref text, 0, ImGuiInputTextFlags.ReadOnly);
            }

            if (ImGui.IsKeyPressed(ImGuiKey.Escape))
            {
                keybind = null;
                ImGui.GetStateStorage().SetBool(idUint, false);
                return;
            }

            foreach (var vk in Enum.GetValues<VirtualKey>())
            {
                if (
                    vk
                    is VirtualKey.NO_KEY
                        or VirtualKey.CONTROL
                        or VirtualKey.LCONTROL
                        or VirtualKey.RCONTROL
                        or VirtualKey.SHIFT
                        or VirtualKey.LSHIFT
                        or VirtualKey.RSHIFT
                        or VirtualKey.MENU
                        or VirtualKey.LMENU
                        or VirtualKey.RMENU
                )
                    continue;

                if (!vk.TryToImGui(out var imKey) || !ImGui.IsKeyPressed(imKey))
                    continue;

                keybind = new ConfigKeyBind { Modifier = currentMods, Key = vk };
                ImGui.GetStateStorage().SetBool(idUint, false);
                return;
            }
        }
        else
        {
            var text = $"({Language.Keybind_None})";
            if (keybind != null)
                text = keybind.ToString();
            if (ImGui.Button(text, new Vector2(-1, 0)))
                ImGui.GetStateStorage().SetBool(idUint, true);
        }
    }

    public static void DrawArrows(
        ref int selected,
        int min,
        int max,
        float spacing,
        int id = 0,
        string? tooltipLeft = null,
        string? tooltipRight = null
    )
    {
        // Prevents changing values from triggering EndDisable
        var isMin = selected == min;
        var isMax = selected == max;

        ImGui.SameLine(0, spacing);
        using (ImRaii.Disabled(isMin))
        {
            if (IconButton(FontAwesomeIcon.ArrowLeft, id.ToString()))
                selected--;
        }

        if (tooltipLeft != null && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
            ImGui.SetTooltip(tooltipLeft);

        ImGui.SameLine(0, spacing);

        using (ImRaii.Disabled(isMax))
        {
            // Parentheses pin the operator precedence: without them this resolves as
            // id.ToString() + "1" (e.g. "01" instead of "1").
            if (IconButton(FontAwesomeIcon.ArrowRight, (id + 1).ToString()))
                selected++;
        }

        if (tooltipRight != null && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
            ImGui.SetTooltip(tooltipRight);
    }

    public static void WrappedTextWithColor(Vector4 color, string text)
    {
        using (ImRaii.PushColor(ImGuiCol.Text, color))
            ImGui.TextWrapped(text);
    }

    public static void CenterText(string text, float indent = 0.0f)
    {
        indent *= ImGuiHelpers.GlobalScale;
        ImGui.SameLine(
            ((ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize(text).X) * 0.5f) + indent
        );
        ImGui.TextUnformatted(text);
    }

    internal static bool TryToImGui(this VirtualKey key, out ImGuiKey result)
    {
        result = key switch
        {
            VirtualKey.NO_KEY => ImGuiKey.None,
            VirtualKey.BACK => ImGuiKey.Backspace,
            VirtualKey.TAB => ImGuiKey.Tab,
            VirtualKey.RETURN => ImGuiKey.Enter,
            VirtualKey.SHIFT => ImGuiKey.ModShift,
            VirtualKey.CONTROL => ImGuiKey.ModCtrl,
            VirtualKey.MENU => ImGuiKey.ModAlt,
            VirtualKey.PAUSE => ImGuiKey.Pause,
            VirtualKey.CAPITAL => ImGuiKey.CapsLock,
            VirtualKey.ESCAPE => ImGuiKey.Escape,
            VirtualKey.SPACE => ImGuiKey.Space,
            VirtualKey.PRIOR => ImGuiKey.PageUp,
            VirtualKey.NEXT => ImGuiKey.PageDown,
            VirtualKey.END => ImGuiKey.End,
            VirtualKey.HOME => ImGuiKey.Home,
            VirtualKey.LEFT => ImGuiKey.LeftArrow,
            VirtualKey.UP => ImGuiKey.UpArrow,
            VirtualKey.RIGHT => ImGuiKey.RightArrow,
            VirtualKey.DOWN => ImGuiKey.DownArrow,
            VirtualKey.SNAPSHOT => ImGuiKey.PrintScreen,
            VirtualKey.INSERT => ImGuiKey.Insert,
            VirtualKey.DELETE => ImGuiKey.Delete,
            VirtualKey.KEY_0 => ImGuiKey.Key0,
            VirtualKey.KEY_1 => ImGuiKey.Key1,
            VirtualKey.KEY_2 => ImGuiKey.Key2,
            VirtualKey.KEY_3 => ImGuiKey.Key3,
            VirtualKey.KEY_4 => ImGuiKey.Key4,
            VirtualKey.KEY_5 => ImGuiKey.Key5,
            VirtualKey.KEY_6 => ImGuiKey.Key6,
            VirtualKey.KEY_7 => ImGuiKey.Key7,
            VirtualKey.KEY_8 => ImGuiKey.Key8,
            VirtualKey.KEY_9 => ImGuiKey.Key9,
            VirtualKey.A => ImGuiKey.A,
            VirtualKey.B => ImGuiKey.B,
            VirtualKey.C => ImGuiKey.C,
            VirtualKey.D => ImGuiKey.D,
            VirtualKey.E => ImGuiKey.E,
            VirtualKey.F => ImGuiKey.F,
            VirtualKey.G => ImGuiKey.G,
            VirtualKey.H => ImGuiKey.H,
            VirtualKey.I => ImGuiKey.I,
            VirtualKey.J => ImGuiKey.J,
            VirtualKey.K => ImGuiKey.K,
            VirtualKey.L => ImGuiKey.L,
            VirtualKey.M => ImGuiKey.M,
            VirtualKey.N => ImGuiKey.N,
            VirtualKey.O => ImGuiKey.O,
            VirtualKey.P => ImGuiKey.P,
            VirtualKey.Q => ImGuiKey.Q,
            VirtualKey.R => ImGuiKey.R,
            VirtualKey.S => ImGuiKey.S,
            VirtualKey.T => ImGuiKey.T,
            VirtualKey.U => ImGuiKey.U,
            VirtualKey.V => ImGuiKey.V,
            VirtualKey.W => ImGuiKey.W,
            VirtualKey.X => ImGuiKey.X,
            VirtualKey.Y => ImGuiKey.Y,
            VirtualKey.Z => ImGuiKey.Z,
            VirtualKey.LWIN => ImGuiKey.LeftSuper,
            VirtualKey.RWIN => ImGuiKey.RightSuper,
            VirtualKey.NUMPAD0 => ImGuiKey.Keypad0,
            VirtualKey.NUMPAD1 => ImGuiKey.Keypad1,
            VirtualKey.NUMPAD2 => ImGuiKey.Keypad2,
            VirtualKey.NUMPAD3 => ImGuiKey.Keypad3,
            VirtualKey.NUMPAD4 => ImGuiKey.Keypad4,
            VirtualKey.NUMPAD5 => ImGuiKey.Keypad5,
            VirtualKey.NUMPAD6 => ImGuiKey.Keypad6,
            VirtualKey.NUMPAD7 => ImGuiKey.Keypad7,
            VirtualKey.NUMPAD8 => ImGuiKey.Keypad8,
            VirtualKey.NUMPAD9 => ImGuiKey.Keypad9,
            VirtualKey.MULTIPLY => ImGuiKey.KeypadMultiply,
            VirtualKey.ADD => ImGuiKey.KeypadAdd,
            VirtualKey.SUBTRACT => ImGuiKey.KeypadSubtract,
            VirtualKey.DECIMAL => ImGuiKey.KeypadDecimal,
            VirtualKey.DIVIDE => ImGuiKey.KeypadDivide,
            VirtualKey.F1 => ImGuiKey.F1,
            VirtualKey.F2 => ImGuiKey.F2,
            VirtualKey.F3 => ImGuiKey.F3,
            VirtualKey.F4 => ImGuiKey.F4,
            VirtualKey.F5 => ImGuiKey.F5,
            VirtualKey.F6 => ImGuiKey.F6,
            VirtualKey.F7 => ImGuiKey.F7,
            VirtualKey.F8 => ImGuiKey.F8,
            VirtualKey.F9 => ImGuiKey.F9,
            VirtualKey.F10 => ImGuiKey.F10,
            VirtualKey.F11 => ImGuiKey.F11,
            VirtualKey.F12 => ImGuiKey.F12,
            VirtualKey.NUMLOCK => ImGuiKey.NumLock,
            VirtualKey.SCROLL => ImGuiKey.ScrollLock,
            VirtualKey.OEM_NEC_EQUAL => ImGuiKey.KeypadEqual,
            VirtualKey.LSHIFT => ImGuiKey.LeftShift,
            VirtualKey.RSHIFT => ImGuiKey.RightShift,
            VirtualKey.LCONTROL => ImGuiKey.LeftCtrl,
            VirtualKey.RCONTROL => ImGuiKey.RightCtrl,
            VirtualKey.LMENU => ImGuiKey.LeftAlt,
            VirtualKey.RMENU => ImGuiKey.RightAlt,
            VirtualKey.OEM_1 => ImGuiKey.Semicolon,
            VirtualKey.OEM_PLUS => ImGuiKey.Equal,
            VirtualKey.OEM_COMMA => ImGuiKey.Comma,
            VirtualKey.OEM_MINUS => ImGuiKey.Minus,
            VirtualKey.OEM_PERIOD => ImGuiKey.Period,
            VirtualKey.OEM_2 => ImGuiKey.Slash,
            VirtualKey.OEM_3 => ImGuiKey.GraveAccent,
            VirtualKey.OEM_4 => ImGuiKey.LeftBracket,
            VirtualKey.OEM_5 => ImGuiKey.Backslash,
            VirtualKey.OEM_6 => ImGuiKey.RightBracket,
            VirtualKey.OEM_7 => ImGuiKey.Apostrophe,
            _ => 0,
        };

        return result != 0 || key == VirtualKey.NO_KEY;
    }

    public static void ChannelSelector(
        string headerText,
        Dictionary<ChatType, (ChatSource Source, ChatSource Target)> chatCodes
    )
    {
        var spacing = 3.0f * ImGuiHelpers.GlobalScale;

        using var channelNode = ImRaii.TreeNode(headerText);
        if (!channelNode.Success)
            return;

        foreach (var (header, types) in ChatTypeExt.SortOrder)
        {
            using var pushedId = ImRaii.PushId(header);

            if (ImGuiComponents.IconButton(FontAwesomeIcon.Check))
            {
                foreach (var type in types)
                    chatCodes.TryAdd(type, (ChatSourceExt.All, ChatSourceExt.All));
            }

            if (ImGui.IsItemHovered())
                ImGui.SetTooltip(Language.ChannelSelector_Select);

            ImGui.SameLine(0, spacing);

            if (ImGuiComponents.IconButton(FontAwesomeIcon.Times))
            {
                foreach (var type in types)
                    chatCodes.Remove(type);
            }

            if (ImGui.IsItemHovered())
                ImGui.SetTooltip(Language.ChannelSelector_Unselect);

            ImGui.SameLine(0, spacing);

            using var headerNode = ImRaii.TreeNode(header);
            if (!headerNode.Success)
                continue;

            foreach (var type in types)
            {
                if (type.IsGm())
                    continue;

                var enabled = chatCodes.ContainsKey(type);
                if (ImGui.Checkbox($"##{type.Name()}", ref enabled))
                {
                    if (enabled)
                        chatCodes[type] = (ChatSourceExt.All, ChatSourceExt.All);
                    else
                        chatCodes.Remove(type);
                }

                ImGui.SameLine();

                if (!type.HasSource())
                {
                    ImGui.TextUnformatted(type.Name());
                    continue;
                }

                using var typeNode = ImRaii.TreeNode($"{type.Name()}");
                if (!typeNode.Success)
                    continue;

                ImGui.Text(Language.ImGuiUtil_ChannelSelector_Source);
                ImGui.SameLine(400.0f * ImGuiHelpers.GlobalScale);
                ImGui.Text(Language.ImGuiUtil_ChannelSelector_Target);

                chatCodes.TryGetValue(type, out var sourcesEnum);
                var sources = (uint)sourcesEnum.Source;
                var targets = (uint)sourcesEnum.Target;

                foreach (var kind in Enum.GetValues<ChatSource>().Where(s => s != ChatSource.None))
                {
                    if (ImGui.CheckboxFlags($"{kind.Name()}##source", ref sources, (uint)kind))
                        chatCodes[type] = ((ChatSource)sources, sourcesEnum.Target);

                    ImGui.SameLine(400.0f * ImGuiHelpers.GlobalScale);

                    if (ImGui.CheckboxFlags($"{kind.Name()}##target", ref targets, (uint)kind))
                        chatCodes[type] = (sourcesEnum.Source, (ChatSource)targets);
                }
            }
        }
    }

    public static void ExtraChatSelector(
        string headerText,
        ref bool all,
        HashSet<Guid> extraChatChannels
    )
    {
        if (Plugin.ExtraChat.ChannelNames.Count <= 0)
            return;

        using var extraTree = ImRaii.TreeNode(headerText);
        if (!extraTree.Success)
            return;

        ImGui.Checkbox(Language.Options_Tabs_ExtraChatAll, ref all);
        ImGui.Separator();

        using var _ = ImRaii.Disabled(all);
        foreach (var (id, name) in Plugin.ExtraChat.ChannelNames)
        {
            var enabled = extraChatChannels.Contains(id);
            if (!ImGui.Checkbox($"{name}##ec-{id}", ref enabled))
                continue;

            if (enabled)
                extraChatChannels.Add(id);
            else
                extraChatChannels.Remove(id);
        }
    }
}
