using System.Collections;
using HellionChat.Code;
using HellionChat.GameFunctions.Types;
using HellionChat.Resources;
using HellionChat.Util;
using Dalamud;
using Dalamud.Configuration;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Bindings.ImGui;

namespace HellionChat;

[Serializable]
public class ConfigKeyBind
{
    public ModifierFlag Modifier;
    public VirtualKey Key;

    public override string ToString()
    {
        var modString = "";
        if (Modifier.HasFlag(ModifierFlag.Ctrl))
            modString += Language.Keybind_Modifier_Ctrl + " + ";
        if (Modifier.HasFlag(ModifierFlag.Shift))
            modString += Language.Keybind_Modifier_Shift + " + ";
        if (Modifier.HasFlag(ModifierFlag.Alt))
            modString += Language.Keybind_Modifier_Alt + " + ";
        return modString+Key.GetFancyName();
    }
}

[Serializable]
public class Configuration : IPluginConfiguration
{
    private const int LatestVersion = 14;

    public int Version { get; set; } = LatestVersion;

    // v1.1.0 — Theme-Engine. Slug-basiert; ThemeRegistry liefert das Objekt.
    public string Theme = "hellion-arctic";

    // v1.1.0 — Globale Window-Opacity, theme-übergreifend. Migration aus
    // HellionThemeWindowOpacity beim Bump v13 → v14.
    public float WindowOpacity = 0.85f;

    // v1.1.0 — Felder für künftige UI-Toggles (v1.2.0 / v1.3.0). Werden
    // vorab angelegt, damit später keine Migration nötig ist.
    public bool ReduceMotion;
    public bool UseCompactDensity;
    public bool ShowThemeQuickPicker;

    // Hellion Chat — Privacy filter (DSGVO Art. 25 Privacy by Default).
    // Master-switch defaults to true; set false to restore upstream behavior.
    public bool PrivacyFilterEnabled = true;
    // Empty set means the migration has not run yet — see Plugin.cs v6→v7.
    public HashSet<ChatType> PrivacyPersistChannels = [];
    // Failsafe for ChatTypes added by future FFXIV patches we don't know about.
    public bool PrivacyPersistUnknownChannels;

    public bool IsAllowedForStorage(ChatType type)
    {
        if (!PrivacyFilterEnabled)
            return true;
        if (PrivacyPersistChannels.Contains(type))
            return true;
        return PrivacyPersistUnknownChannels;
    }

    // Hellion Chat — Message retention (GDPR data minimization, time axis).
    // Master switch defaults to false; the plugin will not delete history
    // until the user explicitly opts in.
    public bool RetentionEnabled;
    public int RetentionDefaultDays = 30;
    public Dictionary<ChatType, int> RetentionPerChannelDays = [];
    public DateTimeOffset RetentionLastRunAt = DateTimeOffset.MinValue;

    // Hellion Chat first-run wizard — opens once on a fresh install. Existing
    // ChatTwo users skip it because the v6→v7 migration sets the flag.
    public bool FirstRunCompleted;

    // Hellion Chat global ImGui theme — applied to every plugin window in
    // Plugin.Draw. Default ON; users who prefer the upstream Dalamud look
    // can flip this off in the Privacy tab.
    [Obsolete("Replaced by Theme slug + WindowOpacity in v14")]
    public bool HellionThemeEnabled = true;

    // Window background opacity, 0.5–1.0. Lower values make the plugin
    // panes more glass-like so the game shines through. Default 0.5
    // matches the maintainer's daily-driver preference; users who want
    // a less translucent look bump it up in Aussehen → Theme.
    [Obsolete("Replaced by WindowOpacity in v14")]
    public float HellionThemeWindowOpacity = 0.5f;

    // Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font
    // instead of whatever GlobalFontV2.FontId points at. Default ON so a
    // fresh install gets the Hellion typography out-of-the-box; flip OFF
    // to fall back to the user's chosen system or Dalamud font.
    public bool UseHellionFont = true;

