using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Oxide.Ext.Discord.Attributes;
using Oxide.Ext.Discord.Builders;
using Oxide.Ext.Discord.Cache;
using Oxide.Ext.Discord.Clients;
using Oxide.Ext.Discord.Connections;
using Oxide.Ext.Discord.Constants;
using Oxide.Ext.Discord.Entities;
using Oxide.Ext.Discord.Extensions;
using Oxide.Ext.Discord.Interfaces;
using Oxide.Ext.Discord.Libraries;
using Oxide.Ext.Discord.Logging;
using Oxide.Ext.Discord.Types;
using ProtoBuf;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using UnityEngine;

//DiscordPlayers created with PluginMerge v(1.0.9.0) by MJSU @ https://github.com/dassjosh/Plugin.Merge
namespace Oxide.Plugins
{
    [Info("Discord Players", "MJSU", "3.0.0")]
    [Description("Displays online players in discord")]
    public partial class DiscordPlayers : CovalencePlugin, IDiscordPlugin, IDiscordPool
    {
        #region Plugins\DiscordPlayers.Config.cs
        protected override void LoadDefaultConfig() { }
        
        protected override void LoadConfig()
        {
            base.LoadConfig();
            Config.Settings.DefaultValueHandling = DefaultValueHandling.Populate;
            _pluginConfig = AdditionalConfig(Config.ReadObject<PluginConfig>());
            Config.WriteObject(_pluginConfig);
        }
        
        private PluginConfig AdditionalConfig(PluginConfig config)
        {
            config.CommandMessages ??= new List<CommandSettings>();
            if (config.CommandMessages.Count == 0)
            {
                config.CommandMessages.Add(new CommandSettings
                {
                    Command = "players",
                    ShowAdmins = true,
                    AllowInDm = true,
                    EmbedFieldLimit = 25
                });
                
                config.CommandMessages.Add(new CommandSettings
                {
                    Command = "playersadmin",
                    ShowAdmins = true,
                    AllowInDm = true,
                    EmbedFieldLimit = 25
                });
            }
            
            config.Permanent ??= new List<PermanentMessageSettings>
            {
                new()
                {
                    Enabled = false,
                    ChannelId = new Snowflake(0),
                    UpdateRate = 1f,
                    EmbedFieldLimit = 25
                }
            };
            
            for (int index = 0; index < config.CommandMessages.Count; index++)
            {
                CommandSettings settings = new(config.CommandMessages[index]);
                config.CommandMessages[index] = settings;
            }
            
            for (int index = 0; index < config.Permanent.Count; index++)
            {
                PermanentMessageSettings settings = new(config.Permanent[index]);
                config.Permanent[index] = settings;
            }
            
            return config;
        }
        #endregion

        #region Plugins\DiscordPlayers.DiscordHooks.cs
        [HookMethod(DiscordExtHooks.OnDiscordGatewayReady)]
        private void OnDiscordGatewayReady(GatewayReadyEvent ready)
        {
            DiscordApplication app = Client.Bot.Application;
            
            foreach (CommandSettings command in _pluginConfig.CommandMessages)
            {
                CreateApplicationCommand(command);
            }
            
            foreach (KeyValuePair<string, Snowflake> command in _pluginData.RegisteredCommands.ToList())
            {
                if (_pluginConfig.CommandMessages.All(c => c.Command != command.Key))
                {
                    if (command.Value.IsValid())
                    {
                        app.GetGlobalCommand(Client, command.Value).Then(oldCommand => oldCommand.Delete(Client).Then(() =>
                        {
                            _pluginData.RegisteredCommands.Remove(command);
                            SaveData();
                        }).Catch<ResponseError>(error =>
                        {
                            if (error.DiscordError?.Code == 10063)
                            {
                                _pluginData.RegisteredCommands.Remove(command);
                                SaveData();
                                error.SuppressErrorMessage();
                            }
                        }));
                    }
                }
            }
            
            Puts($"{Title} Ready");
        }
        
        public void CreateApplicationCommand(CommandSettings settings)
        {
            string command = settings.Command;
            if (string.IsNullOrEmpty(command))
            {
                return;
            }
            
            ApplicationCommandBuilder builder = new(command, "Shows players currently on the server", ApplicationCommandType.ChatInput);
            builder.AllowInDirectMessages(settings.AllowInDm);
            builder.AddDefaultPermissions(PermissionFlags.None);
            
            CommandCreate cmd = builder.Build();
            DiscordCommandLocalization loc = builder.BuildCommandLocalization();
            
            _commandCache[command] = settings;
            
            _localizations.RegisterCommandLocalizationAsync(this, settings.GetTemplateName(), loc, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0, 0)).Then(_ =>
            {
                _localizations.ApplyCommandLocalizationsAsync(this, cmd, settings.GetTemplateName()).Then(() =>
                {
                    Client.Bot.Application.CreateGlobalCommand(Client, builder.Build()).Then(appCommand =>
                    {
                        _pluginData.RegisteredCommands[command] = appCommand.Id;
                        SaveData();
                    });
                });
            });
            
            _appCommand.AddApplicationCommand(this, Client.Bot.Application.Id, HandleApplicationCommand, command);
        }
        
