/// <summary>
 /// User command to see information on how to update projects.
 /// </summary>
 public void CMD_Update(string[] cmds, SocketMessage message)
 {
     if (cmds.Length == 0)
     {
         if (!Bot.ChannelToDetails.TryGetValue(message.Channel.Id, out ChannelDetails details) || details.Updates.Length == 0)
         {
             SendErrorMessageReply(message, "Unknown input for Update command", "Please specify which project(s) you want the update link for.");
             return;
         }
         foreach (ProjectDetails proj in details.Updates)
         {
             SendReply(message, proj.GetUpdateEmbed());
         }
         return;
     }
     foreach (string projectName in cmds)
     {
         string projectNameLower = projectName.ToLowerFast();
         if (Bot.ProjectToDetails.TryGetValue(projectNameLower, out ProjectDetails detail))
         {
             SendReply(message, detail.GetUpdateEmbed());
         }
         else
         {
             string closeName = StringConversionHelper.FindClosestString(Bot.ProjectToDetails.Keys, projectName, 20);
             SendErrorMessageReply(message, "Unknown project name for Update command", $"Unknown project name `{projectName.Replace('`', '\'')}`."
                                   + (closeName == null ? "" : $" Did you mean `{closeName}`?"));
         }
     }
 }
 /// <summary>
 /// User command to see a link to the GitHub.
 /// </summary>
 public void CMD_Issues(string[] cmds, IUserMessage message)
 {
     if (DenizenMetaBot.ProjectToDetails.IsEmpty())
     {
         return;
     }
     if (cmds.Length == 0)
     {
         if (!DenizenMetaBot.ChannelToDetails.TryGetValue(message.Channel.Id, out ChannelDetails details) || details.Updates.Length == 0)
         {
             SendErrorMessageReply(message, "Unknown input for Issues command", $"Please specify which project(s) you want the Issues link for, like `{DenizenMetaBotConstants.COMMAND_PREFIX}issues denizen`.");
             return;
         }
         SendReply(message, details.Updates[0].GetIssuesEmbed());
         return;
     }
     foreach (string projectName in cmds)
     {
         string projectNameLower = projectName.ToLowerFast();
         if (DenizenMetaBot.ProjectToDetails.TryGetValue(projectNameLower, out ProjectDetails detail))
         {
             SendReply(message, detail.GetIssuesEmbed());
         }
         else
         {
             string closeName = StringConversionHelper.FindClosestString(DenizenMetaBot.ProjectToDetails.Keys, projectName, 20);
             SendErrorMessageReply(message, "Unknown project name for Issues command", $"Unknown project name `{projectName.Replace('`', '\'')}`."
                                   + (closeName == null ? "" : $" Did you mean `{closeName}`?"));
         }
     }
 }
 /// <summary>
 /// User command to get some predefined informational output.
 /// </summary>
 public void CMD_Info(string[] cmds, IUserMessage message)
 {
     if (DenizenMetaBot.InformationalData.IsEmpty())
     {
         return;
     }
     if (cmds.Length == 0)
     {
         SendErrorMessageReply(message, "Command Syntax Incorrect", "`!info <info item or 'list'>`");
         return;
     }
     if (cmds.Length > 5)
     {
         SendErrorMessageReply(message, "Command Syntax Incorrect", "Please request no more than 5 info items at a time.");
         return;
     }
     foreach (string searchRaw in cmds)
     {
         string commandSearch = searchRaw.ToLowerFast().Trim();
         if (commandSearch == "list" || commandSearch == "all")
         {
             string fullList = "`" + string.Join("`, `", DenizenMetaBot.InformationalDataNames) + "`";
             SendReply(message, new EmbedBuilder().WithThumbnailUrl(Constants.INFO_ICON).WithTitle("Available Info Names").WithDescription($"Available info names: {fullList}").Build());
         }
         else if (DenizenMetaBot.InformationalData.TryGetValue(commandSearch, out string infoOutput))
         {
             infoOutput = infoOutput.Trim();
             if (infoOutput.StartsWith("NO_BOX:"))
             {
                 infoOutput = infoOutput.Substring("NO_BOX:".Length).Trim();
                 message.Channel.SendMessageAsync("+++ Info `" + commandSearch + "`: " + infoOutput);
             }
             else
             {
                 SendReply(message, new EmbedBuilder().WithThumbnailUrl(Constants.INFO_ICON).WithTitle($"Info: {commandSearch}").WithDescription(infoOutput).Build());
             }
         }
         else
         {
             string closeName = StringConversionHelper.FindClosestString(DenizenMetaBot.InformationalData.Keys, commandSearch, 20);
             SendErrorMessageReply(message, "Cannot Display Info", "Unknown info name." + (closeName == null ? "" : $" Did you mean `{closeName}`?"));
         }
     }
 }
 /// <summary>User command to see information on how to update projects.</summary>
 public void CMD_Update(CommandData command)
 {
     if (DenizenMetaBot.ProjectToDetails.IsEmpty())
     {
         return;
     }
     if (command.CleanedArguments.Length == 0)
     {
         ulong chanId = command.Message.Channel is SocketThreadChannel thread ? thread.ParentChannel.Id : command.Message.Channel.Id;
         if (!DenizenMetaBot.ChannelToDetails.TryGetValue(chanId, out ChannelDetails details) || details.Updates.Length == 0)
         {
             SendErrorMessageReply(command.Message, "Unknown input for Update command", $"Please specify which project(s) you want the update link for, like `{DenizenMetaBotConstants.COMMAND_PREFIX}update denizen`.");
             return;
         }
         foreach (ProjectDetails proj in details.Updates)
         {
             SendReply(command.Message, proj.GetUpdateEmbed());
         }
         return;
     }
     if (command.CleanedArguments.Length > 5)
     {
         SendErrorMessageReply(command.Message, "Command Syntax Incorrect", "Please request no more than 5 info items at a time.");
         return;
     }
     foreach (string projectName in command.CleanedArguments)
     {
         string projectNameLower = projectName.ToLowerFast();
         if (DenizenMetaBot.ProjectToDetails.TryGetValue(projectNameLower, out ProjectDetails detail))
         {
             SendReply(command.Message, detail.GetUpdateEmbed());
         }
         else
         {
             string closeName = StringConversionHelper.FindClosestString(DenizenMetaBot.ProjectToDetails.Keys, projectName, 20);
             SendErrorMessageReply(command.Message, "Unknown project name for Update command", $"Unknown project name `{projectName.Replace('`', '\'')}`."
                                   + (closeName == null ? "" : $" Did you mean `{closeName}`?"));
         }
     }
 }
        /// <summary>
        /// Mechanism meta docs user command.
        /// </summary>
        public void CMD_Mechanism(string[] cmds, SocketMessage message)
        {
            List <string> secondarySearches = new List <string>();

            if (cmds.Length > 0)
            {
                int dotIndex = cmds[0].IndexOf('.');
                if (dotIndex > 0)
                {
                    secondarySearches.Add(cmds[0].Substring(0, dotIndex) + "tag" + cmds[0].Substring(dotIndex));
                }
            }
            int closeness = AutoMetaCommand(Program.CurrentMeta.Mechanisms, MetaDocs.META_TYPE_MECHANISM, cmds, message, secondarySearches);

            if (closeness > 0)
            {
                string closeCmd = StringConversionHelper.FindClosestString(Program.CurrentMeta.Commands.Keys, cmds[0].ToLowerFast(), 7);
                if (closeCmd != null)
                {
                    SendGenericPositiveMessageReply(message, "Possible Confusion", $"Did you mean to search for `command {closeCmd}`?");
                }
            }
        }
        /// <summary>
        /// Mechanism meta docs user command.
        /// </summary>
        public void CMD_Mechanism(string[] cmds, IUserMessage message)
        {
            List <string> secondarySearches = new List <string>();

            if (cmds.Length > 0)
            {
                int dotIndex = cmds[0].IndexOf('.');
                if (dotIndex > 0)
                {
                    secondarySearches.Add(cmds[0].Substring(0, dotIndex) + "tag" + cmds[0].Substring(dotIndex));
                }
            }
            int closeness = AutoMetaCommand(MetaDocs.CurrentMeta.Mechanisms, MetaDocs.META_TYPE_MECHANISM, cmds, message, secondarySearches);

            if (closeness > 0)
            {
                string closeCmd = StringConversionHelper.FindClosestString(MetaDocs.CurrentMeta.Commands.Keys, cmds[0].ToLowerFast(), 7);
                if (closeCmd != null)
                {
                    SendDidYouMeanReply(message, "Possible Confusion", $"Did you mean to search for `command {closeCmd}`?", $"{DenizenMetaBotConstants.COMMAND_PREFIX}command {closeCmd}");
                }
            }
        }
        /// <summary>
        /// Command meta docs user command.
        /// </summary>
        public void CMD_Command(string[] cmds, SocketMessage message)
        {
            void singleReply(MetaCommand cmd)
            {
                if (cmds.Length >= 2)
                {
                    string outputType = cmds[1].ToLowerFast();
                    if (outputType.StartsWith("u"))
                    {
                        SendReply(message, cmd.GetUsagesEmbed().Build());
                    }
                    else if (outputType.StartsWith("t"))
                    {
                        SendReply(message, cmd.GetTagsEmbed().Build());
                    }
                    else
                    {
                        SendErrorMessageReply(message, "Bad Command Syntax", "Second argument is unknown.\n\nUsage: `command [name] [usage/tags]`.");
                    }
                }
                else
                {
                    SendReply(message, cmd.GetEmbed().Build());
                }
            }

            int closeness = AutoMetaCommand(Program.CurrentMeta.Commands, MetaDocs.META_TYPE_COMMAND, cmds, message, altSingleOutput: singleReply);

            if (closeness > 0)
            {
                string closeMech = StringConversionHelper.FindClosestString(Program.CurrentMeta.Mechanisms.Keys.Select(s => s.After('.')), cmds[0].ToLowerFast(), 10);
                if (closeMech != null)
                {
                    SendGenericPositiveMessageReply(message, "Possible Confusion", $"Did you mean to search for `mechanism {closeMech}`?");
                }
            }
        }
        /// <summary>
        /// Automatically processes a meta search command.
        /// </summary>
        /// <typeparam name="T">The meta object type.</typeparam>
        /// <param name="docs">The docs mapping.</param>
        /// <param name="type">The meta type.</param>
        /// <param name="cmds">The command args.</param>
        /// <param name="message">The Discord message object.</param>
        /// <param name="secondarySearches">A list of secondary search strings if the first fails.</param>
        /// <param name="secondaryMatcher">A secondary matching function if needed.</param>
        /// <param name="altSingleOutput">An alternate method of processing the single-item-result.</param>
        /// <param name="altFindClosest">Alternate method to find the closest result.</param>
        /// <returns>How close of an answer was gotten (0 = perfect, -1 = no match needed, 1000 = none).</returns>
        public int AutoMetaCommand <T>(Dictionary <string, T> docs, MetaType type, string[] cmds, SocketMessage message,
                                       List <string> secondarySearches = null, Func <T, bool> secondaryMatcher            = null, Action <T> altSingleOutput = null,
                                       Func <string> altFindClosest    = null, Func <List <T>, List <T> > altMatchOrderer = null) where T : MetaObject
        {
            if (CheckMetaDenied(message))
            {
                return(-1);
            }
            if (cmds.Length == 0)
            {
                SendErrorMessageReply(message, $"Need input for '{type.Name}' command",
                                      $"Please specify a {type.Name} to search, like `!{type.Name} Some{type.Name}Here`. Or, use `!{type.Name} all` to view all documented {type.Name.ToLowerFast()}s.");
                return(-1);
            }
            string search = cmds[0].ToLowerFast();

            if (search == "all")
            {
                SendGenericPositiveMessageReply(message, $"All {type.Name}s", $"Find all {type.Name}s at {Constants.DOCS_URL_BASE}{type.WebPath}/");
                return(-1);
            }
            if (altSingleOutput == null)
            {
                altSingleOutput = (singleObj) => SendReply(message, singleObj.GetEmbed().Build());
            }
            if (altFindClosest == null)
            {
                altFindClosest = () =>
                {
                    string initialPossibleResult = StringConversionHelper.FindClosestString(docs.Keys, search, out int lowestDistance, 20);
                    string lowestStr             = null;
                    foreach (string possibleName in docs.Values.Where(o => o.HasMultipleNames).SelectMany(o => o.MultiNames))
                    {
                        int currentDistance = StringConversionHelper.GetLevenshteinDistance(search, possibleName);
                        if (currentDistance < lowestDistance)
                        {
                            lowestDistance = currentDistance;
                            lowestStr      = possibleName;
                        }
                    }
                    return(lowestStr);
                };
            }
            if (altMatchOrderer == null)
            {
                altMatchOrderer = (list) => list.OrderBy((mat) => StringConversionHelper.GetLevenshteinDistance(search, mat.CleanName)).ToList();
            }
            if (docs.TryGetValue(search, out T obj))
            {
                string multiNameData = string.Join("', '", obj.MultiNames);
                Console.WriteLine($"Meta-Command for '{type.Name}' found perfect match for search '{search}': '{obj.CleanName}', multi={obj.HasMultipleNames}='{multiNameData}'");
                altSingleOutput(obj);
                return(0);
            }
            if (secondarySearches != null)
            {
                secondarySearches = secondarySearches.Select(s => s.ToLowerFast()).ToList();
                foreach (string secondSearch in secondarySearches)
                {
                    if (docs.TryGetValue(secondSearch, out obj))
                    {
                        Console.WriteLine($"Meta-Command for '{type.Name}' found perfect match for secondary search '{secondSearch}': '{obj.CleanName}', multi={obj.HasMultipleNames}");
                        altSingleOutput(obj);
                        return(0);
                    }
                }
            }
            List <T> matched       = new List <T>();
            List <T> strongMatched = new List <T>();

            int tryProcesSingleMatch(T objVal, string objName, int min)
            {
                if (objName.Contains(search))
                {
                    Console.WriteLine($"Meta-Command for '{type.Name}' found a strong match (main contains) for search '{search}': '{objName}'");
                    strongMatched.Add(objVal);
                    return(2);
                }
                if (secondarySearches != null)
                {
                    foreach (string secondSearch in secondarySearches)
                    {
                        if (objName.Contains(secondSearch))
                        {
                            Console.WriteLine($"Meta-Command for '{type.Name}' found a strong match (secondary contains) for search '{secondSearch}': '{objName}'");
                            strongMatched.Add(objVal);
                            return(2);
                        }
                    }
                }
                if (min < 1 && secondaryMatcher != null && secondaryMatcher(objVal))
                {
                    Console.WriteLine($"Meta-Command for '{type.Name}' found a weak match (secondaryMatcher) for search '{search}': '{objName}'");
                    matched.Add(objVal);
                    return(1);
                }
                return(min);
            }

            foreach (KeyValuePair <string, T> objPair in docs)
            {
                if (objPair.Value.HasMultipleNames)
                {
                    int matchQuality = 0;
                    foreach (string name in objPair.Value.MultiNames)
                    {
                        matchQuality = tryProcesSingleMatch(objPair.Value, name, matchQuality);
                        if (matchQuality == 2)
                        {
                            break;
                        }
                    }
                }
                else
                {
                    tryProcesSingleMatch(objPair.Value, objPair.Key, 0);
                }
            }
            if (strongMatched.Count > 0)
            {
                matched = strongMatched;
            }
            if (matched.Count == 0)
            {
                string closeName = altFindClosest();
                SendErrorMessageReply(message, $"Cannot Find Searched {type.Name}", $"Unknown {type.Name.ToLowerFast()}." + (closeName == null ? "" : $" Did you mean `{closeName}`?"));
                return(closeName == null ? 1000 : StringConversionHelper.GetLevenshteinDistance(search, closeName));
            }
            else if (matched.Count > 1)
            {
                matched = altMatchOrderer(matched);
                string suffix = ".";
                if (matched.Count > 20)
                {
                    matched = matched.GetRange(0, 20);
                    suffix  = ", ...";
                }
                string listText = string.Join("`, `", matched.Select((m) => m.Name));
                SendErrorMessageReply(message, $"Cannot Specify Searched {type.Name}", $"Multiple possible {type.Name.ToLowerFast()}s: `{listText}`{suffix}");
                return(StringConversionHelper.GetLevenshteinDistance(search, matched[0].CleanName));
            }
            else // Count == 1
            {
                obj = matched[0];
                Console.WriteLine($"Meta-Command for '{type.Name}' found imperfect single match for search '{search}': '{obj.CleanName}', multi={obj.HasMultipleNames}");
                altSingleOutput(obj);
                return(0);
            }
        }
        /// <summary>
        /// Meta docs total search command.
        /// </summary>
        public void CMD_Search(string[] cmds, IUserMessage message)
        {
            if (CheckMetaDenied(message))
            {
                return;
            }
            if (cmds.Length == 0)
            {
                SendErrorMessageReply(message, "Need input for Search command", "Please specify some text to search, like `!search someobjecthere`.");
                return;
            }
            for (int i = 0; i < cmds.Length; i++)
            {
                cmds[i] = cmds[i].ToLowerFast();
            }
            string            fullSearch         = string.Join(' ', cmds);
            List <MetaObject> strongMatch        = new List <MetaObject>();
            List <MetaObject> partialStrongMatch = new List <MetaObject>();
            List <MetaObject> weakMatch          = new List <MetaObject>();
            List <MetaObject> partialWeakMatch   = new List <MetaObject>();

            foreach (MetaObject obj in MetaDocs.CurrentMeta.AllMetaObjects())
            {
                if (obj.CleanName.Contains(fullSearch))
                {
                    strongMatch.Add(obj);
                    continue;
                }
                foreach (string word in cmds)
                {
                    if (obj.CleanName.Contains(word))
                    {
                        partialStrongMatch.Add(obj);
                        goto fullContinue;
                    }
                }
                if (obj.Searchable.Contains(fullSearch))
                {
                    weakMatch.Add(obj);
                    continue;
                }
                if (fullSearch.Contains(obj.CleanName))
                {
                    partialWeakMatch.Add(obj);
                    continue;
                }
                foreach (string word in cmds)
                {
                    if (obj.Searchable.Contains(word))
                    {
                        partialWeakMatch.Add(obj);
                        goto fullContinue;
                    }
                }
fullContinue:
                continue;
            }
            void backupMatchCheck()
            {
                string possible = StringConversionHelper.FindClosestString(MetaDocs.CurrentMeta.AllMetaObjects().SelectMany(obj => obj.MultiNames), fullSearch, 10);

                if (!string.IsNullOrWhiteSpace(possible))
                {
                    SendDidYouMeanReply(message, "Possible Confusion", $"Did you mean to search for `{possible}`?", $"{DenizenMetaBotConstants.COMMAND_PREFIX}search {possible}");
                }
            }

            if (strongMatch.IsEmpty() && partialStrongMatch.IsEmpty() && weakMatch.IsEmpty() && partialWeakMatch.IsEmpty())
            {
                SendErrorMessageReply(message, "Search Command Has No Results", "Input search text could not be found.");
                backupMatchCheck();
                return;
            }
            string suffix = ".";

            void listWrangle(string typeShort, string typeLong, List <MetaObject> objs)
            {
                objs   = objs.OrderBy((obj) => StringConversionHelper.GetLevenshteinDistance(fullSearch, obj.CleanName)).ToList();
                suffix = ".";
                if (objs.Count > 20)
                {
                    objs   = objs.GetRange(0, 20);
                    suffix = ", ...";
                }
                string listText = string.Join("`, `", objs.Select((obj) => $"{DenizenMetaBotConstants.COMMAND_PREFIX}{obj.Type.Name} {obj.CleanName}"));

                SendGenericPositiveMessageReply(message, $"{typeShort} Search Results", $"{typeShort} ({typeLong}) search results: `{listText}`{suffix}");
            }

            if (strongMatch.Any())
            {
                listWrangle("Best", "very close", strongMatch);
            }
            if (partialStrongMatch.Any())
            {
                listWrangle("Probable", "close but imperfect", partialStrongMatch);
                if (strongMatch.Any())
                {
                    return;
                }
            }
            if (weakMatch.Any())
            {
                listWrangle("Possible", "might be related", weakMatch);
                if (strongMatch.Any() || partialStrongMatch.Any())
                {
                    return;
                }
            }
            if (partialWeakMatch.Any())
            {
                listWrangle("Weak", "if nothing else, some chance of being related", partialWeakMatch);
                if (!weakMatch.Any() && !partialStrongMatch.Any() && !strongMatch.Any())
                {
                    backupMatchCheck();
                }
            }
        }