    // Hellion Chat — Auto-Tell-Tabs. When enabled, an incoming or outgoing
    // /tell spawns a session-only tab dedicated to that conversation
    // partner. See spec: Hellion Chat Auto-Tell-Tabs Spec (Obsidian).
    public bool EnableAutoTellTabs = true;
    // Hard cap on simultaneously open auto tell tabs. Range enforced by the
    // settings slider (1–50). LRU drop favors greeted tabs first.
    public int AutoTellTabsLimit = 15;
    // When true the sidebar shows only a thin separator before the temp
    // tabs; when false a section header "Active Tells (n)" is rendered.
    public bool AutoTellTabsCompactDisplay;
    // Number of prior tells to preload from the message store when an
    // auto tell tab is spawned. Range 0–100; 0 disables preload.
    public int AutoTellTabsHistoryPreload = 20;
    // Show the greeter "marked-as-greeted" toggle button next to each
    // temp tab and dim the tab name when set. Off by default because the
    // workflow is specific to club-greeter use cases — most users just
    // want the auto tabs themselves without the extra UI affordance.
    public bool AutoTellTabsShowGreetedToggle;

    // Hellion Chat — One-Time-Hint-Banner that introduces the v0.6.0 pop-out
    // input feature. Set to true once the user dismisses the banner from a
    // pop-out window; never reset after that.
    public bool SeenPopOutInputHint;

    // Hellion Chat — v0.6.0 master switch for the pop-out input bar.
    // Global on purpose: per-tab makes no sense for Auto-Tell-Tabs which
    // are session-only and would force the user to re-enable it for every
    // new conversation. Default flipped to ON in v0.6.1 (was OFF in v0.6.0)
    // because tester feedback called the manual toggle "umständlich, wirkt
    // unfertig". v11 → v12 migration applies the same flip to existing users.
    public bool PopOutInputEnabled = true;

    // Hellion Chat — v0.6.1 One-Time-Hint-Banner that introduces the
    // chat-header pop-out toolbar button and reminds about the pop-out
    // input default flip. Set to true once the user dismisses the banner
    // from the main chat window; never reset after that.
    public bool SeenPopOutHeaderHint;

    // Hellion Chat — v0.6.1 opt-in: when true, AutoTellTabsService.SpawnTempTab
    // sets tab.PopOut = true on every new auto-tell tab so the conversation
    // pops out as its own window directly. Closing the pop-out returns the
    // tab to the sidebar via the standard Popout.OnClose() flow. Default OFF
    // because the existing sidebar workflow is what most users (especially
    // club greeters tracking many parallel tells) expect by default.
    public bool AutoTellTabsOpenAsPopout;

    public int GetRetentionDays(ChatType type)
    {
        if (RetentionPerChannelDays.TryGetValue(type, out var userOverride))
            return userOverride;
        if (Privacy.PrivacyDefaults.DefaultRetentionDays.TryGetValue(type, out var specDefault))
            return specDefault;
        return RetentionDefaultDays;
    }

    public bool HideChat = true;
    public bool HideDuringCutscenes = true;
    public bool HideWhenNotLoggedIn = true;
    public bool HideWhenUiHidden = true;
    public bool HideInLoadingScreens;
    public bool HideInBattle;
    public bool HideInNewGamePlusMenu;
    public bool HideWhenInactive;
    public int InactivityHideTimeout = 10;
    public bool InactivityHideActiveDuringBattle = true;

    [Obsolete("Use InactivityHideChannelsV2 instead")]
    public Dictionary<ChatType, ChatSource> InactivityHideChannels = [];