        [HookMethod(DiscordExtHooks.OnDiscordGuildCreated)]
        private void OnDiscordGuildCreated(DiscordGuild created)
        {
            foreach (PermanentMessageSettings config in _pluginConfig.Permanent)
            {
                if (!config.Enabled || !config.ChannelId.IsValid())
                {
                    continue;
                }
                
                DiscordChannel channel = created.GetChannel(config.ChannelId);
                if (channel == null)
                {
                    PrintWarning($"Failed to find channel ID: {config.ChannelId} in Guild: {created.Name}");
                    continue;
                }
                
                _commandCache[config.GetTemplateName().Name] = config;
                PermanentMessageData existing = _pluginData.GetPermanentMessage(config);
                if (existing != null)
                {
                    channel.GetMessage(Client, existing.MessageId)
                    .Then(message =>
                    {
                        _permanentHandler[message.Id] = new PermanentMessageHandler(Client, new MessageCache(config), config.UpdateRate, message);
                    })
                    .Catch<ResponseError>(error =>
                    {
                        if (error.HttpStatusCode == DiscordHttpStatusCode.NotFound)
                        {
                            CreatePermanentMessage(config, channel);
                            error.SuppressErrorMessage();
                        }
                    });
                }
                else
                {
                    CreatePermanentMessage(config, channel);
                }
            }
        }
        
        private void CreatePermanentMessage(PermanentMessageSettings config, DiscordChannel channel)
        {
            MessageCache cache = new(config);
            
            CreateMessage<MessageCreate>(cache, null, null, create =>
            {
                channel.CreateMessage(Client, create).Then(message =>
                {
                    _pluginData.SetPermanentMessage(config, new PermanentMessageData
                    {
                        MessageId = message.Id
                    });
                    _permanentHandler[message.Id] = new PermanentMessageHandler(Client, cache, config.UpdateRate, message);
                    SaveData();
                });
            });
        }
        
        private void HandleApplicationCommand(DiscordInteraction interaction, InteractionDataParsed parsed)
        {
            MessageCache cache = GetCache(interaction);
            if (cache == null)
            {
                PrintError("Cache is null!!");
                return;
            }
            
            CreateMessage<InteractionCallbackData>(cache, interaction, null, create =>
            {
                interaction.CreateResponse(Client, new InteractionResponse
                {
                    Type = InteractionResponseType.ChannelMessageWithSource,
                    Data = create
                });
            });
        }
        
        [DiscordMessageComponentCommand(BackCommand)]
        private void HandleBackCommand(DiscordInteraction interaction)
        {
            MessageCache cache = GetCache(interaction);
            if (cache == null)
            {
                return;
            }
            
            cache.State.PreviousPage();
            HandleUpdate(interaction, cache);
        }
        
        [DiscordMessageComponentCommand(RefreshCommand)]
        private void HandleRefreshCommand(DiscordInteraction interaction)
        {
            MessageCache cache = GetCache(interaction);
            if (cache == null)
            {
                return;
            }
            
            HandleUpdate(interaction, cache);
        }
        
        [DiscordMessageComponentCommand(ForwardCommand)]
        private void HandleForwardCommand(DiscordInteraction interaction)
        {
            MessageCache cache = GetCache(interaction);
            if (cache == null)
            {
                return;
            }
            
            cache.State.NextPage();
            HandleUpdate(interaction, cache);
        }
        
        [DiscordMessageComponentCommand(ChangeSort)]
        private void HandleChangeSortCommand(DiscordInteraction interaction)
        {
            MessageCache cache = GetCache(interaction);
            if (cache == null)
            {
                return;
            }
            
            cache.State.NextSort();
            HandleUpdate(interaction, cache);
        }
        
        private void HandleUpdate(DiscordInteraction interaction, MessageCache cache)
        {
            CreateMessage<InteractionCallbackData>(cache, interaction, null, create =>
            {
                interaction.CreateResponse(Client, new InteractionResponse
                {
                    Type = InteractionResponseType.UpdateMessage,
                    Data = create
                });
            });
        }
        #endregion

        #region Plugins\DiscordPlayers.Fields.cs
        public DiscordClient Client { get; set; }
        public DiscordPluginPool Pool { get; set; }
        
        private PluginConfig _pluginConfig; //Plugin Config
        private PluginData _pluginData;
        
        private readonly DiscordAppCommand _appCommand = GetLibrary<DiscordAppCommand>();
        private readonly DiscordPlaceholders _placeholders = GetLibrary<DiscordPlaceholders>();
        private readonly DiscordMessageTemplates _templates = GetLibrary<DiscordMessageTemplates>();
        private readonly DiscordEmbedTemplates _embed = GetLibrary<DiscordEmbedTemplates>();
        private readonly DiscordEmbedFieldTemplates _field = GetLibrary<DiscordEmbedFieldTemplates>();
        private readonly DiscordCommandLocalizations _localizations = GetLibrary<DiscordCommandLocalizations>();
        
        private readonly BotConnection _discordSettings = new();
        
        private readonly Hash<Snowflake, MessageCache> _messageCache = new();
        private readonly Hash<string, BaseMessageSettings> _commandCache = new();
        private readonly OnlinePlayerCache _playerCache = new();
        
