/// <summary>Attempts to convert argument to parameter type.</summary>
        /// <param name="parameter">Parameter to convert argument to.</param>
        /// <param name="argIndex">Index of argument.</param>
        /// <param name="values">Builder values. Must contain Args and ArgumentConverterProvider.</param>
        /// <param name="result">Result of the conversion. Null if conversion failed.</param>
        /// <param name="error">Exception that occured when converting. Null if there was no exception.</param>
        /// <returns>True if converting was successful; otherwise false.</returns>
        protected static bool TryConvertArgument(ParameterInfo parameter, int argIndex, ParameterBuilderValues values, out object result, out Exception error)
        {
            if (argIndex < 0)
            {
                throw new ArgumentException("Argument index cannot be negative", nameof(argIndex));
            }
            if (values == null)
            {
                throw new ArgumentNullException(nameof(values));
            }

            error  = null;
            result = null;

            // check if we didn't run out of args
            if (values.Args == null || values.Args.Length - 1 < argIndex)
            {
                return(false);
            }

            // get from provider
            IArgumentConverterProvider provider  = values.ArgumentConverterProvider;
            IArgumentConverter         converter = provider?.GetConverter(parameter);

            if (converter == null)
            {
                return(false);
            }

            try
            {
                result = converter.Convert(parameter, values.Args[argIndex]);
                return(true);
            }
            catch (Exception ex)
            {
                error = ex;
                return(false);
            }
        }
        /// <inheritdoc/>
        public async Task <ParameterBuildingResult> BuildParamsAsync(IEnumerable <ParameterInfo> parameters, ParameterBuilderValues values, CancellationToken cancellationToken = default)
        {
            if (values == null)
            {
                throw new ArgumentNullException(nameof(values));
            }
            if (parameters?.Any() != true)
            {
                return(ParameterBuildingResult.Success(Array.Empty <object>()));
            }

            object[] paramsValues = new object[parameters.Count()];
            int      argIndex     = 0; // parse arg index, so they're handled in order

            foreach (ParameterInfo param in parameters)
            {
                cancellationToken.ThrowIfCancellationRequested();

                // check additionals first, in case they override something
                if (TryFindAdditional(param.ParameterType, values.AdditionalObjects, out object value))
                {
                }
                // from context
                else if (values.Context != null && param.ParameterType.IsAssignableFrom(values.Context.GetType()))
                {
                    value = values.Context;
                }
                else if (values.Context != null && param.ParameterType.IsAssignableFrom(values.Context.Message.GetType()))
                {
                    value = values.Context.Message;
                }
                else if (values.Context != null && param.ParameterType.IsAssignableFrom(values.Context.Client.GetType()))
                {
                    value = values.Context.Client;
                }
                // command instance
                else if (values.CommandInstance != null && param.ParameterType.IsAssignableFrom(values.CommandInstance.GetType()))
                {
                    value = values.CommandInstance;
                }
                // cancellation token
                else if (param.ParameterType.IsAssignableFrom(typeof(CancellationToken)))
                {
                    value = values.CancellationToken;
                }
                // from services
                else if (TryGetService(param.ParameterType, values.Services, out value))
                {
                }
                // logger from factory
                else if (TryGetGenericLogger(param.ParameterType, values.Services, out value))
                {
                }
                // from args
                else
                {
                    // try to convert as arg
                    if (TryConvertArgument(param, argIndex, values, out value, out Exception convertingError))
                    {
                        argIndex++;
                    }
                    // if there's an error, let's return result with message - but without exception, as we don't want input errors to be logged
                    else if (convertingError != null)
                    {
                        return(ParameterBuildingResult.Failure(null, new string[] {
                            await param.GetConvertingErrorAttribute().ToStringAsync(values.Context, values.Args[argIndex], param, cancellationToken).ConfigureAwait(false)
                        }));
                    }
                    // if it's optional, just let it pass
                    else if (param.IsOptional)
                    {
                        value = param.HasDefaultValue ? param.DefaultValue : null;
                    }
                    // if not default and not thrown conversion error, but still not found yet - means it's arg that is expected, but user didn't provide it in command - so return error with message - do not provide exception, as we don't want it logged
                    else if (argIndex <= values.Args.Length)
                    {
                        return(ParameterBuildingResult.Failure(null, new string[] {
                            await param.GetMissingErrorAttribute().ToStringAsync(values.Context,
                                                                                 values.Args.Length > argIndex ? values.Args[argIndex] : string.Empty,
                                                                                 param, cancellationToken).ConfigureAwait(false)
                        }));
                    }
                    // none found, throw
                    else
                    {
                        throw new InvalidOperationException($"Unsupported param type: {param.ParameterType.FullName}");
                    }
                }
                paramsValues[param.Position] = value;
            }

            return(ParameterBuildingResult.Success(paramsValues));
        }