    public Dictionary<ChatType, (ChatSource, ChatSource)> InactivityHideChannelsV2 = [];
    public bool InactivityHideExtraChatAll = true;
    public HashSet<Guid> InactivityHideExtraChatChannels = [];
    public bool ShowHideButton = true;
    public bool NativeItemTooltips = true;
    public bool PrettierTimestamps = true;
    public bool MoreCompactPretty;
    public bool HideSameTimestamps;
    public bool ShowNoviceNetwork;
    // Hellion Chat — vertical sidebar tab layout reads better than the
    // horizontal tab strip in the company of Auto-Tell-Tabs (a club
    // greeter typically tracks 5–15 simultaneous conversations). Bestand
    // users keep their saved value untouched — only fresh installs pick
    // up the new default.
    public bool SidebarTabView = true;
    public bool PrintChangelog = true;
    public bool OnlyPreviewIf;
    public int PreviewMinimum = 1;
    public PreviewPosition PreviewPosition = PreviewPosition.Inside;
    public CommandHelpSide CommandHelpSide = CommandHelpSide.None;
    public KeybindMode KeybindMode = KeybindMode.Strict;
    public LanguageOverride LanguageOverride = LanguageOverride.None;
    public bool CanMove = true;
    public bool CanResize = true;
    public bool ShowTitleBar = true;
    public bool ShowPopOutTitleBar = true;
    public bool DatabaseBattleMessages;
    public bool LoadPreviousSession;
    public bool FilterIncludePreviousSessions;
    public bool SortAutoTranslate;
    public bool CollapseDuplicateMessages;
    public bool CollapseKeepUniqueLinks;
    public bool PlaySounds = true;
    public bool KeepInputFocus = true;
    public int MaxLinesToRender = 5_000; // 1-10000
    // Default ON to match a German / European 24h locale. The
    // ChatLogWindow.cs format-flip in v0.5.1 honours this strictly via
    // CultureInfo.InvariantCulture so the result is consistent across
    // host locales.
    public bool Use24HourClock = true;

    public bool ShowEmotes = true;
    public HashSet<string> BlockedEmotes = [];