        private readonly Hash<Snowflake, PermanentMessageHandler> _permanentHandler = new();
        
        private const string BaseCommand = nameof(DiscordPlayers) + ".";
        private const string BackCommand = BaseCommand + "B";
        private const string RefreshCommand = BaseCommand + "R";
        private const string ForwardCommand = BaseCommand + "F";
        private const string ChangeSort = BaseCommand + "S";
        
        public static DiscordPlayers Instance;
        
        public PluginTimers Timer => timer;
        #endregion

        #region Plugins\DiscordPlayers.Helpers.cs
        private const string PluginIcon = "https://assets.umod.org/images/icons/plugin/61354f8bd5faf.png";
        
        public MessageCache GetCache(DiscordInteraction interaction)
        {
            DiscordMessage message = interaction.Message;
            BaseMessageSettings command;
            if (interaction.Type == InteractionType.ApplicationCommand)
            {
                InteractionDataParsed args = interaction.Parsed;
                command = _commandCache[args.Command];
                return command != null ? new MessageCache(command) : null;
            }
            
            string customId = interaction.Data.CustomId;
            MessageCache cache = _messageCache[message.Id];
            if (cache != null)
            {
                return cache;
            }
            
            ReadOnlySpan<char> base64 = customId.AsSpan()[(customId.LastIndexOf(" ", StringComparison.Ordinal) + 1)..];
            MessageState state = MessageState.Create(base64);
            if (state == null)
            {
                SendResponse(interaction, TemplateKeys.Errors.UnknownState, GetDefault(interaction));
                return null;
            }
            
            command = _commandCache[state.Command];
            if (command == null)
            {
                SendResponse(interaction, TemplateKeys.Errors.UnknownCommand, GetDefault(interaction).Add(PlaceholderDataKeys.CommandName, state.Command));
                return null;
            }
            
            cache = new MessageCache(command, state);
            _messageCache[message.Id] = cache;
            return cache;
        }
        
        public void SendResponse(DiscordInteraction interaction, TemplateKey templateName, PlaceholderData data, MessageFlags flags = MessageFlags.Ephemeral)
        {
            interaction.CreateTemplateResponse(Client, InteractionResponseType.ChannelMessageWithSource, templateName, new InteractionCallbackData { Flags = flags }, data);
        }
        
        public string Lang(string key)
        {
            return lang.GetMessage(key, this);
        }
        
        public string Lang(string key, params object[] args)
        {
            try
            {
                return string.Format(Lang(key), args);
            }
            catch(Exception ex)
            {
                PrintError($"Lang Key '{key}' threw exception\n:{ex}");
                throw;
            }
        }
        
        private void SaveData()
        {
            if (_pluginData != null)
            {
                Interface.Oxide.DataFileSystem.WriteObject(Name, _pluginData);
            }
        }
        
        public new void PrintError(string format, params object[] args) => base.PrintError(format, args);
        #endregion

