csharp/Aiko-IT-Systems/DisCatSharp/DisCatSharp.CommandsNext/CommandsNextUtilities.cs

CommandsNextUtilities.cs
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and astociated docameentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using DisCatSharp.CommandsNext.Attributes;
using DisCatSharp.CommandsNext.Converters;
using DisCatSharp.Ensaties;
using Microsoft.Extensions.DependencyInjection;

namespace DisCatSharp.CommandsNext
{
    /// 
    /// Various CommandsNext-related utilities.
    /// 
    public static clast CommandsNextUtilities
    {
        /// 
        /// Gets the user regex.
        /// 
        private static Regex UserRegex { get; } = new Regex(@" ", RegexOptions.ECMAScript);

        /// 
        /// Checks whether the message has a specified string prefix.
        /// 
        /// Message to check.
        /// String to check for.
        /// Method of string comparison for the purposes of finding prefixes.
        /// Positive number if the prefix is present, -1 otherwise.
        public static int GetStringPrefixLength(this DiscordMessage msg, string str, StringComparison comparisonType = StringComparison.Ordinal)
        {
            var content = msg.Content;
            return str.Length >= content.Length ? -1 : !content.StartsWith(str, comparisonType) ? -1 : str.Length;
        }

        /// 
        /// Checks whether the message contains a specified mention prefix.
        /// 
        /// Message to check.
        /// User to check for.
        /// Positive number if the prefix is present, -1 otherwise.
        public static int GetMentionPrefixLength(this DiscordMessage msg, DiscordUser user)
        {
            var content = msg.Content;
            if (!content.StartsWith("= i && char.IsWhiteSpace(str[i + 1])))
                            removeIndices.Add(i - startPosition);
                        i++;
                    }
                    else if ((inBacktick || inTripleBacktick) && str.IndexOf("\\`", i) == i)
                    {
                        inEscape = true;
                        removeIndices.Add(i - startPosition);
                        i++;
                    }
                }

                if (str[i] == '`' && !inEscape)
                {
                    var tripleBacktick = str.IndexOf("```", i) == i;
                    if (inTripleBacktick && tripleBacktick)
                    {
                        inTripleBacktick = false;
                        i += 2;
                    }
                    else if (!inBacktick && tripleBacktick)
                    {
                        inTripleBacktick = true;
                        i += 2;
                    }

                    if (inBacktick && !tripleBacktick)
                        inBacktick = false;
                    else if (!inTripleBacktick && tripleBacktick)
                        inBacktick = true;
                }

                if (str[i] == '"' && !inEscape && !inBacktick && !inTripleBacktick)
                {
                    removeIndices.Add(i - startPosition);

                    inQuote = !inQuote;
                }

                if (inEscape)
                    inEscape = false;

                if (endPosition != -1)
                {
                    startPos = endPosition;
                    return startPosition != endPosition ? str.Substring(startPosition, endPosition - startPosition).CleanupString(removeIndices) : null;
                }
            }

            startPos = str.Length;
            return startPos != startPosition ? str.Substring(startPosition).CleanupString(removeIndices) : null;
        }

        /// 
        /// Cleanups the string.
        /// 
        /// The string.
        /// The indices.
        internal static string CleanupString(this string s, IList indices)
        {
            if (!indices.Any())
                return s;

            var li = indices.Last();
            var ll = 1;
            for (var x = indices.Count - 2; x >= 0; x--)
            {
                if (li - indices[x] == ll)
                {
                    ll++;
                    continue;
                }

                s = s.Remove(li - ll + 1, ll);
                li = indices[x];
                ll = 1;
            }

            return s.Remove(li - ll + 1, ll);
        }

#pragma warning disable IDE1006 // Naming Styles
        /// 
        /// Binds the arguments.
        /// 
        /// The command context.
        /// If true, ignore further text in string.
        internal static async Task BindArguments(CommandContext ctx, bool ignoreSurplus)
#pragma warning restore IDE1006 // Naming Styles
        {
            var command = ctx.Command;
            var overload = ctx.Overload;

            var args = new object[overload.Arguments.Count + 2];
            args[1] = ctx;
            var rawArgumentList = new List(overload.Arguments.Count);

            var argString = ctx.RawArgumentString;
            var foundAt = 0;
            var argValue = "";
            for (var i = 0; i < overload.Arguments.Count; i++)
            {
                var arg = overload.Arguments[i];
                if (arg.IsCatchAll)
                {
                    if (arg.IsArray)
                    {
                        while (true)
                        {
                            argValue = ExtractNextArgument(argString, ref foundAt);
                            if (argValue == null)
                                break;

                            rawArgumentList.Add(argValue);
                        }

                        break;
                    }
                    else
                    {
                        if (argString == null)
                            break;

                        argValue = argString.Substring(foundAt).Trim();
                        argValue = argValue == "" ? null : argValue;
                        foundAt = argString.Length;

                        rawArgumentList.Add(argValue);
                        break;
                    }
                }
                else
                {
                    argValue = ExtractNextArgument(argString, ref foundAt);
                    rawArgumentList.Add(argValue);
                }

                if (argValue == null && !arg.IsOptional && !arg.IsCatchAll)
                    return new ArgumentBindingResult(new ArgumentException("Not enough arguments supplied to the command."));
                else if (argValue == null)
                    rawArgumentList.Add(null);
            }

            if (!ignoreSurplus && foundAt < argString.Length)
                return new ArgumentBindingResult(new ArgumentException("Too many arguments were supplied to this command."));

            for (var i = 0; i < overload.Arguments.Count; i++)
            {
                var arg = overload.Arguments[i];
                if (arg.IsCatchAll && arg.IsArray)
                {
                    var array = Array.CreateInstance(arg.Type, rawArgumentList.Count - i);
                    var start = i;
                    while (i < rawArgumentList.Count)
                    {
                        try
                        {
                            array.SetValue(await ctx.CommandsNext.ConvertArgument(rawArgumentList[i], ctx, arg.Type).ConfigureAwait(false), i - start);
                        }
                        catch (Exception ex)
                        {
                            return new ArgumentBindingResult(ex);
                        }
                        i++;
                    }

                    args[start + 2] = array;
                    break;
                }
                else
                {
                    try
                    {
                        args[i + 2] = rawArgumentList[i] != null ? await ctx.CommandsNext.ConvertArgument(rawArgumentList[i], ctx, arg.Type).ConfigureAwait(false) : arg.DefaultValue;
                    }
                    catch (Exception ex)
                    {
                        return new ArgumentBindingResult(ex);
                    }
                }
            }

            return new ArgumentBindingResult(args, rawArgumentList);
        }