    public bool FontsEnabled = true;
    public ExtraGlyphRanges ExtraGlyphRanges = 0;
    public float FontSizeV2 = 12.75f;
    public float SymbolsFontSizeV2 = 12.75f;
    public SingleFontSpec GlobalFontV2 = new()
    {
        // dalamud only ships KR as regular, which chat2 used previously for global fonts
        FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular),
        SizePt = 12.75f,
    };
    public SingleFontSpec JapaneseFontV2 = new()
    {
        FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkMedium),
        SizePt = 12.75f,
    };
    public bool ItalicEnabled;
    public SingleFontSpec ItalicFontV2 = new()
    {
        FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular),
        SizePt = 12.75f,
    };

    public float TooltipOffset;
    public float WindowAlpha = 100f;
    public Dictionary<ChatType, uint> ChatColours = new();
    public bool ColorSelectedInputChannelButton = true;
    public List<Tab> Tabs = [];

    public bool OverrideStyle;
    public string? ChosenStyle;

    public ConfigKeyBind? ChatTabForward;
    public ConfigKeyBind? ChatTabBackward;

    public void UpdateFrom(Configuration other, bool backToOriginal)
    {
        if (backToOriginal)
            foreach (var tab in Tabs.Where(t => t.PopOut))
                tab.PopOut = false;

        HideChat = other.HideChat;
        HideDuringCutscenes = other.HideDuringCutscenes;
        HideWhenNotLoggedIn = other.HideWhenNotLoggedIn;
        HideWhenUiHidden = other.HideWhenUiHidden;
        HideInLoadingScreens = other.HideInLoadingScreens;
        HideInBattle = other.HideInBattle;
        HideInNewGamePlusMenu = other.HideInNewGamePlusMenu;
        HideWhenInactive = other.HideWhenInactive;
        InactivityHideTimeout = other.InactivityHideTimeout;
        InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle;
        InactivityHideChannelsV2 = other.InactivityHideChannelsV2.ToDictionary(pair => pair.Key, pair => pair.Value);
        InactivityHideExtraChatAll = other.InactivityHideExtraChatAll;
        InactivityHideExtraChatChannels = other.InactivityHideExtraChatChannels.ToHashSet();
        ShowHideButton = other.ShowHideButton;
        NativeItemTooltips = other.NativeItemTooltips;
        PrettierTimestamps = other.PrettierTimestamps;
        MoreCompactPretty = other.MoreCompactPretty;
        HideSameTimestamps = other.HideSameTimestamps;
        ShowNoviceNetwork = other.ShowNoviceNetwork;
        SidebarTabView = other.SidebarTabView;
        PrintChangelog = other.PrintChangelog;
        OnlyPreviewIf = other.OnlyPreviewIf;
        PreviewMinimum = other.PreviewMinimum;
        PreviewPosition = other.PreviewPosition;
        CommandHelpSide = other.CommandHelpSide;
        KeybindMode = other.KeybindMode;
        LanguageOverride = other.LanguageOverride;
        CanMove = other.CanMove;
        CanResize = other.CanResize;
        ShowTitleBar = other.ShowTitleBar;
        ShowPopOutTitleBar = other.ShowPopOutTitleBar;
        DatabaseBattleMessages = other.DatabaseBattleMessages;
        LoadPreviousSession = other.LoadPreviousSession;
        FilterIncludePreviousSessions = other.FilterIncludePreviousSessions;
        SortAutoTranslate = other.SortAutoTranslate;
        CollapseDuplicateMessages = other.CollapseDuplicateMessages;
        CollapseKeepUniqueLinks = other.CollapseKeepUniqueLinks;
        PlaySounds = other.PlaySounds;
        KeepInputFocus = other.KeepInputFocus;
        MaxLinesToRender = other.MaxLinesToRender;
        Use24HourClock = other.Use24HourClock;
        ShowEmotes = other.ShowEmotes;
        // Deep-copy the set so the live and mutable Configuration instances don't share state
        // — a HashSet reference assignment would cause edits in the settings window to leak
        // into the live config before the user clicks Save.
        BlockedEmotes = new HashSet<string>(other.BlockedEmotes);
        FontsEnabled = other.FontsEnabled;
        ItalicEnabled = other.ItalicEnabled;
        ExtraGlyphRanges = other.ExtraGlyphRanges;
        FontSizeV2 = other.FontSizeV2;
        GlobalFontV2 = other.GlobalFontV2;
        JapaneseFontV2 = other.JapaneseFontV2;
        ItalicFontV2 = other.ItalicFontV2;
        SymbolsFontSizeV2 = other.SymbolsFontSizeV2;
        TooltipOffset = other.TooltipOffset;
        WindowAlpha = other.WindowAlpha;
        ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
        ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;

        // Hellion Chat — Auto-Tell-Tabs are session-only and therefore
        // never present in a disk-loaded copy. Keep the live temp tabs of
        // *this* configuration alive across an UpdateFrom so a settings
        // save (or sidebar-mode toggle) does not silently destroy the
        // user's open tell conversations. Persistent tabs from `other`
        // still get the regular clone-replace treatment.
        var liveTempTabs = Tabs.Where(t => t.IsTempTab).ToList();
        Tabs = other.Tabs.Where(t => !t.IsTempTab).Select(t => t.Clone()).ToList();
        Tabs.AddRange(liveTempTabs);

        OverrideStyle = other.OverrideStyle;
        ChosenStyle = other.ChosenStyle;
        ChatTabForward = other.ChatTabForward;
        ChatTabBackward = other.ChatTabBackward;

        PrivacyFilterEnabled = other.PrivacyFilterEnabled;
        PrivacyPersistChannels = [..other.PrivacyPersistChannels];
        PrivacyPersistUnknownChannels = other.PrivacyPersistUnknownChannels;

        RetentionEnabled = other.RetentionEnabled;
        RetentionDefaultDays = other.RetentionDefaultDays;
        RetentionPerChannelDays = other.RetentionPerChannelDays.ToDictionary(p => p.Key, p => p.Value);
        RetentionLastRunAt = other.RetentionLastRunAt;

        FirstRunCompleted = other.FirstRunCompleted;
#pragma warning disable CS0612, CS0618 // Obsolete-Felder bleiben bis v1.2.0 als JSON-Safety-Net erhalten
        HellionThemeEnabled = other.HellionThemeEnabled;
        HellionThemeWindowOpacity = other.HellionThemeWindowOpacity;
#pragma warning restore CS0612, CS0618
        UseHellionFont = other.UseHellionFont;

        // v1.1.0 theme engine fields
        Theme = other.Theme;
        WindowOpacity = other.WindowOpacity;
        ReduceMotion = other.ReduceMotion;
        UseCompactDensity = other.UseCompactDensity;
        ShowThemeQuickPicker = other.ShowThemeQuickPicker;

        EnableAutoTellTabs = other.EnableAutoTellTabs;
        AutoTellTabsLimit = other.AutoTellTabsLimit;
        AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
        AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload;
        AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle;

        SeenPopOutInputHint = other.SeenPopOutInputHint;
        PopOutInputEnabled = other.PopOutInputEnabled;
        SeenPopOutHeaderHint = other.SeenPopOutHeaderHint;
        AutoTellTabsOpenAsPopout = other.AutoTellTabsOpenAsPopout;
    }
}

