/// <summary>
        /// Упоминание в тексте целой роли с сервера
        /// </summary>
        /// <param name="text">Текст с сырым Markdown</param>
        /// <param name="chatOption">Опции чата, заданные стримером для виджета</param>
        /// <param name="waitDictionary">Dictionary для саб-блоков</param>
        /// <param name="guild">Гильдия (сервер), внутри которого написано сообщение</param>
        /// <returns></returns>
        public static string MarkMentionsRole(
            string text,
            ChatDrawOption chatOption,
            Dictionary <string, string> waitDictionary,
            EventGuildCreate guild
            )
        {
            text = rMentionRole.Replace(text, m1 =>
            {
                string roleID = m1.Groups[1].Value;

                //
                Role role = guild.roles.FirstOrDefault(t => t.id == roleID);
                if (role == null)
                {
                    return(string.Format("<Role Unknown #{0}>", roleID));
                }

                string nickColor = role.color.ToString("X");
                nickColor        = "#" + nickColor.PadLeft(6, '0');
                var wait         = GetWaitString();

                waitDictionary[wait] = string.Format("<span class='role mention' style='color: {1};'>@{0}</span>",
                                                     HttpUtility.HtmlEncode(role.name),
                                                     nickColor
                                                     );

                return(wait);
            });

            return(text);
        }
        /// <summary>
        /// Спойлеры
        /// </summary>
        /// <param name="text">Текст с сырым Markdown</param>
        /// <param name="chatOption">Опции чата, заданные стримером для виджета</param>
        /// <param name="waitDictionary">Dictionary для саб-блоков</param>
        /// <param name="guild">Гильдия (сервер), внутри которого написано сообщение</param>
        /// <param name="mentions">Список упоминаний, сделанных в сообщении</param>
        /// <param name="usedEmbedsUrls">Список использованных Url'ов в embed</param>
        /// <returns></returns>
        public static string MarkSpoilers(
            string text,
            ChatDrawOption chatOption,
            Dictionary <string, string> waitDictionary,
            EventGuildCreate guild,
            List <EventMessageCreate.EventMessageCreate_Mention> mentions,
            HashSet <string> usedEmbedsUrls
            )
        {
            text = rSpoilerMark.Replace(text, m1 =>
            {
                var subBlock = RenderMarkdownBlockAsHTMLInnerBlock(
                    m1.Groups[1].Value,
                    chatOption,
                    waitDictionary,
                    guild,
                    mentions,
                    usedEmbedsUrls
                    );

                var wait             = GetWaitString();
                waitDictionary[wait] = string.Format(
                    "<span class='spoiler {1}'><span class='spoiler-content'>{0}</span></span>",
                    subBlock,
                    (chatOption.text_spoiler == 1) ? "spoiler-show" : ""
                    );

                return(wait);
            });

            return(text);
        }
        /// <summary>
        /// Жирный наклонный текст
        /// </summary>
        /// <param name="text">Текст с сырым Markdown</param>
        /// <param name="chatOption">Опции чата, заданные стримером для виджета</param>
        /// <param name="waitDictionary">Dictionary для саб-блоков</param>
        /// <param name="guild">Гильдия (сервер), внутри которого написано сообщение</param>
        /// <param name="mentions">Список упоминаний, сделанных в сообщении</param>
        /// <param name="usedEmbedsUrls">Список использованных Url'ов в embed</param>
        /// <returns></returns>
        public static string MarkBoldItalic(
            string text,
            ChatDrawOption chatOption,
            Dictionary <string, string> waitDictionary,
            EventGuildCreate guild,
            List <EventMessageCreate.EventMessageCreate_Mention> mentions,
            HashSet <string> usedEmbedsUrls
            )
        {
            text = rBoldItalic.Replace(text, m1 =>
            {
                var subBlock = RenderMarkdownBlockAsHTMLInnerBlock(
                    m1.Groups[1].Value,
                    chatOption,
                    waitDictionary,
                    guild,
                    mentions,
                    usedEmbedsUrls
                    );

                var wait             = GetWaitString();
                waitDictionary[wait] = string.Format("<strong><em>{0}</em></strong>", subBlock);
                return(wait);
            });

            return(text);
        }
        /// <summary>
        /// Получаем сырой текст с маркдауном и превращаем его в HTML-код
        /// </summary>
        /// <param name="text">Многострочный сырой текст с маркдауном</param>
        /// <param name="chatOption"></param>
        /// <param name="mentions"></param>
        /// <param name="guildID"></param>
        /// <param name="usedEmbedsUrls"></param>
        /// <returns>HTML-код, который можно безопасно рендерить в чате</returns>
        public static string RenderMarkdownAsHTML(
            string text,
            ChatDrawOption chatOption,
            List <EventMessageCreate.EventMessageCreate_Mention> mentions,
            string guildID,
            HashSet <string> usedEmbedsUrls
            )
        {
            // ReSharper disable once UseNullPropagation
            if (text == null)
            {
                return(null);
            }

            // hint: Цитаты в Дискорде ТОЛЬКО одноуровневые, поэтому парсер цитат нерекурсивный
            string result           = "";
            bool   isInQuote        = false;
            var    currentQuoteHTML = "";

            foreach (var line in text.Split("\n"))
            {
                var trimmedLine = line.TrimEnd('\r');
                if ((trimmedLine.Length >= 2) && (trimmedLine.Substring(0, 2) == "> "))
                {
                    // Это кусок цитаты
                    currentQuoteHTML += string.Format("<div class='line'>{0}</div>",
                                                      RenderLineAsHTML(trimmedLine.Substring(2), chatOption, mentions, guildID, usedEmbedsUrls));
                    isInQuote = true;
                }
                else
                {
                    if (isInQuote)
                    {
                        // hint: цитата может быть пустой, но это всё равно цитата, поэтому тут не просто
                        // проверка на currentQuoteHTML != ""
                        result += string.Format(
                            "<div class='quote-block'><div class='quote-border'></div><div class='quote-content'>{0}</div></div>",
                            currentQuoteHTML
                            );
                        currentQuoteHTML = "";
                        isInQuote        = false;
                    }

                    result += string.Format("<div class='line'>{0}</div>",
                                            RenderLineAsHTML(trimmedLine, chatOption, mentions, guildID, usedEmbedsUrls));
                }
            }

            if (isInQuote)
            {
                // Текст заканчивается цитатой
                result += string.Format(
                    "<div class='quote-block'><div class='quote-border'></div><div class='quote-content'>{0}</div></div>",
                    currentQuoteHTML
                    );
            }

            return(result);
        }
        public void MainTest(
            string rawMarkdown,
            string expectedHtml,
            ChatDrawOption chatOption,
            List<EventMessageCreate.EventMessageCreate_Mention> mentions,
            IEnumerable<string> usedEmbedUrl
        )
        {
            if (chatOption == null)
            {
                chatOption = new ChatDrawOption();
            }

            var usedEmbedUrlHash = (usedEmbedUrl != null) ? usedEmbedUrl.ToHashSet() : new HashSet<string>();

            var renderedText = MessageMarkdownParser.RenderMarkdownAsHTML(
                rawMarkdown,
                chatOption,
                mentions,
                guildID,
                usedEmbedUrlHash
            );

            IHtmlDocument document;
            {
                var configuration = Configuration.Default;
                var context = BrowsingContext.New(configuration);
                document = (IHtmlDocument) context.OpenAsync(res => res
                    .Content(renderedText)
                    .Address("http://localhost:5050/chat.cgi")).Result;
            }

            var realExpectedHtml = "";
            {
                var r1 = new Regex("<em><strong>(.*?)</strong></em>",
                    RegexOptions.Compiled);
                expectedHtml = r1.Replace(expectedHtml,
                    (m) => m.Groups[1].Value.IndexOf("<", StringComparison.Ordinal) == -1
                        ? string.Format("<strong><em>{0}</em></strong>", m.Groups[1].Value)
                        : m.Groups[0].Value);
                var r2 = new Regex("<u><em>(.*?)</em></u>",
                    RegexOptions.Compiled);
                expectedHtml = r2.Replace(expectedHtml,
                    (m) => m.Groups[1].Value.IndexOf("<", StringComparison.Ordinal) == -1
                        ? string.Format("<em><u>{0}</u></em>", m.Groups[1].Value)
                        : m.Groups[0].Value);

                var configuration = Configuration.Default;
                var context = BrowsingContext.New(configuration);
                var document1 = (IHtmlDocument) context.OpenAsync(res => res
                    .Content(expectedHtml)
                    .Address("http://localhost:5050/chat.cgi")).Result;
                realExpectedHtml = document1.Body.InnerHtml;
            }
            var resultHTML = document.Body.InnerHtml;
            Assert.Equal(realExpectedHtml, resultHTML);
        }
        /// <summary>
        /// Упоминание в сообщении конкретного человека
        /// </summary>
        /// <param name="text">Текст с сырым Markdown</param>
        /// <param name="chatOption">Опции чата, заданные стримером для виджета</param>
        /// <param name="waitDictionary">Dictionary для саб-блоков</param>
        /// <param name="guild">Гильдия (сервер), внутри которого написано сообщение</param>
        /// <param name="mentions">Список упоминаний, сделанных в сообщении</param>
        /// <returns></returns>
        public static string MarkMentionsPeople(
            string text,
            ChatDrawOption chatOption,
            Dictionary <string, string> waitDictionary,
            EventGuildCreate guild,
            List <EventMessageCreate.EventMessageCreate_Mention> mentions
            )
        {
            // Упоминание человека
            text = rMentionNick.Replace(text, m1 =>
            {
                string mentionID = m1.Groups[1].Value;
                if (mentions == null)
                {
                    // Приколы дискорда
                    return(string.Format("<Пользователь Unknown #{0}>", mentionID));
                }

                EventMessageCreate.EventMessageCreate_Mention mention =
                    mentions.FirstOrDefault(eMention => eMention.id == mentionID);

                if (mention == null)
                {
                    return(string.Format("<Пользователь Unknown #{0}>", mentionID));
                }

                // Выбираем наиболее приоритетную роль
                var nickColor = "inherit";
                if ((guild.roles != null) && (mention.member?.roles != null) &&
                    (chatOption.message_mentions_style == 1))
                {
                    var mention_roles_local =
                        guild.roles.Where(t => mention.member.roles.Contains(t.id)).ToList();
                    mention_roles_local.Sort((a, b) => b.position.CompareTo(a.position));
                    var role = mention_roles_local.Any() ? mention_roles_local.First() : null;
                    if (role != null)
                    {
                        nickColor = role.color.ToString("X");
                        nickColor = "#" + nickColor.PadLeft(6, '0');
                    }
                }
                //

                var wait = GetWaitString();

                waitDictionary[wait] = string.Format("<span class='user mention' {1}>@{0}</span>",
                                                     HttpUtility.HtmlEncode(mention.member?.nick ?? mention.username),
                                                     (chatOption.message_mentions_style == 1) ? string.Format(" style='color: {0};'", nickColor) : ""
                                                     );

                return(wait);
            });

            return(text);
        }
        /// <summary>
        /// Обработка разметки текста ОДНОЙ СТРОКИ. Без использования цитат
        /// </summary>
        /// <param name="text">Текст с сырым Markdown</param>
        /// <param name="chatOption">Опции чата, заданные стримером для виджета</param>
        /// <param name="mentions">Список упоминаний, сделанных в сообщении</param>
        /// <param name="guildID">ID гильдии (сервера), в котором написано сообщение</param>
        /// <param name="usedEmbedsUrls">Список использованных Url'ов в embed</param>
        /// <returns></returns>
        private static string RenderLineAsHTML(
            string text,
            ChatDrawOption chatOption,
            List <EventMessageCreate.EventMessageCreate_Mention> mentions,
            string guildID,
            HashSet <string> usedEmbedsUrls
            )
        {
            // У нас есть блоки, порождающие саб-блоки:
            // - цитаты (они уже обработаны, вложенных быть не может)
            // - спойлеры
            // Другие подтипы не порождают саб-блоки, даже удаление
            var guild = NKDiscordChatWidget.DiscordBot.Bot.guilds[guildID];

            var waitDictionary = new Dictionary <string, string>();

            var textWithWaiting =
                RenderMarkdownBlockAsHTMLInnerBlock(text, chatOption, waitDictionary, guild, mentions, usedEmbedsUrls);

            // Меняем все wait'ы внутри текста на значения из словаря
            // hint: Мы делаем это в бесконечном цикле, потому что маркировки могут быть вложенными
            // в произвольном порядке
            while (true)
            {
                // hint: Это можно сделать другим способом: Обходить waitDictionary, если смена произошла,
                // то удалять включение в waitDictionary и делать это пока waitDictionary.Any(),
                // это решение более наглядно, но более затратно, потому что приходится перестраивать Dictionary
                bool u = false;
                foreach (var(wait, replace) in waitDictionary)
                {
                    var textWithWaitingNew = textWithWaiting.Replace(wait, replace);
                    if (textWithWaitingNew != textWithWaiting)
                    {
                        textWithWaiting = textWithWaitingNew;
                        u = true;
                    }
                }

                if (!u)
                {
                    // Ни одной смены в цикле не было
                    break;
                }
            }

            return(textWithWaiting);
        }
        /// <summary>
        /// Format (no mark)
        /// </summary>
        /// <param name="text">Текст с сырым Markdown</param>
        /// <param name="chatOption">Опции чата, заданные стримером для виджета</param>
        /// <param name="waitDictionary">Dictionary для саб-блоков</param>
        /// <returns></returns>
        public static string MarkNoFormatting(
            string text,
            ChatDrawOption chatOption,
            Dictionary <string, string> waitDictionary
            )
        {
            // TODO: double backtick

            text = rWithoutMark.Replace(text, m1 =>
            {
                var wait             = GetWaitString();
                waitDictionary[wait] = string.Format("<span class='without-mark'>{0}</span>",
                                                     HttpUtility.HtmlEncode(m1.Groups[1].Value));

                return(wait);
            });

            return(text);
        }
        /// <summary>
        /// Emoji (картинки)
        /// </summary>
        /// <param name="text">Текст с сырым Markdown</param>
        /// <param name="chatOption">Опции чата, заданные стримером для виджета</param>
        /// <param name="waitDictionary">Dictionary для саб-блоков</param>
        /// <param name="guild">Гильдия (сервер), внутри которого написано сообщение</param>
        /// <returns></returns>
        public static string MarkEmojiImages(
            string text,
            ChatDrawOption chatOption,
            Dictionary <string, string> waitDictionary,
            EventGuildCreate guild
            )
        {
            var thisGuildEmojis = new HashSet <string>();

            foreach (var emoji in guild.emojis)
            {
                thisGuildEmojis.Add(emoji.id);
            }

            // Эмодзи внутри текста
            text = rEmojiWithinText.Replace(text, m1 =>
            {
                string emojiID  = m1.Groups[3].Value;
                bool isRelative = thisGuildEmojis.Contains(emojiID);
                int emojiShow   = isRelative ? chatOption.emoji_relative : chatOption.emoji_stranger;
                if (emojiShow == 2)
                {
                    return(":" + m1.Groups[2].Value + ":");
                }

                var wait = GetWaitString();
                var url  = string.Format("https://cdn.discordapp.com/emojis/{0}.{1}",
                                         emojiID,
                                         (m1.Groups[1].Value == "a") ? "gif" : "png"
                                         );

                waitDictionary[wait] = string.Format("<span class='emoji {2}'><img src='{0}' alt=':{1}:'></span>",
                                                     HttpUtility.HtmlEncode(url),
                                                     HttpUtility.HtmlEncode(m1.Groups[2].Value),
                                                     (emojiShow == 1) ? "blur" : ""
                                                     );

                return(wait);
            });

            return(text);
        }
        private static void AddMessageReactionHTML(
            ICollection <string> reactionHTMLs,
            NKDiscordChatWidget.DiscordBot.Classes.EventMessageCreate.EventMessageCreate_Reaction reaction,
            int emojiShow,
            ChatDrawOption chatOption
            )
        {
            if (reaction.emoji.id != null)
            {
                // Эмодзи из Дискорда (паки эмодзей с серверов)
                reactionHTMLs.Add(string.Format(
                                      "<div class='emoji {2}'><img src='{0}' alt=':{1}:'><span class='count'>{3}</span></div>",
                                      HttpUtility.HtmlEncode(reaction.emoji.URL),
                                      HttpUtility.HtmlEncode(reaction.emoji.name),
                                      (emojiShow == 1) ? "blur" : "",
                                      reaction.count
                                      ));
            }
            else
            {
                // Стандартные Unicode-эмодзи
                string emojiHtml;
                var    emojiPack = chatOption.unicode_emoji_displaying;
                // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression
                if (emojiPack != EmojiPackType.StandardOS)
                {
                    emojiHtml = AddMessageReactionHTMLWithEmojiPack(reaction, emojiPack);
                }
                else
                {
                    emojiHtml = HttpUtility.HtmlEncode(reaction.emoji.name);
                }

                reactionHTMLs.Add(string.Format(
                                      "<div class='emoji {1}'>{0}<span class='count'>{2}</span></div>",
                                      emojiHtml,
                                      (emojiShow == 1) ? "blur" : "",
                                      reaction.count
                                      ));
            }
        }
        /// <summary>
        /// Наклонный (курсивный) текст через звёздочки
        /// </summary>
        /// <param name="text">Текст с сырым Markdown</param>
        /// <param name="chatOption">Опции чата, заданные стримером для виджета</param>
        /// <param name="waitDictionary">Dictionary для саб-блоков</param>
        /// <param name="guild">Гильдия (сервер), внутри которого написано сообщение</param>
        /// <param name="mentions">Список упоминаний, сделанных в сообщении</param>
        /// <param name="usedEmbedsUrls">Список использованных Url'ов в embed</param>
        /// <returns></returns>
        public static string MarkItalicViaAsterisk(
            string text,
            ChatDrawOption chatOption,
            Dictionary <string, string> waitDictionary,
            EventGuildCreate guild,
            List <EventMessageCreate.EventMessageCreate_Mention> mentions,
            HashSet <string> usedEmbedsUrls
            )
        {
            text = rSingleAsterisk.Replace(text, m1 =>
            {
                var firstChar = m1.Groups[1].Value.Substring(0, 1);
                if (firstChar == " ")
                {
                    return(m1.Groups[0].Value);
                }

                var lastChar = m1.Groups[1].Value.Substring(m1.Groups[1].Value.Length - 1);
                if (lastChar == " ")
                {
                    return(m1.Groups[0].Value);
                }

                var subBlock = RenderMarkdownBlockAsHTMLInnerBlock(
                    m1.Groups[1].Value,
                    chatOption,
                    waitDictionary,
                    guild,
                    mentions,
                    usedEmbedsUrls
                    );

                var wait             = GetWaitString();
                waitDictionary[wait] = string.Format("<em>{0}</em>", subBlock);
                return(wait);
            });

            return(text);
        }
        private static IEnumerable<object[]> GetInputs()
        {
            var result = new List<object[]>();
            var inputs = new List<string[]>()
            {
                new[]
                {
                    "<:st1:568685037868810269> <a:box1:663446227550994452> 😏",
                    "<span class='emoji '><img src='https://cdn.discordapp.com/emojis/568685037868810269.png' alt=':st1:'></span> <span class='emoji '><img src='https://cdn.discordapp.com/emojis/663446227550994452.gif' alt=':box1:'></span> <span class='emoji unicode-emoji '><img src='/images/emoji/twemoji/1f60f.svg' alt=':1f60f:'></span>",
                },
                new[]
                {
                    "😏 😼",
                    "<span class='emoji unicode-emoji '><img src='/images/emoji/twemoji/1f60f.svg' alt=':1f60f:'></span> <span class='emoji unicode-emoji '><img src='/images/emoji/twemoji/1f63c.svg' alt=':1f63c:'></span>",
                },
                new[]
                {
                    "<@!428567095563780107>",
                    "<span class='user mention' style='color: #F1C40F;'>@北風</span>",
                },
                new[]
                {
                    "<@&633965723764523028>",
                    "<span class='role mention' style='color: #9B59B6;'>@Фиолетовый</span>",
                },
                new[]
                {
                    "<@!400000000000000000>",
                    "<Пользователь Unknown #400000000000000000>",
                },
                new[]
                {
                    "<@&600000000000000000>",
                    "<Role Unknown #600000000000000000>",
                },
            };

            var mentions = new List<EventMessageCreate.EventMessageCreate_Mention>();
            // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
            foreach (var member in NKDiscordChatWidget.DiscordBot.Bot.guilds[guildID].members)
            {
                mentions.Add(new EventMessageCreate.EventMessageCreate_Mention()
                {
                    member = member,
                    username = member.user.username,
                    id = member.user.id,
                });
            }

            var chatOption = new ChatDrawOption()
            {
                message_mentions_style = 1,
            };

            foreach (var input in inputs)
            {
                foreach (var u1 in new[] {false, true})
                {
                    var prefix = u1 ? GetRandomWord() + " " : "";

                    for (var i1 = 0; i1 < 3; i1++)
                    {
                        string postfix;
                        switch (i1)
                        {
                            case 0:
                                postfix = "";
                                break;
                            case 1:
                                postfix = " ";
                                break;
                            case 2:
                                postfix = " " + GetRandomWord();
                                break;
                            default:
                                throw new Exception();
                        }

                        var markdown = prefix + input[0] + postfix;
                        var html = prefix + input[1] + postfix;

                        result.Add(new object[]
                        {
                            markdown,
                            "<div class='line'>" + html + "</div>",
                            chatOption,
                            mentions,
                            null
                        });
                    }
                }
            }

            foreach (var t1 in inputs)
            {
                result.AddRange(inputs.Select(t2 => new object[]
                {
                    t1[0] + " " + t2[0],
                    "<div class='line'>" + t1[1] + " " + t2[1] + "</div>",
                    chatOption,
                    mentions,
                    null
                }));
            }

            return result;
        }
        /// <summary>
        /// Обработка разметки сообщения внутри логического блока сообщения (root-сообщение, цитата, спойлер)
        /// </summary>
        /// <param name="text">Текст с сырым Markdown</param>
        /// <param name="chatOption">Опции чата, заданные стримером для виджета</param>
        /// <param name="waitDictionary">Dictionary для саб-блоков</param>
        /// <param name="guild">Гильдия (сервер), внутри которого написано сообщение</param>
        /// <param name="mentions">Список упоминаний, сделанных в сообщении</param>
        /// <param name="usedEmbedsUrls">Список использованных Url'ов в embed</param>
        /// <returns></returns>
        private static string RenderMarkdownBlockAsHTMLInnerBlock(
            string text,
            ChatDrawOption chatOption,
            Dictionary <string, string> waitDictionary,
            EventGuildCreate guild,
            List <EventMessageCreate.EventMessageCreate_Mention> mentions,
            HashSet <string> usedEmbedsUrls
            )
        {
            while (true)
            {
                var highlights = new List <dynamic>();
                foreach (var pair in new List <dynamic>()
                {
                    new
                    {
                        regexp = rWithoutMark,
                        deleg = (Func <string>)(() => MarkNoFormatting(text, chatOption, waitDictionary))
                    },
                    new
                    {
                        regexp = rLink,
                        deleg = (Func <string>)(() => MarkLinks(text, chatOption, waitDictionary, usedEmbedsUrls))
                    },
                    new
                    {
                        regexp = rEmojiWithinText,
                        deleg = (Func <string>)(() => MarkEmojiImages(text, chatOption, waitDictionary, guild))
                    },
                    // mentions
                    new
                    {
                        regexp = rMentionNick,
                        deleg = (Func <string>)(() =>
                                                MarkMentionsPeople(text, chatOption, waitDictionary, guild, mentions))
                    },
                    new
                    {
                        regexp = rMentionRole,
                        deleg = (Func <string>)(() => MarkMentionsRole(text, chatOption, waitDictionary, guild))
                    },

                    // simple mark
                    new
                    {
                        regexp = rSpoilerMark,
                        deleg = (Func <string>)(() =>
                                                MarkSpoilers(text, chatOption, waitDictionary, guild, mentions, usedEmbedsUrls))
                    },
                    new
                    {
                        regexp = rBoldItalic,
                        deleg = (Func <string>)(() =>
                                                MarkBoldItalic(text, chatOption, waitDictionary, guild, mentions, usedEmbedsUrls))
                    },
                    new
                    {
                        regexp = rBold,
                        deleg = (Func <string>)(() =>
                                                MarkBold(text, chatOption, waitDictionary, guild, mentions, usedEmbedsUrls))
                    },

                    new
                    {
                        regexp = rTripleUnderscore,
                        deleg = (Func <string>)(() =>
                                                MarkUnderscoreItalic(text, chatOption, waitDictionary, guild, mentions, usedEmbedsUrls))
                    },
                    new
                    {
                        regexp = rDoubleUnderscore,
                        deleg = (Func <string>)(() =>
                                                MarkUnderscore(text, chatOption, waitDictionary, guild, mentions, usedEmbedsUrls))
                    },
                    new
                    {
                        regexp = rSingleAsterisk,
                        deleg = (Func <string>)(() =>
                                                MarkItalicViaAsterisk(text, chatOption, waitDictionary, guild, mentions, usedEmbedsUrls))
                    },
                    new
                    {
                        regexp = rItalicUnderscore,
                        deleg = (Func <string>)(() =>
                                                MarkItalicViaUnderscore(text, chatOption, waitDictionary, guild, mentions, usedEmbedsUrls))
                    },
                    new
                    {
                        regexp = rDelete,
                        deleg = (Func <string>)(() =>
                                                MarkDelete(text, chatOption, waitDictionary, guild, mentions, usedEmbedsUrls))
                    },
                })
                {
                    Regex regex = pair.regexp;
                    var   m     = regex.Match(text);
                    if (!m.Success)
                    {
                        continue;
                    }

                    highlights.Add(new { index = m.Index, pair.deleg });
                    if (m.Index == 0)
                    {
                        break;
                    }
                }

                if (!highlights.Any())
                {
                    // Не осталось маркировки, выходим

                    break;
                }

                highlights.Sort((a, b) => a.index.CompareTo(b.index));

                var u = false;
                // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
                foreach (var highlight in highlights)
                {
                    Func <string> delegateLocal = highlight.deleg;
                    var           result        = delegateLocal.Invoke();
                    if (result != text)
                    {
                        text = result;
                        u    = true;
                        break;
                    }
                }

                if (!u)
                {
                    // hint: Это warning однозначный
                    break;
                }
            }

            text = MarkEmojiUnicode(text, chatOption, waitDictionary);

            return(text);
        }
        private static IEnumerable<object[]> GetLinksCases()
        {
            var result = new List<object[]>();

            var chatOptionNotShort = new ChatDrawOption
            {
                short_anchor = 0,
            };
            var chatOptionShort = new ChatDrawOption
            {
                short_anchor = 1,
            };

            // ReSharper disable once LoopCanBeConvertedToQuery
            foreach (var input in new[]
            {
                new[] {"http://example.com/", "http://example.com/", "http://example.com/"},
                new[]
                {
                    "https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D0%BA%D0%B8%D0%BF%D0%B5%D0%B4%D0%B8%D1%8F:%D0%92%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5",
                    "https://ru.wikipedia.org/wiki/Википедия:Введение",
                    "https://ru.wikipedia.org/wiki/Википед...",
                },
            })
            {
                var markdown = input[0];
                var html = string.Format("<a href='{0}' target='_blank'>{1}</a>",
                    HttpUtility.HtmlEncode(input[0]),
                    HttpUtility.HtmlEncode(input[1])
                );
                var htmlShortAnchor = string.Format("<a href='{0}' target='_blank'>{1}</a>",
                    HttpUtility.HtmlEncode(input[0]),
                    HttpUtility.HtmlEncode(input[2])
                );

                result.Add(new object[]
                {
                    markdown,
                    string.Format("<div class='line'>{0}</div>", html),
                    chatOptionNotShort,
                    null,
                    null
                });
                result.Add(new object[]
                {
                    markdown,
                    string.Format("<div class='line'>{0}</div>", htmlShortAnchor),
                    chatOptionShort,
                    null,
                    null
                });
            }

            //
            // ReSharper disable once LoopCanBeConvertedToQuery
            foreach (var input in new[]
            {
                new dynamic[]
                {
                    "https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg",
                    new List<string>() {"https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg"},
                    "",
                    chatOptionShort
                },
                new dynamic[]
                {
                    "https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg",
                    new List<string>() {"https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg"},
                    "",
                    chatOptionNotShort
                },
                new dynamic[]
                {
                    "https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg",
                    new List<string>(),
                    "<a href='https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg' target='_blank'>https://cs7.pikabu.ru/post_img/big/20...</a>",
                    chatOptionShort
                },
                new dynamic[]
                {
                    "https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg",
                    new List<string>(),
                    "<a href='https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg' target='_blank'>https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg</a>",
                    chatOptionNotShort
                },

                new dynamic[]
                {
                    "https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D0%BA%D0%B8%D0%BF%D0%B5%D0%B4%D0%B8%D1%8F:%D0%92%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5",
                    new List<string>() {"https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg"},
                    " <a href='https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D0%BA%D0%B8%D0%BF%D0%B5%D0%B4%D0%B8%D1%8F:%D0%92%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5' target='_blank'>https://ru.wikipedia.org/wiki/Википед...</a>",
                    chatOptionShort
                },
                new dynamic[]
                {
                    "https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D0%BA%D0%B8%D0%BF%D0%B5%D0%B4%D0%B8%D1%8F:%D0%92%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5",
                    new List<string>() {"https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg"},
                    " <a href='https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D0%BA%D0%B8%D0%BF%D0%B5%D0%B4%D0%B8%D1%8F:%D0%92%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5' target='_blank'>https://ru.wikipedia.org/wiki/Википедия:Введение</a>",
                    chatOptionNotShort
                },
                new dynamic[]
                {
                    "https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D0%BA%D0%B8%D0%BF%D0%B5%D0%B4%D0%B8%D1%8F:%D0%92%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5",
                    new List<string>() {"https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg"},
                    "<a href='https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D0%BA%D0%B8%D0%BF%D0%B5%D0%B4%D0%B8%D1%8F:%D0%92%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5' target='_blank'>https://ru.wikipedia.org/wiki/Википед...</a>",
                    chatOptionShort
                },
                new dynamic[]
                {
                    "https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D0%BA%D0%B8%D0%BF%D0%B5%D0%B4%D0%B8%D1%8F:%D0%92%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5",
                    new List<string>() {"https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg"},
                    "<a href='https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D0%BA%D0%B8%D0%BF%D0%B5%D0%B4%D0%B8%D1%8F:%D0%92%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5' target='_blank'>https://ru.wikipedia.org/wiki/Википедия:Введение</a>",
                    chatOptionNotShort
                },
                new dynamic[]
                {
                    "https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D0%BA%D0%B8%D0%BF%D0%B5%D0%B4%D0%B8%D1%8F:%D0%92%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5",
                    new List<string>()
                    {
                        "https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg",
                        "https://media.discordapp.net/attachments/421392740970921996/599311410807439390/terminator-thumbs-up.gif"
                    },
                    " <a href='https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D0%BA%D0%B8%D0%BF%D0%B5%D0%B4%D0%B8%D1%8F:%D0%92%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5' target='_blank'>https://ru.wikipedia.org/wiki/Википед...</a>",
                    chatOptionShort
                },
                new dynamic[]
                {
                    "https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D0%BA%D0%B8%D0%BF%D0%B5%D0%B4%D0%B8%D1%8F:%D0%92%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5",
                    new List<string>()
                    {
                        "https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg",
                        "https://media.discordapp.net/attachments/421392740970921996/599311410807439390/terminator-thumbs-up.gif"
                    },
                    " <a href='https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D0%BA%D0%B8%D0%BF%D0%B5%D0%B4%D0%B8%D1%8F:%D0%92%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5' target='_blank'>https://ru.wikipedia.org/wiki/Википедия:Введение</a>",
                    chatOptionNotShort
                },
                new dynamic[]
                {
                    "https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D0%BA%D0%B8%D0%BF%D0%B5%D0%B4%D0%B8%D1%8F:%D0%92%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5",
                    new List<string>()
                    {
                        "https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg",
                        "https://media.discordapp.net/attachments/421392740970921996/599311410807439390/terminator-thumbs-up.gif"
                    },
                    "<a href='https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D0%BA%D0%B8%D0%BF%D0%B5%D0%B4%D0%B8%D1%8F:%D0%92%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5' target='_blank'>https://ru.wikipedia.org/wiki/Википед...</a>",
                    chatOptionShort
                },
                new dynamic[]
                {
                    "https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D0%BA%D0%B8%D0%BF%D0%B5%D0%B4%D0%B8%D1%8F:%D0%92%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5",
                    new List<string>()
                    {
                        "https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg",
                        "https://media.discordapp.net/attachments/421392740970921996/599311410807439390/terminator-thumbs-up.gif"
                    },
                    "<a href='https://ru.wikipedia.org/wiki/%D0%92%D0%B8%D0%BA%D0%B8%D0%BF%D0%B5%D0%B4%D0%B8%D1%8F:%D0%92%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5' target='_blank'>https://ru.wikipedia.org/wiki/Википедия:Введение</a>",
                    chatOptionNotShort
                },
                //
                new dynamic[]
                {
                    "https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg https://media.discordapp.net/attachments/421392740970921996/599311410807439390/terminator-thumbs-up.gif",
                    new List<string>()
                    {
                        "https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg",
                        "https://media.discordapp.net/attachments/421392740970921996/599311410807439390/terminator-thumbs-up.gif"
                    },
                    " ",
                    chatOptionShort
                },
                new dynamic[]
                {
                    "https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg https://media.discordapp.net/attachments/421392740970921996/599311410807439390/terminator-thumbs-up.gif",
                    new List<string>()
                    {
                        "https://cs7.pikabu.ru/post_img/big/2018/10/31/8/1540989921187952325.jpg",
                        "https://media.discordapp.net/attachments/421392740970921996/599311410807439390/terminator-thumbs-up.gif"
                    },
                    " ",
                    chatOptionNotShort
                },
            })
            {
                string markdown = input[0];
                List<string> list = input[1];
                string expectedHTML = input[2];
                ChatDrawOption chatOption = input[3];

                result.Add(new object[]
                {
                    markdown,
                    string.Format("<div class='line'>{0}</div>", expectedHTML),
                    chatOption,
                    null,
                    list
                });
            }

            return result;
        }
        private static IEnumerable<object[]> GetEdgeCases()
        {
            var result = new List<object[]>();

            // Тестируем Эмодзи: chatOption.emoji_relative & chatOption.emoji_stranger
            const string ourServerEmoji = "<a:box1:663446227550994452> <:st1:568685037868810269>";
            const string otherServerEmoji = "<a:unk1:600000000000000000> <:unk2:500000000000000000>";
            const string ourServerEmojiHTML =
                "<span class='emoji {0}'><img src='https://cdn.discordapp.com/emojis/663446227550994452.gif' alt=':box1:'></span> " +
                "<span class='emoji {0}'><img src='https://cdn.discordapp.com/emojis/568685037868810269.png' alt=':st1:'></span>";
            const string otherServerEmojiHTML =
                "<span class='emoji {0}'><img src='https://cdn.discordapp.com/emojis/600000000000000000.gif' alt=':unk1:'></span> " +
                "<span class='emoji {0}'><img src='https://cdn.discordapp.com/emojis/500000000000000000.png' alt=':unk2:'></span>";
            const string ourServerEmojiPlain = ":box1: :st1:";
            const string otherServerEmojiPlain = ":unk1: :unk2:";

            var mentions = new List<EventMessageCreate.EventMessageCreate_Mention>();
            // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
            foreach (var member in NKDiscordChatWidget.DiscordBot.Bot.guilds[guildID].members)
            {
                mentions.Add(new EventMessageCreate.EventMessageCreate_Mention()
                {
                    member = member,
                    username = member.user.username,
                    id = member.user.id,
                });
            }

            for (int i1 = 1; i1 < 4; i1++)
            {
                var markdown = ((i1 % 2 == 1) ? ourServerEmoji : "") + ((i1 >> 1 == 1) ? otherServerEmoji : "");

                for (int i2 = 0; i2 < (1 << 4); i2++)
                {
                    var chatOption = new ChatDrawOption
                    {
                        emoji_relative = i2 % 4,
                        emoji_stranger = (i2 >> 2) % 4,
                    };
                    if ((chatOption.emoji_relative == 3) || (chatOption.emoji_stranger == 3))
                    {
                        continue;
                    }

                    var expectedHTML = "";
                    if (i1 % 2 == 1)
                    {
                        switch (chatOption.emoji_relative)
                        {
                            case 0:
                                expectedHTML += string.Format(ourServerEmojiHTML, "");
                                break;
                            case 1:
                                expectedHTML += string.Format(ourServerEmojiHTML, "blur");
                                break;
                            case 2:
                                expectedHTML += ourServerEmojiPlain;
                                break;
                        }
                    }

                    if (i1 >> 1 == 1)
                    {
                        switch (chatOption.emoji_stranger)
                        {
                            case 0:
                                expectedHTML += string.Format(otherServerEmojiHTML, "");
                                break;
                            case 1:
                                expectedHTML += string.Format(otherServerEmojiHTML, "blur");
                                break;
                            case 2:
                                expectedHTML += otherServerEmojiPlain;
                                break;
                        }
                    }

                    result.Add(new object[]
                    {
                        markdown,
                        "<div class='line'>" + expectedHTML + "</div>",
                        chatOption,
                        null,
                        null
                    });
                }
            }

            {
                var inputs = new dynamic[]
                {
                    new
                    {
                        codes = new[] {0x1F346},
                        expected =
                            "<span class='emoji unicode-emoji '><img src='/images/emoji/twemoji/1f346.svg' alt=':1f346:'></span>"
                    },
                    new
                    {
                        codes = new[] {0x1F47A},
                        expected =
                            "<span class='emoji unicode-emoji '><img src='/images/emoji/twemoji/1f47a.svg' alt=':1f47a:'></span>"
                    },
                    new
                    {
                        codes = new[] {0x1F346, 0x1F47A},
                        expected =
                            "<span class='emoji unicode-emoji '><img src='/images/emoji/twemoji/1f346.svg' alt=':1f346:'></span>" +
                            "<span class='emoji unicode-emoji '><img src='/images/emoji/twemoji/1f47a.svg' alt=':1f47a:'></span>"
                    },
                    new
                    {
                        codes = new[] {0x1F1F7, 0x1F1FA},
                        expected =
                            "<span class='emoji unicode-emoji '><img src='/images/emoji/twemoji/1f1f7-1f1fa.svg' alt=':1f1f7-1f1fa:'></span>"
                    },
                    new
                    {
                        codes = new[] {0x31, 0xfe0f, 0x20e3},
                        expected = (string) null,
                    },
                };

                foreach (var input in inputs)
                {
                    int[] codes = input.codes;
                    string expectedTwemoji = input.expected;

                    foreach (var emojiPack in new[] {EmojiPackType.Twemoji, EmojiPackType.StandardOS})
                    {
                        var chatOption = new ChatDrawOption {unicode_emoji_displaying = emojiPack};
                        var emoji = codes.Aggregate("",
                            (current, code) => current + Utf8ToUnicode.UnicodeCodeToString(code));

                        var expectedHTML = "";
                        // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
                        switch (emojiPack)
                        {
                            case EmojiPackType.Twemoji:
                                expectedHTML = expectedTwemoji ?? emoji;
                                break;
                            case EmojiPackType.StandardOS:
                                expectedHTML = emoji;
                                break;
                            default:
                                throw new ArgumentOutOfRangeException();
                        }

                        result.Add(new object[]
                        {
                            emoji,
                            "<div class='line'>" + expectedHTML + "</div>",
                            chatOption,
                            null,
                            null
                        });
                    }
                }
            }

            // text_spoiler
            {
                var word = GetRandomWord();
                string markdown = string.Format("||{0}||", word);
                for (int i = 0; i < 2; i++)
                {
                    var chatOption = new ChatDrawOption {text_spoiler = i};
                    var html = string.Format(
                        "<div class='line'><span class='spoiler {0}'><span class='spoiler-content'>{1}</span></span></div>",
                        (i == 1) ? "spoiler-show" : "",
                        word
                    );

                    result.Add(new object[]
                    {
                        markdown,
                        html,
                        chatOption,
                        null,
                        null
                    });
                }
            }

            // message_mentions_style
            {
                var inputs = new[]
                {
                    new[]
                    {
                        "428567095563780107",
                        "北風",
                        "F1C40F",
                    },
                    new[]
                    {
                        "568138249986375682",
                        "NKDiscordChatWidget",
                        "E74C3C",
                    },
                };

                foreach (var input in inputs)
                {
                    for (int i = 0; i < 2; i++)
                    {
                        string markdown = string.Format("<@!{0}>", input[0]);
                        var chatOption = new ChatDrawOption {message_mentions_style = i};
                        var html = string.Format(
                            "<div class='line'><span class='user mention'{0}>@{1}</span></div>",
                            (i == 1) ? string.Format(" style='color: #{0};'", input[2]) : "",
                            input[1]
                        );

                        result.Add(new object[]
                        {
                            markdown,
                            html,
                            chatOption,
                            mentions,
                            null
                        });
                        result.Add(new object[]
                        {
                            markdown,
                            string.Format("<div class='line'><Пользователь Unknown #{0}></div>", input[0]),
                            chatOption,
                            null,
                            null
                        });
                    }
                }
            }

            return result;
        }
        private static IEnumerable<object[]> GetQuoteCheck()
        {
            var result = new List<object[]>();
            var inputs = new List<string[]>()
            {
                new[]
                {
                    "<:st1:568685037868810269> <a:box1:663446227550994452> 😏",
                    "<span class='emoji '><img src='https://cdn.discordapp.com/emojis/568685037868810269.png' alt=':st1:'></span> <span class='emoji '><img src='https://cdn.discordapp.com/emojis/663446227550994452.gif' alt=':box1:'></span> <span class='emoji unicode-emoji '><img src='/images/emoji/twemoji/1f60f.svg' alt=':1f60f:'></span>",
                },
                new[]
                {
                    "😏 😼",
                    "<span class='emoji unicode-emoji '><img src='/images/emoji/twemoji/1f60f.svg' alt=':1f60f:'></span> <span class='emoji unicode-emoji '><img src='/images/emoji/twemoji/1f63c.svg' alt=':1f63c:'></span>",
                },
                new[]
                {
                    "<@!428567095563780107>",
                    "<span class='user mention' style='color: #F1C40F;'>@北風</span>",
                },
                new[]
                {
                    "<@&633965723764523028>",
                    "<span class='role mention' style='color: #9B59B6;'>@Фиолетовый</span>",
                },
            };
            inputs.AddRange(GetThreeAsteriskTestsRaw());

            var mentions = new List<EventMessageCreate.EventMessageCreate_Mention>();
            // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
            foreach (var member in NKDiscordChatWidget.DiscordBot.Bot.guilds[guildID].members)
            {
                mentions.Add(new EventMessageCreate.EventMessageCreate_Mention()
                {
                    member = member,
                    username = member.user.username,
                    id = member.user.id,
                });
            }

            var chatOption = new ChatDrawOption() {message_mentions_style = 1};

            for (int i = 0; i < Math.Pow(4, 4); i++)
            {
                var ars = new List<int>();
                {
                    int i1 = i;
                    for (int j = 0; j < 4; j++)
                    {
                        int n = i1 % 4;
                        ars.Add(n);
                        i1 >>= 2;
                    }
                }

                if (ars[0] == 0)
                {
                    continue;
                }

                var inputMarkdown = "";
                var outputHTML = "";

                bool isInQuote = false;
                var outputHTMLQuote = "";
                foreach (var ar in ars)
                {
                    string localInputMarkdown, localOutputHTML;
                    if (ar % 2 == 0)
                    {
                        localInputMarkdown = "";
                        localOutputHTML = "";
                    }
                    else
                    {
                        var input = inputs[_rnd.Next(0, inputs.Count - 1)];
                        localInputMarkdown = input[0];
                        localOutputHTML = input[1];
                    }

                    if ((ar == 2) || (ar == 3))
                    {
                        inputMarkdown += "\n> " + localInputMarkdown;
                        outputHTMLQuote += "<div class='line'>" + localOutputHTML + "</div>";
                        isInQuote = true;
                    }
                    else
                    {
                        inputMarkdown += "\n" + localInputMarkdown;
                        if (isInQuote)
                        {
                            outputHTML += string.Format(
                                "<div class='quote-block'><div class='quote-border'></div><div class='quote-content'>{0}</div></div>",
                                outputHTMLQuote
                            );
                            isInQuote = false;
                            outputHTMLQuote = "";
                        }

                        outputHTML += "<div class='line'>" + localOutputHTML + "</div>";
                    }
                }

                if (isInQuote)
                {
                    outputHTML += string.Format(
                        "<div class='quote-block'><div class='quote-border'></div><div class='quote-content'>{0}</div></div>",
                        outputHTMLQuote
                    );
                }

                result.Add(new object[]
                {
                    inputMarkdown.Substring(1),
                    outputHTML,
                    chatOption,
                    mentions,
                    null
                });
            }

            return result;
        }
        public static AnswerMessage DrawMessage(EventMessageCreate message, ChatDrawOption chatDrawOption)
        {
            string htmlContent = string.Format("<div class='content-message' data-id='{1}'>{0}</div>",
                                               DrawMessageContent(message, chatDrawOption), message.id);
            var timeUpdate = (message.edited_timestampAsDT != DateTime.MinValue)
                ? message.edited_timestampAsDT
                : message.timestampAsDT;

            /*
             * if (chatOption.merge_same_user_messages)
             * {
             *  // Соединяем сообщения одного и того же человека
             *  // @todo где-то здесь баг имплементации. Надо поправить
             *  for (var j = i + 1; j < messages.Count; j++)
             *  {
             *      if (messages[j].author.id == messages[i].author.id)
             *      {
             *          var localTimeUpdate = (messages[j].edited_timestampAsDT != DateTime.MinValue)
             *              ? messages[j].edited_timestampAsDT
             *              : messages[j].timestampAsDT;
             *          if (localTimeUpdate > timeUpdate)
             *          {
             *              timeUpdate = localTimeUpdate;
             *          }
             *
             *          htmlContent += string.Format("<div class='content-message' data-id='{1}'>{0}</div>",
             *              DrawMessageContent(messages[j], chatOption), messages[j].id);
             *          i = j;
             *      }
             *  }
             * }
             *
             */

            string nickColor = "inherit";

            if (message.member.roles.Any())
            {
                var roles = NKDiscordChatWidget.DiscordBot.Bot.guilds[message.guild_id].roles.ToList();
                roles.Sort((a, b) => b.position.CompareTo(a.position));
                Role role = roles.FirstOrDefault(t => message.member.roles.Contains(t.id));
                if (role != null)
                {
                    nickColor = role.color.ToString("X");
                    nickColor = "#" + nickColor.PadLeft(6, '0');
                }
            }

            string sha1hash;

            using (var hashA = SHA1.Create())
            {
                byte[] data = hashA.ComputeHash(Encoding.UTF8.GetBytes(htmlContent));
                sha1hash = data.Aggregate("", (current, c) => current + c.ToString("x2"));
            }

            var drawDateTime = message.timestampAsDT.AddMinutes(chatDrawOption.timezone);

            var html = string.Format(
                "<div class='user'><img src='{0}' alt='{1}'></div>" +
                "<div class='content'>" +
                "<div class='content-header'><span class='content-user' style='color: {4};'>{1}</span><span class='content-time'>{3:HH:mm:ss dd.MM.yyyy}</span></div>" +
                "{2}" +
                "</div><div style='clear: both;'></div><hr>",
                HttpUtility.HtmlEncode(message.author.avatarURL),
                HttpUtility.HtmlEncode(
                    !string.IsNullOrEmpty(message.member.nick)
                        ? message.member.nick
                        : message.author.username
                    ),
                htmlContent,
                drawDateTime,
                nickColor
                );

            return(new AnswerMessage()
            {
                id = message.id,
                time = ((DateTimeOffset)message.timestampAsDT).ToUnixTimeMilliseconds() * 0.001d,
                time_update = ((DateTimeOffset)timeUpdate).ToUnixTimeMilliseconds() * 0.001d,
                html = html,
                hash = sha1hash,
            });
        }
        /// <summary>
        /// Парсинг ссылок внутри текста
        /// </summary>
        /// <param name="text">Текст с сырым Markdown</param>
        /// <param name="chatOption">Опции чата, заданные стримером для виджета</param>
        /// <param name="waitDictionary">Dictionary для саб-блоков</param>
        /// <param name="usedEmbedsUrls">Список использованных Url'ов в embed</param>
        /// <returns></returns>
        public static string MarkLinks(
            string text,
            ChatDrawOption chatOption,
            Dictionary <string, string> waitDictionary,
            HashSet <string> usedEmbedsUrls
            )
        {
            var r = new Regex("(%[0-9a-f]{2,2})+", RegexOptions.Compiled | RegexOptions.IgnoreCase);

            text = rLink.Replace(text, m1 =>
            {
                var wait = GetWaitString();
                var url  = string.Format("{0}://{1}{2}{3}",
                                         m1.Groups[2].Value,
                                         m1.Groups[3].Value,
                                         m1.Groups[4].Value,
                                         m1.Groups[5].Value
                                         );

                if ((chatOption.hide_used_embed_links == 1) && (usedEmbedsUrls.Contains(url)))
                {
                    return(m1.Groups[1].Value);
                }

                var rawPath = m1.Groups[4].Value;
                while (true)
                {
                    var m = r.Match(rawPath);
                    if (!m.Success)
                    {
                        break;
                    }

                    var s     = m.Groups[0].Value;
                    var bytes = new List <byte>();
                    while (s != "")
                    {
                        var hex = s.Substring(1, 2);
                        s       = s.Substring(3);

                        var b = byte.Parse(hex, System.Globalization.NumberStyles.HexNumber);
                        bytes.Add(b);
                    }

                    var s1  = rawPath.Substring(0, m.Index);
                    var s2  = Encoding.UTF8.GetString(bytes.ToArray());
                    var s3  = rawPath.Substring(m.Index + m.Length);
                    rawPath = s1 + s2 + s3;
                }

                var anchor = HttpUtility.HtmlEncode(string.Format("{0}://{1}{2}{3}",
                                                                  m1.Groups[2].Value,
                                                                  m1.Groups[3].Value,
                                                                  rawPath,
                                                                  m1.Groups[5].Value
                                                                  ));

                if ((chatOption.short_anchor == 1) && (anchor.Length > 40))
                {
                    anchor = anchor.Substring(0, 37) + "...";
                }

                // TODO: Иногда стирать всю ссылку, потому что она просто для превью картинки/видео
                waitDictionary[wait] = string.Format(
                    "<a href='{0}' target='_blank'>{1}</a>",
                    HttpUtility.HtmlEncode(url),
                    HttpUtility.HtmlEncode(anchor)
                    );

                return(m1.Groups[1].Value + wait);
            });

            return(text);
        }
        private static string DrawMessageContent(EventMessageCreate message, ChatDrawOption chatOption)
        {
            var usedEmbedsUrls = new HashSet <string>();

            foreach (var embed in message.embeds)
            {
                usedEmbedsUrls.Add(embed.url);
            }

            var guildID         = message.guild_id;
            var guild           = NKDiscordChatWidget.DiscordBot.Bot.guilds[guildID];
            var thisGuildEmojis = new HashSet <string>(guild.emojis.Select(emoji => emoji.id).ToList());

            // Основной текст
            string directContentHTML = NKDiscordChatWidget.General.MessageMarkdownParser.RenderMarkdownAsHTML(
                message.content, chatOption, message.mentions, guildID, usedEmbedsUrls);
            bool containOnlyUnicodeAndSpace;
            {
                var    rEmojiWithinText = new Regex(@"<\:(.+?)\:([0-9]+)>", RegexOptions.Compiled);
                long[] longs            = { };
                if (message.content != null)
                {
                    longs = Utf8ToUnicode.ToUnicodeCode(rEmojiWithinText.Replace(message.content, ""));
                }

                containOnlyUnicodeAndSpace = Utf8ToUnicode.ContainOnlyUnicodeAndSpace(longs);
            }
            string html = string.Format("<div class='content-direct {1}'>{0}</div>",
                                        directContentHTML, containOnlyUnicodeAndSpace ? "only-emoji" : "");

            // attachments
            if ((message.attachments != null) && message.attachments.Any() && (chatOption.attachments != 2))
            {
                string attachmentHTML = "";

                foreach (var attachment in message.attachments)
                {
                    var extension = attachment.filename.Split('.').Last().ToLowerInvariant();
                    // ReSharper disable once SwitchStatementMissingSomeCases
                    switch (extension)
                    {
                    case "mp4":
                    case "webm":
                        // TODO: Отображать размер видео
                        attachmentHTML += string.Format(
                            "<div class='attachment {1}'><div class='attachment-wrapper'><video><source src='{0}'></video></div></div>",
                            HttpUtility.HtmlEncode(attachment.proxy_url),
                            ((chatOption.attachments == 1) || attachment.IsSpoiler) ? "blur" : ""
                            );
                        break;

                    case "jpeg":
                    case "jpg":
                    case "bmp":
                    case "gif":
                    case "png":
                        attachmentHTML += string.Format(
                            "<div class='attachment {3}'><div class='attachment-wrapper'><img src='{0}' data-width='{1}' data-height='{2}'></div></div>",
                            HttpUtility.HtmlEncode(attachment.proxy_url),
                            attachment.width,
                            attachment.height,
                            ((chatOption.attachments == 1) || attachment.IsSpoiler) ? "blur" : ""
                            );
                        break;
                    }
                }

                html += string.Format("<div class='attachment-block'>{0}</div>", attachmentHTML);
            }

            // Preview
            if ((message.embeds != null) && message.embeds.Any() && (chatOption.link_preview != 2))
            {
                string previewHTML = "";

                foreach (var embed in message.embeds)
                {
                    var subHTML = "";
                    if (embed.provider != null)
                    {
                        subHTML += string.Format("<div class='provider'>{0}</div>",
                                                 HttpUtility.HtmlEncode(embed.provider.name));
                    }

                    if (embed.author != null)
                    {
                        subHTML += string.Format("<div class='author'>{0}</div>",
                                                 HttpUtility.HtmlEncode(embed.author.name));
                    }

                    subHTML += string.Format("<div class='title'>{0}</div>",
                                             HttpUtility.HtmlEncode(embed.title));

                    if (embed.thumbnail != null)
                    {
                        subHTML += string.Format("<div class='preview'><img src='{0}' alt='{1}'></div>",
                                                 HttpUtility.HtmlEncode(embed.thumbnail.proxy_url),
                                                 HttpUtility.HtmlEncode("Превью для «" + embed.title + "»")
                                                 );
                    }

                    var nickColor = embed.color.ToString("X");
                    nickColor = "#" + nickColor.PadLeft(6, '0');

                    previewHTML += string.Format(
                        "<div class='embed {2}'><div class='embed-pill' style='background-color: {1};'></div>" +
                        "<div class='embed-content {3}'>{0}</div></div>",
                        subHTML,
                        nickColor,
                        (chatOption.link_preview == 1) ? "blur" : "",
                        string.IsNullOrEmpty(embed.title) ? "embed-content_no-title" : ""
                        );
                }

                html += string.Format("<div class='embed-block'>{0}</div>", previewHTML);
            }

            // Реакции
            if (
                (message.reactions != null) && message.reactions.Any() &&
                ((chatOption.message_relative_reaction != 2) || (chatOption.message_stranger_reaction != 2))
                )
            {
                // Реакции
                var reactionHTMLs = new List <string>();
                foreach (var reaction in message.reactions)
                {
                    bool isRelative = ((reaction.emoji.id == null) || thisGuildEmojis.Contains(reaction.emoji.id));
                    int  emojiShow  = isRelative
                        ? chatOption.message_relative_reaction
                        : chatOption.message_stranger_reaction;
                    if (emojiShow == 2) // @todo убрать магическую константу
                    {
                        continue;
                    }

                    AddMessageReactionHTML(
                        reactionHTMLs,
                        reaction,
                        emojiShow,
                        chatOption
                        );
                }

                var s = reactionHTMLs.Aggregate("", (current, s1) => current + s1);
                html += string.Format("<div class='content-reaction'>{0}</div>", s);
            }

            return(html);
        }
        /// <summary>
        /// Парсинг эмодзи из дополнительных plain'ов в Unicode
        /// </summary>
        /// <param name="text">Текст с сырым Markdown</param>
        /// <param name="chatOption">Опции чата, заданные стримером для виджета</param>
        /// <param name="waitDictionary">Dictionary для саб-блоков</param>
        /// <returns></returns>
        public static string MarkEmojiUnicode(
            string text,
            ChatDrawOption chatOption,
            Dictionary <string, string> waitDictionary
            )
        {
            if (!UnicodeEmojiEngine.emojiList[chatOption.unicode_emoji_displaying].Any())
            {
                return(text);
            }

            var activeEmoji = new List <long>();

            var longs        = Utf8ToUnicode.ToUnicodeCode(text);
            var textAfter    = "";
            var containEmoji = false;

            foreach (var code in longs)
            {
                if (!UnicodeEmojiEngine.IsInIntervalEmoji(code, chatOption.unicode_emoji_displaying))
                {
                    // Этот символ НЕ является unicode emoji
                    if (activeEmoji.Any())
                    {
                        // У нас непустой буффер emoji символов, надо их записать в строку
                        var localEmojiList = UnicodeEmojiEngine.RenderEmojiAsStringList(
                            chatOption.unicode_emoji_displaying, activeEmoji);
                        textAfter += RenderEmojiStringListAsHtml(
                            localEmojiList,
                            chatOption.unicode_emoji_displaying,
                            waitDictionary,
                            chatOption.emoji_relative
                            );
                        activeEmoji = new List <long>();
                    }

                    textAfter += Utf8ToUnicode.UnicodeCodeToString(code);

                    continue;
                }

                // Этот символ ЯВЛЯЕТСЯ unicode emoji
                containEmoji = true;
                activeEmoji.Add(code);
            } // foreach

            if (activeEmoji.Any())
            {
                // У нас не пустой буффер emoji символов, надо их записать в строку
                var localEmojiList = UnicodeEmojiEngine.RenderEmojiAsStringList(
                    chatOption.unicode_emoji_displaying, activeEmoji);
                textAfter += RenderEmojiStringListAsHtml(
                    localEmojiList,
                    chatOption.unicode_emoji_displaying,
                    waitDictionary,
                    chatOption.emoji_relative
                    );
            }

            if (containEmoji)
            {
                text = textAfter;
            }

            return(text);
        }