        #region Plugins\DiscordPlayers.Lang.cs
        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                [LangKeys.SortByEnumName] = "Name",
                [LangKeys.SortByEnumTime] = "Time",
            }, this);
        }
        #endregion

        #region Plugins\DiscordPlayers.MessageHandling.cs
        public void CreateMessage<T>(MessageCache cache, DiscordInteraction interaction, T create, Action<T> callback) where T : class, IDiscordMessageTemplate, new()
        {
            List<IPlayer> allList = GetPlayerList(cache);
            int perPage = cache.Settings.EmbedFieldLimit;
            List<IPlayer> pageList = allList.Skip(cache.State.Page * perPage).Take(perPage).ToPooledList(Pool);
            
            int maxPage = (allList.Count - 1) / cache.Settings.EmbedFieldLimit;
            cache.State.ClampPage((short)maxPage);
            
            PlaceholderData data = GetDefault(cache, interaction, maxPage + 1);
            data.ManualPool();
            
            T message = CreateMessage(cache.Settings, data, interaction, create);
            message.AllowedMentions = AllowedMentions.None;
            SetButtonState(message, BackCommand, cache.State.Page > 0);
            SetButtonState(message, ForwardCommand, cache.State.Page < maxPage);
            
            DiscordEmbed embed = CreateEmbeds(cache.Settings, data, interaction);
            
            message.Embeds = new List<DiscordEmbed>{embed};
            CreateFields(cache, data, interaction, pageList).Then(fields =>
            {
                ProcessEmbeds(embed, fields);
            }).Finally(() =>
            {
                callback.Invoke(message);
                data.Dispose();
                Pool.FreeList(pageList);
            });
        }
        
        public List<IPlayer> GetPlayerList(MessageCache cache)
        {
            return _playerCache.GetList(cache.State.Sort, cache.Settings.ShowAdmins);
        }
        
        public T CreateMessage<T>(BaseMessageSettings settings, PlaceholderData data, DiscordInteraction interaction, T message) where T : class, IDiscordMessageTemplate, new()
        {
            if (settings.IsPermanent())
            {
                return _templates.GetGlobalTemplate(this, settings.GetTemplateName()).ToMessage(data, message);
            }
            
            return _templates.GetLocalizedTemplate(this, settings.GetTemplateName(), interaction).ToMessage(data, message);
        }
        
        public void SetButtonState(IDiscordMessageTemplate message, string command, bool enabled)
        {
            for (int index = 0; index < message.Components.Count; index++)
            {
                ActionRowComponent row = message.Components[index];
                for (int i = 0; i < row.Components.Count; i++)
                {
                    BaseComponent component = row.Components[i];
                    if (component is ButtonComponent)
                    {
                        ButtonComponent button = (ButtonComponent)component;
                        if (button.CustomId.StartsWith(command))
                        {
                            button.Disabled = !enabled;
                            return;
                        }
                    }
                }
            }
        }
        
        public DiscordEmbed CreateEmbeds(BaseMessageSettings settings, PlaceholderData data, DiscordInteraction interaction)
        {
            TemplateKey name = settings.GetTemplateName();
            return settings.IsPermanent() ? _embed.GetGlobalTemplate(this, name).ToEntity(data) : _embed.GetLocalizedTemplate(this, name, interaction).ToEntity(data);
        }
        
        public IPromise<List<EmbedField>> CreateFields(MessageCache cache, PlaceholderData data, DiscordInteraction interaction, List<IPlayer> onlineList)
        {
            DiscordEmbedFieldTemplate template;
            if (cache.Settings.IsPermanent())
            {
                template = _field.GetGlobalTemplate(this, cache.Settings.GetTemplateName());
            }
            else
            {
                template = _field.GetLocalizedTemplate(this, cache.Settings.GetTemplateName(), interaction);
            }
            
            List<PlaceholderData> placeholders = new(onlineList.Count);
            
            for (int index = 0; index < onlineList.Count; index++)
            {
                PlaceholderData playerData = CloneForPlayer(data, onlineList[index], cache.State.Page * cache.Settings.EmbedFieldLimit + index + 1);
                playerData.ManualPool();
                placeholders.Add(playerData);
            }
            
            return template.ToEntityBulk(placeholders).Finally(() =>
            {
                foreach (PlaceholderData data in placeholders)
                {
                    data.Dispose();
                }
            });
        }
        
        public void ProcessEmbeds(DiscordEmbed embed, List<EmbedField> fields)
        {
            embed.Fields ??= new List<EmbedField>();
            embed.Fields.AddRange(fields);
        }
        #endregion

        #region Plugins\DiscordPlayers.Placeholders.cs
        public void RegisterPlaceholders()
        {
            _placeholders.RegisterPlaceholder<int>(this,PlaceholderKeys.PlayerIndex, PlaceholderDataKeys.PlayerIndex);
            _placeholders.RegisterPlaceholder<MessageState, int>(this, PlaceholderKeys.Page, PlaceholderDataKeys.MessageState, GetPage);
            _placeholders.RegisterPlaceholder<MessageState, string>(this, PlaceholderKeys.SortState, PlaceholderDataKeys.MessageState, GetSort);
            _placeholders.RegisterPlaceholder<string>(this, PlaceholderKeys.CommandId, PlaceholderDataKeys.CommandId);
            _placeholders.RegisterPlaceholder<string>(this,PlaceholderKeys.CommandName, PlaceholderDataKeys.CommandName);
            _placeholders.RegisterPlaceholder<int>(this, PlaceholderKeys.MaxPage, PlaceholderDataKeys.MaxPage);
        }
        
        public int GetPage(MessageState embed) => embed.Page + 1;
        public string GetSort(PlaceholderState state, MessageState embed)
        {
            DiscordInteraction interaction = state.Data.Get<DiscordInteraction>();
            string key = embed.Sort == SortBy.Name ? LangKeys.SortByEnumName : LangKeys.SortByEnumTime;
            return interaction != null ? interaction.GetLangMessage(this, key) : Lang(key);
        }
        
        public PlaceholderData CloneForPlayer(PlaceholderData source, IPlayer player, int index)
        {
            DiscordUser user = player.GetDiscordUser();
            TimeSpan onlineDuration = _playerCache.GetOnlineDuration(player);
            return source.Clone()
            .RemoveUser()
            .AddUser(user)
            .AddPlayer(player)
            .Add(PlaceholderDataKeys.PlayerIndex, index)
            .Add(PlaceholderDataKeys.PlayerDuration, onlineDuration)
            .AddTimestamp(DateTimeOffset.UtcNow - onlineDuration);
        }
        
        public PlaceholderData GetDefault(DiscordInteraction interaction)
        {
            return _placeholders.CreateData(this).AddInteraction(interaction);
        }
        
        public PlaceholderData GetDefault(MessageCache cache, DiscordInteraction interaction)
        {
            return GetDefault(interaction)
            .Add(PlaceholderDataKeys.MessageState, cache.State);
        }
        
        public PlaceholderData GetDefault(MessageCache cache, DiscordInteraction interaction, int maxPage)
        {
            return GetDefault(cache, interaction)
            .Add(PlaceholderDataKeys.MaxPage, maxPage)
            .Add(PlaceholderDataKeys.CommandId, cache.State.CreateBase64String());
        }
        #endregion

        #region Plugins\DiscordPlayers.Setup.cs
        private void Init()
        {
            Instance = this;
            _discordSettings.ApiToken = _pluginConfig.DiscordApiKey;
            _discordSettings.LogLevel = _pluginConfig.ExtensionDebugging;
            _discordSettings.Intents = GatewayIntents.Guilds;
            
            _pluginData = Interface.Oxide.DataFileSystem.ReadObject<PluginData>(Name) ?? new PluginData();
        }
        
        private void OnServerInitialized()
        {
            if (string.IsNullOrEmpty(_pluginConfig.DiscordApiKey))
            {
                PrintWarning("Please set the Discord Bot Token and reload the plugin");
                return;
            }
            
            _playerCache.Initialize(players.Connected);
            
            RegisterPlaceholders();
            RegisterTemplates();
            
            foreach (CommandSettings message in _pluginConfig.CommandMessages)
            {
                if (message.EmbedFieldLimit > 25)
                {
                    PrintWarning($"Players For Embed cannot be greater than 25 for command {message.Command}");
                }
                else if (message.EmbedFieldLimit < 0)
                {
                    PrintWarning($"Players For Embed cannot be less than 0 for command {message.Command}");
                }
                
                message.EmbedFieldLimit = Mathf.Clamp(message.EmbedFieldLimit, 0, 25);
            }
            
            Client.Connect(_discordSettings);
        }
        
        private void OnUserConnected(IPlayer player)
        {
            _playerCache.OnUserConnected(player);
        }
        
        private void OnUserDisconnected(IPlayer player)
        {
            _playerCache.OnUserDisconnected(player);
        }
        
        private void Unload()
        {
            SaveData();
            Instance = null;
        }
        #endregion

        #region Plugins\DiscordPlayers.Templates.cs
        public void RegisterTemplates()
        {
            foreach (CommandSettings command in _pluginConfig.CommandMessages)
            {
                DiscordEmbedFieldTemplate embed = command.Command == "playersadmin" ? GetDefaultAdminFieldTemplate() : GetDefaultFieldTemplate();
                CreateCommandTemplates(command, embed, false);
            }
            
            foreach (PermanentMessageSettings permanent in _pluginConfig.Permanent)
            {
                DiscordEmbedFieldTemplate embed = permanent.TemplateName == "PermanentAdmin" ? GetDefaultAdminFieldTemplate() : GetDefaultFieldTemplate();
                CreateCommandTemplates(permanent, embed, true);
            }
            
            DiscordMessageTemplate unknownState = CreateTemplateEmbed("Error: Failed to find a state for this message. Please create a new message.", DiscordColor.Danger);
            _templates.RegisterLocalizedTemplateAsync(this, TemplateKeys.Errors.UnknownState, unknownState, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0, 0));
            
            DiscordMessageTemplate unknownCommand = CreateTemplateEmbed($"Error: Command not found '{PlaceholderKeys.CommandName}'. Please create a new message", DiscordColor.Danger);
            _templates.RegisterLocalizedTemplateAsync(this, TemplateKeys.Errors.UnknownCommand, unknownCommand, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0, 0));
        }
        
        private void CreateCommandTemplates(BaseMessageSettings command, DiscordEmbedFieldTemplate @default, bool isGlobal)
        {
            DiscordMessageTemplate template = CreateBaseMessage();
            TemplateKey name = command.GetTemplateName();
            RegisterTemplate(_templates, name, template, isGlobal, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0, 0));
            RegisterTemplate(_field, name, @default, isGlobal, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0, 0));
            
            DiscordEmbedTemplate embed = GetDefaultEmbedTemplate();
            RegisterTemplate(_embed, name, embed, isGlobal, new TemplateVersion(1, 0, 0), new TemplateVersion(1, 0, 0));
        }
        
        public void RegisterTemplate<TTemplate>(BaseMessageTemplateLibrary<TTemplate> library, TemplateKey name, TTemplate template, bool isGlobal, TemplateVersion version, TemplateVersion minVersion) where TTemplate : class, new()
        {
            if (isGlobal)
            {
                library.RegisterGlobalTemplateAsync(this, name, template, version, minVersion);
            }
            else
            {
                library.RegisterLocalizedTemplateAsync(this, name, template, version, minVersion);
            }
        }
        
        public DiscordMessageTemplate CreateBaseMessage()
        {
            return new DiscordMessageTemplate
            {
                Content = string.Empty,
                Components =
                {
                    new ButtonTemplate("Back", ButtonStyle.Primary, $"{BackCommand} {PlaceholderKeys.CommandId}", "⬅"),
                    new ButtonTemplate($"Page: {PlaceholderKeys.Page}/{PlaceholderKeys.MaxPage}", ButtonStyle.Primary, "PAGE", false),
                    new ButtonTemplate("Next", ButtonStyle.Primary, $"{ForwardCommand} {PlaceholderKeys.CommandId}", "➡"),
                    new ButtonTemplate("Refresh", ButtonStyle.Primary, $"{RefreshCommand} {PlaceholderKeys.CommandId}", "🔄"),
                    new ButtonTemplate($"Sorted By: {PlaceholderKeys.SortState}", ButtonStyle.Primary, $"{ChangeSort} {PlaceholderKeys.CommandId}")
                }
            };
        }
        
        public DiscordEmbedTemplate GetDefaultEmbedTemplate()
        {
            return new DiscordEmbedTemplate
            {
                Title = $"{DefaultKeys.Server.Name}",
                Description = $"{DefaultKeys.Server.Players}/{DefaultKeys.Server.MaxPlayers} Online Players | {{server.players.loading}} Loading | {{server.players.queued}} Queued",
                Color = DiscordColor.Blurple.ToHex(),
                TimeStamp = true,
                Footer =
                {
                    Enabled = true,
                    Text = $"{DefaultKeys.Plugin.Name} V{DefaultKeys.Plugin.Version} by {DefaultKeys.Plugin.Author}",
                    IconUrl = PluginIcon
                }
            };
        }
        
        public DiscordEmbedFieldTemplate GetDefaultFieldTemplate()
        {
            return new DiscordEmbedFieldTemplate($"#{PlaceholderKeys.PlayerIndex} {DefaultKeys.Player.NameClan}", $"**Connected:** {DefaultKeys.Timespan.Hours}h {DefaultKeys.Timespan.Minutes}m {DefaultKeys.Timespan.Seconds}s");
        }
        
        public DiscordEmbedFieldTemplate GetDefaultAdminFieldTemplate()
        {
            return new DiscordEmbedFieldTemplate($"#{PlaceholderKeys.PlayerIndex} {DefaultKeys.Player.NameClan}",
            $"**Steam ID:**{DefaultKeys.Player.Id}\n" +
            $"**Connected:** {DefaultKeys.Timespan.Hours}h {DefaultKeys.Timespan.Minutes}m {DefaultKeys.Timespan.Seconds}s\n" +
            $"**Ping:** {DefaultKeys.Player.Ping}ms\n" +
            $"**Country:** {DefaultKeys.Player.Country}\n" +
            $"**User:** {DefaultKeys.User.Mention}");
        }
        
        public DiscordMessageTemplate CreateTemplateEmbed(string description, DiscordColor color)
        {
            return new DiscordMessageTemplate
            {
                Embeds = new List<DiscordEmbedTemplate>
                {
                    new()
                    {
                        Description = $"[{DefaultKeys.Plugin.Title}] {description}",
                        Color = color.ToHex()
                    }
                }
            };
        }
        #endregion

        #region Cache\MessageCache.cs
        public class MessageCache
        {
            public readonly BaseMessageSettings Settings;
            public readonly MessageState State;
            
            public MessageCache(BaseMessageSettings settings, MessageState state = null)
            {
                Settings = settings;
                State = state ?? MessageState.CreateNew(Settings.GetTemplateName());
            }
        }
        #endregion

        #region Cache\OnlinePlayerCache.cs
        public class OnlinePlayerCache
        {
            private readonly PlayerListCache _byNameCache = new(new NameComparer());
            private readonly PlayerListCache _byOnlineTime;
            private readonly Hash<string, DateTime> _onlineSince = new();
            
            public OnlinePlayerCache()
            {
                _byOnlineTime = new PlayerListCache(new OnlineSinceComparer(_onlineSince));
            }
            
            public void Initialize(IEnumerable<IPlayer> connected)
            {
                #if RUST
                foreach (Network.Connection connection in Network.Net.sv.connections)
                {
                    _onlineSince[connection.ownerid.ToString()] = DateTime.UtcNow - TimeSpan.FromSeconds(connection.GetSecondsConnected());
                }
                #endif
                
                foreach (IPlayer player in connected)
                {
                    OnUserConnected(player);
                }
            }
            
            public TimeSpan GetOnlineDuration(IPlayer player)
            {
                return DateTime.UtcNow - _onlineSince[player.Id];
            }
            
            public List<IPlayer> GetList(SortBy sort, bool includeAdmin)
            {
                List<IPlayer> list = sort == SortBy.Time ? _byOnlineTime.GetList(includeAdmin) : _byNameCache.GetList(includeAdmin);
                //return Enumerable.Range(0, 100).Select(i => list[0]).ToList();
                return list;
            }
            
            public void OnUserConnected(IPlayer player)
            {
                _onlineSince.TryAdd(player.Id, DateTime.UtcNow);
                _byNameCache.Add(player);
                _byOnlineTime.Add(player);
            }
            
            public void OnUserDisconnected(IPlayer player)
            {
                _onlineSince.Remove(player.Id);
                _byNameCache.Remove(player);
                _byOnlineTime.Remove(player);
            }
            
            class NameComparer : IComparer<IPlayer>
            {
                public int Compare(IPlayer x, IPlayer y)
                {
                    return string.Compare(x?.Name, y?.Name, StringComparison.Ordinal);
                }
            }
            
            class OnlineSinceComparer : IComparer<IPlayer>
            {
                private readonly Hash<string, DateTime> _onlineSince;
                
                public OnlineSinceComparer(Hash<string, DateTime> onlineSince)
                {
                    _onlineSince = onlineSince;
                }
                
                public int Compare(IPlayer x, IPlayer y)
                {
                    return _onlineSince[x.Id].CompareTo(_onlineSince[y.Id]);
                }
            }
        }
        #endregion

        #region Cache\PlayerListCache.cs
        public class PlayerListCache
        {
            private readonly List<IPlayer> _allList = new();
            private readonly List<IPlayer> _nonAdminList = new();
            
            private readonly IComparer<IPlayer> _comparer;
            
            public PlayerListCache(IComparer<IPlayer> comparer)
            {
                _comparer = comparer;
            }
            
            public void Add(IPlayer player)
            {
                Remove(player);
                Insert(_allList, player);
                Insert(_nonAdminList, player);
            }
            
            public void Insert(List<IPlayer> list, IPlayer player)
            {
                int index = list.BinarySearch(player, _comparer);
                if (index < 0)
                {
                    list.Insert(~index, player);
                }
                else
                {
                    list[index] = player;
                }
            }
            
            public void Remove(IPlayer player)
            {
                _allList.Remove(player);
                _nonAdminList.Remove(player);
            }
            
            public List<IPlayer> GetList(bool includeAdmin)
            {
                return includeAdmin ? _allList : _nonAdminList;
            }
        }
        #endregion

        #region Configuration\BaseMessageSettings.cs
        public abstract class BaseMessageSettings
        {
            [JsonProperty(PropertyName = "Display Admins In The Player List", Order = 1001)]
            public bool ShowAdmins { get; set; }
            
            [DefaultValue(25)]
            [JsonProperty(PropertyName = "Players Per Embed (0 - 25)", Order = 1002)]
            public int EmbedFieldLimit { get; set; }
            
            public abstract bool IsPermanent();
            public abstract TemplateKey GetTemplateName();
            
            [JsonConstructor]
            public BaseMessageSettings() { }
            
            public BaseMessageSettings(BaseMessageSettings settings)
            {
                ShowAdmins = settings?.ShowAdmins ?? true;
                EmbedFieldLimit = settings?.EmbedFieldLimit ?? 25;
            }
        }
        #endregion

        #region Configuration\CommandSettings.cs
        public class CommandSettings : BaseMessageSettings
        {
            [JsonProperty(PropertyName = "Command Name (Must Be Unique)")]
            public string Command { get; set; }
            
            [JsonProperty(PropertyName = "Allow Command In Direct Messages")]
            public bool AllowInDm { get; set; }
            
            [JsonConstructor]
            public CommandSettings() { }
            
            public CommandSettings(CommandSettings settings) : base(settings)
            {
                Command = settings?.Command ?? "players";
                AllowInDm = settings?.AllowInDm ?? true;
            }
            
            public override bool IsPermanent() => false;
            public override TemplateKey GetTemplateName() => new(Command);
        }
        #endregion

        #region Configuration\PermanentMessageSettings.cs
        public class PermanentMessageSettings : BaseMessageSettings
        {
            [JsonProperty(PropertyName = "Enabled")]
            public bool Enabled { get; set; }
            
            [JsonProperty(PropertyName = "Template Name (Must Be Unique)")]
            public string TemplateName { get; set; }
            
            [JsonProperty(PropertyName = "Permanent Message Channel ID")]
            public Snowflake ChannelId { get; set; }
            
            [JsonProperty(PropertyName = "Update Rate (Minutes)")]
            public float UpdateRate { get; set; }
            
            [JsonConstructor]
            public PermanentMessageSettings() { }
            
            public PermanentMessageSettings(PermanentMessageSettings settings) : base(settings)
            {
                Enabled = settings?.Enabled ?? false;
                TemplateName = settings?.TemplateName ?? "Permanent";
                ChannelId = settings?.ChannelId ?? default(Snowflake);
                UpdateRate = settings?.UpdateRate ?? 1f;
            }
            
            public override bool IsPermanent() => true;
            public override TemplateKey GetTemplateName() => new(TemplateName);
        }
        #endregion

        #region Configuration\PluginConfig.cs
        public class PluginConfig
        {
            [DefaultValue("")]
            [JsonProperty(PropertyName = "Discord Bot Token")]
            public string DiscordApiKey { get; set; }
            
            [JsonProperty(PropertyName = "Command Messages")]
            public List<CommandSettings> CommandMessages { get; set; }
            
            [JsonProperty(PropertyName = "Permanent Messages")]
            public List<PermanentMessageSettings> Permanent { get; set; }
            
            [JsonConverter(typeof(StringEnumConverter))]
            [DefaultValue(DiscordLogLevel.Info)]
            [JsonProperty(PropertyName = "Discord Extension Log Level (Verbose, Debug, Info, Warning, Error, Exception, Off)")]
            public DiscordLogLevel ExtensionDebugging { get; set; }
        }
        #endregion

        #region Data\PermanentMessageData.cs
        public class PermanentMessageData
        {
            public Snowflake MessageId { get; set; }
        }
        #endregion

        #region Data\PluginData.cs
        public class PluginData
        {
            public Hash<string, PermanentMessageData> PermanentMessageIds = new();
            public Hash<string, Snowflake> RegisteredCommands = new();
            
            public PermanentMessageData GetPermanentMessage(PermanentMessageSettings config)
            {
                return PermanentMessageIds[config.TemplateName];
            }
            
            public void SetPermanentMessage(PermanentMessageSettings config, PermanentMessageData data)
            {
                PermanentMessageIds[config.TemplateName] = data;
            }
        }
        #endregion

        #region Enums\SortBy.cs
        public enum SortBy : byte
        {
            Name,
            Time
        }
        #endregion

        #region Handlers\PermanentMessageHandler.cs
        public class PermanentMessageHandler
        {
            private readonly DiscordClient _client;
            private readonly MessageCache _cache;
            private readonly DiscordMessage _message;
            private readonly MessageUpdate _update = new();
            private readonly Timer _timer;
            private DateTime _lastUpdate;
            
            public PermanentMessageHandler(DiscordClient client, MessageCache cache, float updateRate, DiscordMessage message)
            {
                _client = client;
                _cache = cache;
                _message = message;
                _timer = DiscordPlayers.Instance.Timer.Every(updateRate * 60f, SendUpdate);
                SendUpdate();
            }
            
            private void SendUpdate()
            {
                if (_lastUpdate + TimeSpan.FromSeconds(5) > DateTime.UtcNow)
                {
                    return;
                }
                
                _lastUpdate = DateTime.UtcNow;
                
                DiscordPlayers.Instance.CreateMessage(_cache, null, _update, message =>
                {
                    _message.Edit(_client, message).Catch<ResponseError>(error =>
                    {
                        if (error.HttpStatusCode == DiscordHttpStatusCode.NotFound)
                        {
                            _timer?.Destroy();
                        }
                    });
                });
            }
        }
        #endregion

        #region Lang\LangKeys.cs
        public static class LangKeys
        {
            public const string SortByEnumName = nameof(SortByEnumName);
            public const string SortByEnumTime = nameof(SortByEnumTime);
        }
        #endregion

        #region Placeholders\PlaceholderDataKeys.cs
        public static class PlaceholderDataKeys
        {
            public static readonly PlaceholderDataKey CommandId = new("command.id");
            public static readonly PlaceholderDataKey CommandName = new("command.name");
            public static readonly PlaceholderDataKey PlayerIndex = new("player.index");
            public static readonly PlaceholderDataKey PlayerDuration = new("timespan");
            public static readonly PlaceholderDataKey MaxPage = new("page.max");
            public static readonly PlaceholderDataKey MessageState = new("message.state");
        }
        #endregion

        #region Placeholders\PlaceholderKeys.cs
        public class PlaceholderKeys
        {
            public static readonly PlaceholderKey PlayerIndex = new(nameof(DiscordPlayers), "player.index");
            public static readonly PlaceholderKey Page = new(nameof(DiscordPlayers), "state.page");
            public static readonly PlaceholderKey SortState = new(nameof(DiscordPlayers), "state.sort");
            public static readonly PlaceholderKey CommandId = new(nameof(DiscordPlayers), "command.id");
            public static readonly PlaceholderKey CommandName = new(nameof(DiscordPlayers), "command.name");
            public static readonly PlaceholderKey MaxPage = new(nameof(DiscordPlayers), "page.max");
        }
        #endregion

        #region State\MessageState.cs
        [ProtoContract]
        public class MessageState
        {
            [ProtoMember(1)]
            public short Page;
            
            [ProtoMember(2)]
            public SortBy Sort;
            
            [ProtoMember(3)]
            public string Command;
            
            private MessageState() { }
            
            public static MessageState CreateNew(TemplateKey command)
            {
                return new MessageState
                {
                    Command = command.Name
                };
            }
            
            public static MessageState Create(ReadOnlySpan<char> base64)
            {
                try
                {
                    Span<byte> buffer = stackalloc byte[64];
                    Convert.TryFromBase64Chars(base64, buffer, out int written);
                    MemoryStream stream = DiscordPlayers.Instance.Pool.GetMemoryStream();
                    stream.Write(buffer[..written]);
                    stream.Flush();
                    stream.Position = 0;
                    MessageState state = Serializer.Deserialize<MessageState>(stream);
                    DiscordPlayers.Instance.Pool.FreeMemoryStream(stream);
                    return state;
                }
                catch (Exception ex)
                {
                    DiscordPlayers.Instance.PrintError($"An error occured parsing state. State: {base64.ToString()}. Exception:\n{ex}");
                    return null;
                }
            }
            
            public string CreateBase64String()
            {
                MemoryStream stream = DiscordPlayers.Instance.Pool.GetMemoryStream();
                Serializer.Serialize(stream, this);
                stream.TryGetBuffer(out ArraySegment<byte> buffer);
                string base64 = Convert.ToBase64String(buffer.AsSpan());
                DiscordPlayers.Instance.Pool.FreeMemoryStream(stream);
                return base64;
            }
            
            public void NextPage() => Page++;
            
            public void PreviousPage() => Page--;
            
            public void ClampPage(short maxPage) => Page = Page.Clamp((short)0, maxPage);
            
            public void NextSort() => Sort = EnumCache<SortBy>.Instance.Next(Sort);
            
            public override string ToString()
            {
                return $"{{ Command = '{Command}' Sort = {Sort.ToString()} Page = {Page} }}";
            }
        }
        #endregion

        #region Templates\TemplateKeys.cs
        public static class TemplateKeys
        {
            public static class Errors
            {
                private const string Base = nameof(Errors) + ".";
                
                public static readonly TemplateKey UnknownState = new(Base + nameof(UnknownState));
                public static readonly TemplateKey UnknownCommand = new(Base + nameof(UnknownCommand));
            }
        }
        #endregion

    }

}