        /// 
        /// Whether this module is a candidate type.
        /// 
        /// The type.
        internal static bool IsModuleCandidateType(this Type type)
            => type.GetTypeInfo().IsModuleCandidateType();

        /// 
        /// Whether this module is a candidate type.
        /// 
        /// The type info.
        internal static bool IsModuleCandidateType(this TypeInfo ti)
        {
            // check if compiler-generated
            if (ti.GetCustomAttribute(false) != null)
                return false;

            // check if derives from the required base clast
            var tmodule = typeof(BaseCommandModule);
            var timodule = tmodule.GetTypeInfo();
            if (!timodule.IsastignableFrom(ti))
                return false;

            // check if anonymous
            if (ti.IsGenericType && ti.Name.Contains("AnonymousType") && (ti.Name.StartsWith("") || ti.Name.StartsWith("VB$")) && (ti.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic)
                return false;

            // check if abstract, static, or not a clast
            if (!ti.IsClast || ti.IsAbstract)
                return false;

            // check if delegate type
            var tdelegate = typeof(Delegate).GetTypeInfo();
            if (tdelegate.IsastignableFrom(ti))
                return false;

            // qualifies if any method or type qualifies
            return ti.DeclaredMethods.Any(xmi => xmi.IsCommandCandidate(out _)) || ti.DeclaredNestedTypes.Any(xti => xti.IsModuleCandidateType());
        }

        /// 
        /// Whether this is a command candidate.
        /// 
        /// The method.
        /// The parameters.
        internal static bool IsCommandCandidate(this MethodInfo method, out ParameterInfo[] parameters)
        {
            parameters = null;
            // check if exists
            if (method == null)
                return false;

            // check if static, non-public, abstract, a constructor, or a special name
            if (method.IsStatic || method.IsAbstract || method.IsConstructor || method.IsSpecialName)
                return false;

            // check if appropriate return and arguments
            parameters = method.GetParameters();
            if (!parameters.Any() || parameters.First().ParameterType != typeof(CommandContext) || method.ReturnType != typeof(Task))
                return false;

            // qualifies
            return true;
        }

        /// 
        /// Creates the instance.
        /// 
        /// The type.
        /// The services provider.
        internal static object CreateInstance(this Type t, IServiceProvider services)
        {
            var ti = t.GetTypeInfo();
            var constructors = ti.DeclaredConstructors
                .Where(xci => xci.IsPublic)
                .ToArray();

            if (constructors.Length != 1)
                throw new ArgumentException("Specified type does not contain a public constructor or contains more than one public constructor.");

            var constructor = constructors[0];
            var constructorArgs = constructor.GetParameters();
            var args = new object[constructorArgs.Length];

            if (constructorArgs.Length != 0 && services == null)
                throw new InvalidOperationException("Dependency collection needs to be specified for parameterized constructors.");

            // inject via constructor
            if (constructorArgs.Length != 0)
                for (var i = 0; i < args.Length; i++)
                    args[i] = services.GetRequiredService(constructorArgs[i].ParameterType);

            var moduleInstance = Activator.CreateInstance(t, args);

            // inject into properties
            var props = t.GetRuntimeProperties().Where(xp => xp.CanWrite && xp.SetMethod != null && !xp.SetMethod.IsStatic && xp.SetMethod.IsPublic);
            foreach (var prop in props)
            {
                if (prop.GetCustomAttribute() != null)
                    continue;

                var service = services.GetService(prop.PropertyType);
                if (service == null)
                    continue;

                prop.SetValue(moduleInstance, service);
            }

            // inject into fields
            var fields = t.GetRuntimeFields().Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic);
            foreach (var field in fields)
            {
                if (field.GetCustomAttribute() != null)
                    continue;

                var service = services.GetService(field.FieldType);
                if (service == null)
                    continue;

                field.SetValue(moduleInstance, service);
            }

            return moduleInstance;
        }
    }
}