[Serializable]
public enum UnreadMode
{
    All,
    Unseen,
    None,
}

public static class UnreadModeExt
{
    internal static string Name(this UnreadMode mode) => mode switch
    {
        UnreadMode.All => Language.UnreadMode_All,
        UnreadMode.Unseen => Language.UnreadMode_Unseen,
        UnreadMode.None => Language.UnreadMode_None,
        _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
    };

    internal static string? Tooltip(this UnreadMode mode) => mode switch
    {
        UnreadMode.All => Language.UnreadMode_All_Tooltip,
        UnreadMode.Unseen => Language.UnreadMode_Unseen_Tooltip,
        UnreadMode.None => Language.UnreadMode_None_Tooltip,
        _ => null,
    };
}

[Serializable]
public class Tab
{
    public string Name = Language.Tab_DefaultName;

    // v1.2.0 — optionaler FontAwesome-Glyph-Name. Null bedeutet:
    // Default-Mapping aus TabIconMapping greift (basiert auf Tab-Name).
    // User können hier per Settings → Tabs einen eigenen Glyph setzen.
    public string? Icon = null;

    [Obsolete("Removed in favor of SelectedChannels")]
    public Dictionary<ChatType, ChatSource> ChatCodes = new();

    public Dictionary<ChatType, (ChatSource, ChatSource)> SelectedChannels = new();
    public bool ExtraChatAll;
    public HashSet<Guid> ExtraChatChannels = [];

    public UnreadMode UnreadMode = UnreadMode.Unseen;
    public bool UnhideOnActivity;
    public bool DisplayTimestamp = true;
    public InputChannel? Channel;
    public bool PopOut;
    public bool IndependentOpacity;
    public float Opacity = 100f;
    public bool InputDisabled;

    public bool CanMove = true;
    public bool CanResize = true;

    public bool IndependentHide;
    public bool HideDuringCutscenes = true;
    public bool HideWhenNotLoggedIn = true;
    public bool HideWhenUiHidden = true;
    public bool HideInLoadingScreens;
    public bool HideInBattle;
    public bool HideWhenInactive;

    public bool IsTempTab;
    public bool AllSenderMessages;
    public TellTarget TellTarget = TellTarget.Empty();

    [NonSerialized] public uint Unread;
    [NonSerialized] public uint LastSendUnread;
    [NonSerialized] public long LastActivity;
    [NonSerialized] public MessageList Messages = new();

    [NonSerialized] public UsedChannel CurrentChannel = new();

    [NonSerialized] public Guid Identifier = Guid.NewGuid();

    // Hellion Chat — Auto-Tell-Tabs greeted flag. Toggled manually from the
    // sidebar to mark a tell partner as already greeted in the current
    // session. NonSerialized because the temp tab itself is session-only.
    [NonSerialized] public bool IsGreeted;

    public bool Matches(Message message)
    {
        if (!message.Matches(SelectedChannels, ExtraChatAll, ExtraChatChannels))
        {
            return false;
        }

        // Auto-tell temp tabs are bound to a single conversation partner;
        // every other tell that matches the channel filter must NOT land
        // here, otherwise all temp tabs would mirror "Tell Exclusive".
        if (IsTempTab && TellTarget?.IsSet() == true)
        {
            return ChunkUtil.MatchesSender(message, TellTarget.Name, TellTarget.World);
        }

        return true;
    }

    public void AddMessage(Message message, bool unread = true)
    {
        Messages.AddPrune(message, MessageManager.MessageDisplayLimit);
        if (!unread)
            return;

        Unread += 1;
        if (message.Matches(Plugin.Config.InactivityHideChannelsV2, Plugin.Config.InactivityHideExtraChatAll, Plugin.Config.InactivityHideExtraChatChannels))
            LastActivity = Environment.TickCount64;
    }

    public void Clear()
        => Messages.Clear();

    public Tab Clone()
    {
        return new Tab
        {
            Name = Name,
            SelectedChannels = SelectedChannels.ToDictionary(pair => pair.Key, pair => pair.Value),
            ExtraChatAll = ExtraChatAll,
            ExtraChatChannels = ExtraChatChannels.ToHashSet(),
            UnreadMode = UnreadMode,
            UnhideOnActivity = UnhideOnActivity,
            Unread = Unread,
            LastActivity = LastActivity,
            DisplayTimestamp = DisplayTimestamp,
            Channel = Channel,
            PopOut = PopOut,
            IndependentOpacity = IndependentOpacity,
            Opacity = Opacity,
            Identifier = Identifier,
            InputDisabled = InputDisabled,
            CurrentChannel = CurrentChannel,
            CanMove = CanMove,
            CanResize = CanResize,
            IndependentHide = IndependentHide,
            HideDuringCutscenes = HideDuringCutscenes,
            HideWhenNotLoggedIn = HideWhenNotLoggedIn,
            HideWhenUiHidden = HideWhenUiHidden,
            HideInLoadingScreens = HideInLoadingScreens,
            HideInBattle = HideInBattle,
            HideWhenInactive = HideWhenInactive,
            IsTempTab = IsTempTab,
            AllSenderMessages = AllSenderMessages,
            TellTarget = TellTarget.From(TellTarget),
            IsGreeted = IsGreeted,
        };
    }

    /// <summary>
    /// MessageList provides an ordered list of messages with duplicate ID
    /// tracking, sorting and mutex protection.
    /// </summary>
    public class MessageList
    {
        private readonly SemaphoreSlim LockSlim = new(1, 1);

        private readonly List<Message> Messages;
        private readonly HashSet<Guid> TrackedMessageIds;

        public MessageList()
        {
            Messages = [];
            TrackedMessageIds = [];
        }

        public MessageList(int initialCapacity)
        {
            Messages = new List<Message>(initialCapacity);
            TrackedMessageIds = new HashSet<Guid>(initialCapacity);
        }

        public void AddPrune(Message message, int max)
        {
            LockSlim.Wait(-1);
            try
            {
                AddLocked(message);
                PruneMaxLocked(max);
            }
            finally
            {
                LockSlim.Release();
            }
        }

        public void AddSortPrune(IEnumerable<Message> messages, int max)
        {
            LockSlim.Wait(-1);
            try
            {
                foreach (var message in messages)
                    AddLocked(message);

                SortLocked();
                PruneMaxLocked(max);
            }
            finally
            {
                LockSlim.Release();
            }
        }

        private void AddLocked(Message message)
        {
            if (TrackedMessageIds.Contains(message.Id))
                return;

            Messages.Add(message);
            TrackedMessageIds.Add(message.Id);
        }

        public void Clear()
        {
            LockSlim.Wait(-1);
            try
            {
                Messages.Clear();
                TrackedMessageIds.Clear();
            }
            finally
            {
                LockSlim.Release();
            }
        }

        private void SortLocked()
        {
            Messages.Sort((a, b) => a.Date.CompareTo(b.Date));
        }

        private void PruneMaxLocked(int max)
        {
            while (Messages.Count > max)
            {
                TrackedMessageIds.Remove(Messages[0].Id);
                Messages.RemoveAt(0);
            }
        }

        /// <summary>
        /// Returns an array copy of the message list for usage outside of main thread
        /// </summary>
        public async Task<Message[]> GetCopy(int millisecondsTimeout = -1)
        {
            await LockSlim.WaitAsync(millisecondsTimeout);
            try
            {
                return Messages.ToArray();
            }
            finally
            {
                LockSlim.Release();
            }
        }

        /// <summary>
        /// GetReadOnly returns a read-only list of messages while holding a
        /// reader lock. The list should be used with a using statement.
        /// </summary>
        public RLockedMessageList GetReadOnly(int millisecondsTimeout = -1)
        {
            LockSlim.Wait(millisecondsTimeout);
            return new RLockedMessageList(LockSlim, Messages);
        }

        public class RLockedMessageList(SemaphoreSlim lockSlim, List<Message> messages) : IReadOnlyList<Message>, IDisposable
        {
            public IEnumerator<Message> GetEnumerator()
            {
                return messages.GetEnumerator();
            }

            IEnumerator IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }

            public int Count => messages.Count;

            public Message this[int index] => messages[index];

            public void Dispose()
            {
                lockSlim.Release();
            }
        }
    }
}

public class UsedChannel
{
    public InputChannel Channel = InputChannel.Invalid;
    public List<Chunk> Name = [];
    public TellTarget? TellTarget;

    public bool UseTempChannel;
    public InputChannel TempChannel = InputChannel.Invalid;
    public TellTarget? TempTellTarget;

    public void ResetTempChannel()
    {
        UseTempChannel = false;
        TempTellTarget = null;
        TempChannel = InputChannel.Invalid;
    }

    public void SetChannel(InputChannel channel)
    {
        Channel = channel;
    }
}

[Serializable]
public enum PreviewPosition
{
    None,
    Inside,
    Top,
    Bottom,
    Tooltip,
}

public static class PreviewPositionExt
{
    public static string Name(this PreviewPosition position) => position switch
    {
        PreviewPosition.None => Language.Options_Preview_None,
        PreviewPosition.Inside => Language.Options_Preview_Inside,
        PreviewPosition.Top => Language.Options_Preview_Top,
        PreviewPosition.Bottom => Language.Options_Preview_Bottom,
        PreviewPosition.Tooltip => Language.Options_Preview_Tooltip,
        _ => throw new ArgumentOutOfRangeException(nameof(position), position, null),
    };
}

[Serializable]
public enum CommandHelpSide
{
    None,
    Left,
    Right,
}

public static class CommandHelpSideExt
{
    public static string Name(this CommandHelpSide side) => side switch
    {
        CommandHelpSide.None => Language.CommandHelpSide_None,
        CommandHelpSide.Left => Language.CommandHelpSide_Left,
        CommandHelpSide.Right => Language.CommandHelpSide_Right,
        _ => throw new ArgumentOutOfRangeException(nameof(side), side, null),
    };
}

[Serializable]
public enum KeybindMode
{
    Flexible,
    Strict,
}

public static class KeybindModeExt
{
    public static string Name(this KeybindMode mode) => mode switch
    {
        KeybindMode.Flexible => Language.KeybindMode_Flexible_Name,
        KeybindMode.Strict => Language.KeybindMode_Strict_Name,
        _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
    };

    public static string? Tooltip(this KeybindMode mode) => mode switch
    {
        KeybindMode.Flexible => Language.KeybindMode_Flexible_Tooltip,
        KeybindMode.Strict => Language.KeybindMode_Strict_Tooltip,
        _ => null,
    };
}

[Serializable]
public enum LanguageOverride
{
    None,
    ChineseSimplified,
    ChineseTraditional,
    Dutch,
    English,
    French,
    German,
    Greek,

    // Italian,
    Japanese,

    // Korean,
    // Norwegian,
    PortugueseBrazil,
    Romanian,
    Russian,
    Spanish,
    Swedish,
}

public static class LanguageOverrideExt
{
    public static string Name(this LanguageOverride mode) => mode switch
    {
        LanguageOverride.None => Language.LanguageOverride_None,
        LanguageOverride.ChineseSimplified => "简体中文",
        LanguageOverride.ChineseTraditional => "繁體中文",
        LanguageOverride.Dutch => "Nederlands",
        LanguageOverride.English => "English",
        LanguageOverride.French => "Français",
        LanguageOverride.German => "Deutsch",
        LanguageOverride.Greek => "Ελληνικά",
        // LanguageOverride.Italian => "Italiano",
        LanguageOverride.Japanese => "日本語",
        // LanguageOverride.Korean => "한국어 (Korean)",
        // LanguageOverride.Norwegian => "Norsk",
        LanguageOverride.PortugueseBrazil => "Português do Brasil",
        LanguageOverride.Romanian => "Română",
        LanguageOverride.Russian => "Русский",
        LanguageOverride.Spanish => "Español",
        LanguageOverride.Swedish => "Svenska",
        _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
    };

    public static string Code(this LanguageOverride mode) => mode switch
    {
        LanguageOverride.None => "",
        LanguageOverride.ChineseSimplified => "zh-hans",
        LanguageOverride.ChineseTraditional => "zh-hant",
        LanguageOverride.Dutch => "nl",
        LanguageOverride.English => "en",
        LanguageOverride.French => "fr",
        LanguageOverride.German => "de",
        LanguageOverride.Greek => "el",
        // LanguageOverride.Italian => "it",
        LanguageOverride.Japanese => "ja",
        // LanguageOverride.Korean => "ko",
        // LanguageOverride.Norwegian => "no",
        LanguageOverride.PortugueseBrazil => "pt-br",
        LanguageOverride.Romanian => "ro",
        LanguageOverride.Russian => "ru",
        LanguageOverride.Spanish => "es",
        LanguageOverride.Swedish => "sv",
        _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
    };
}

[Serializable]
[Flags]
public enum ExtraGlyphRanges
{
    ChineseFull = 1 << 0,
    ChineseSimplifiedCommon = 1 << 1,
    Cyrillic = 1 << 2,
    Japanese = 1 << 3,
    Korean = 1 << 4,
    Thai = 1 << 5,
    Vietnamese = 1 << 6,
}

public static class ExtraGlyphRangesExt
{
    public static string Name(this ExtraGlyphRanges ranges) => ranges switch
    {
        ExtraGlyphRanges.ChineseFull => Language.ExtraGlyphRanges_ChineseFull_Name,
        ExtraGlyphRanges.ChineseSimplifiedCommon => Language.ExtraGlyphRanges_ChineseSimplifiedCommon_Name,
        ExtraGlyphRanges.Cyrillic => Language.ExtraGlyphRanges_Cyrillic_Name,
        ExtraGlyphRanges.Japanese => Language.ExtraGlyphRanges_Japanese_Name,
        ExtraGlyphRanges.Korean => Language.ExtraGlyphRanges_Korean_Name,
        ExtraGlyphRanges.Thai => Language.ExtraGlyphRanges_Thai_Name,
        ExtraGlyphRanges.Vietnamese => Language.ExtraGlyphRanges_Vietnamese_Name,
        _ => throw new ArgumentOutOfRangeException(nameof(ranges), ranges, null),
    };

    public static unsafe nint Range(this ExtraGlyphRanges ranges) => ranges switch
    {
        ExtraGlyphRanges.ChineseFull => (nint)ImGui.GetIO().Fonts.GetGlyphRangesChineseFull(),
        ExtraGlyphRanges.ChineseSimplifiedCommon => (nint)ImGui.GetIO().Fonts.GetGlyphRangesChineseSimplifiedCommon(),
        ExtraGlyphRanges.Cyrillic => (nint)ImGui.GetIO().Fonts.GetGlyphRangesCyrillic(),
        ExtraGlyphRanges.Japanese => (nint)ImGui.GetIO().Fonts.GetGlyphRangesJapanese(),
        ExtraGlyphRanges.Korean => (nint)ImGui.GetIO().Fonts.GetGlyphRangesKorean(),
        ExtraGlyphRanges.Thai => (nint)ImGui.GetIO().Fonts.GetGlyphRangesThai(),
        ExtraGlyphRanges.Vietnamese => (nint)ImGui.GetIO().Fonts.GetGlyphRangesVietnamese(),
        _ => throw new ArgumentOutOfRangeException(nameof(ranges), ranges, null),
    };
}
