using System;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection;

namespace Swan
{
    /// <summary>
    /// Provides a standard way to convert strings to different types.
    /// </summary>
    public static class FromString
    {
        // It doesn't matter which converter we get here: ConvertFromInvariantString is not virtual.
        private static readonly MethodInfo ConvertFromInvariantStringMethod
            = new Func<string, object>(TypeDescriptor.GetConverter(typeof(int)).ConvertFromInvariantString).Method;

        private static readonly MethodInfo TryConvertToInternalMethod
            = typeof(FromString).GetMethod(nameof(TryConvertToInternal), BindingFlags.Static | BindingFlags.NonPublic);

        private static readonly MethodInfo ConvertToInternalMethod
            = typeof(FromString).GetMethod(nameof(ConvertToInternal), BindingFlags.Static | BindingFlags.NonPublic);

        private static readonly ConcurrentDictionary<Type, Func<string[], (bool Success, object Result)>> GenericTryConvertToMethods
            = new ConcurrentDictionary<Type, Func<string[], (bool Success, object Result)>>();

        private static readonly ConcurrentDictionary<Type, Func<string[], object>> GenericConvertToMethods
            = new ConcurrentDictionary<Type, Func<string[], object>>();

        /// <summary>
        /// Determines whether a string can be converted to the specified type.
        /// </summary>
        /// <param name="type">The type resulting from the conversion.</param>
        /// <returns><see langword="true" /> if the conversion is possible;
        /// otherwise, <see langword="false" />.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="type" /> is <see langword="null" />.</exception>
        public static bool CanConvertTo(Type type)
            => TypeDescriptor.GetConverter(type).CanConvertFrom(typeof(string));

        /// <summary>
        /// Determines whether a string can be converted to the specified type.
        /// </summary>
        /// <typeparam name="TResult">The type resulting from the conversion.</typeparam>
        /// <returns><see langword="true" /> if the conversion is possible;
        /// otherwise, <see langword="false" />.</returns>
        public static bool CanConvertTo<TResult>()
            => TypeDescriptor.GetConverter(typeof(TResult)).CanConvertFrom(typeof(string));

        /// <summary>
        /// Attempts to convert a string to the specified type.
        /// </summary>
        /// <param name="type">The type resulting from the conversion.</param>
        /// <param name="str">The string to convert.</param>
        /// <param name="result">When this method returns <see langword="true" />,
        /// the result of the conversion. This parameter is passed uninitialized.</param>
        /// <returns><see langword="true" /> if the conversion is successful;
        /// otherwise, <see langword="false" />.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="type" /> is <see langword="null" />.</exception>
        public static bool TryConvertTo(Type type, string str, out object? result)
        {
            var converter = TypeDescriptor.GetConverter(type);
            if (!converter.CanConvertFrom(typeof(string)))
            {
                result = null;
                return false;
            }

            try
            {
                result = converter.ConvertFromInvariantString(str);
                return true;
            }
            catch (Exception e) when (!e.IsCriticalException())
            {
                result = null;
                return false;
            }
        }

        /// <summary>
        /// Attempts to convert a string to the specified type.
        /// </summary>
        /// <typeparam name="TResult">The type resulting from the conversion.</typeparam>
        /// <param name="str">The string to convert.</param>
        /// <param name="result">When this method returns <see langword="true" />,
        /// the result of the conversion. This parameter is passed uninitialized.</param>
        /// <returns><see langword="true" /> if the conversion is successful;
        /// otherwise, <see langword="false" />.</returns>
        public static bool TryConvertTo<TResult>(string str, out TResult result)
        {
            var converter = TypeDescriptor.GetConverter(typeof(TResult));
            if (!converter.CanConvertFrom(typeof(string)))
            {
                result = default;
                return false;
            }

            try
            {
                result = (TResult)converter.ConvertFromInvariantString(str);
                return true;
            }
            catch (Exception e) when (!e.IsCriticalException())
            {
                result = default;
                return false;
            }
        }

        /// <summary>
        /// Converts a string to the specified type.
        /// </summary>
        /// <param name="type">The type resulting from the conversion.</param>
        /// <param name="str">The string to convert.</param>
        /// <returns>An instance of <paramref name="type" />.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="type" /> is <see langword="null" />.</exception>
        /// <exception cref="StringConversionException">The conversion was not successful.</exception>
        public static object ConvertTo(Type type, string str)
        {
            if (type == null)
                throw new ArgumentNullException(nameof(type));

            try
            {
                return TypeDescriptor.GetConverter(type).ConvertFromInvariantString(str);
            }
            catch (Exception e) when (!e.IsCriticalException())
            {
                throw new StringConversionException(type, e);
            }
        }

        /// <summary>
        /// Converts a string to the specified type.
        /// </summary>
        /// <typeparam name="TResult">The type resulting from the conversion.</typeparam>
        /// <param name="str">The string to convert.</param>
        /// <returns>An instance of <typeparamref name="TResult" />.</returns>
        /// <exception cref="StringConversionException">
        /// The conversion was not successful.
        /// </exception>
        public static TResult ConvertTo<TResult>(string str)
        {
            try
            {
                return (TResult)TypeDescriptor.GetConverter(typeof(TResult)).ConvertFromInvariantString(str);
            }
            catch (Exception e) when (!e.IsCriticalException())
            {
                throw new StringConversionException(typeof(TResult), e);
            }
        }

        /// <summary>
        /// Attempts to convert an array of strings to an array of the specified type.
        /// </summary>
        /// <param name="type">The type resulting from the conversion of each
        /// element of <paramref name="strings"/>.</param>
        /// <param name="strings">The array to convert.</param>
        /// <param name="result">When this method returns <see langword="true" />,
        /// the result of the conversion. This parameter is passed uninitialized.</param>
        /// <returns><see langword="true" /> if the conversion is successful;
        /// otherwise, <see langword="false" />.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="type" /> is <see langword="null" />.</exception>
        public static bool TryConvertTo(Type type, string[] strings, out object? result)
        {
            if (strings == null)
            {
                result = null;
                return false;
            }

            var method = GenericTryConvertToMethods.GetOrAdd(type, BuildNonGenericTryConvertLambda);
            var (success, methodResult) = method(strings);
            result = methodResult;
            return success;
        }

        /// <summary>
        /// Attempts to convert an array of strings to an array of the specified type.
        /// </summary>
        /// <typeparam name="TResult">The type resulting from the conversion of each
        /// element of <paramref name="strings"/>.</typeparam>
        /// <param name="strings">The array to convert.</param>
        /// <param name="result">When this method returns <see langword="true" />,
        /// the result of the conversion. This parameter is passed uninitialized.</param>
        /// <returns><see langword="true" /> if the conversion is successful;
        /// otherwise, <see langword="false" />.</returns>
        public static bool TryConvertTo<TResult>(string[] strings, out TResult[]? result)
        {
            if (strings == null)
            {
                result = null;
                return false;
            }

            var converter = TypeDescriptor.GetConverter(typeof(TResult));
            if (!converter.CanConvertFrom(typeof(string)))
            {
                result = null;
                return false;
            }

            try
            {
                result = new TResult[strings.Length];
                var i = 0;
                foreach (var str in strings)
                    result[i++] = (TResult)converter.ConvertFromInvariantString(str);

                return true;
            }
            catch (Exception e) when (!e.IsCriticalException())
            {
                result = null;
                return false;
            }
        }

        /// <summary>
        /// Converts an array of strings to an array of the specified type.
        /// </summary>
        /// <param name="type">The type resulting from the conversion of each
        /// element of <paramref name="strings"/>.</param>
        /// <param name="strings">The array to convert.</param>
        /// <returns>An array of <paramref name="type" />.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="type" /> is <see langword="null" />.</exception>
        /// <exception cref="StringConversionException">The conversion of at least one
        /// of the elements of <paramref name="strings"/>was not successful.</exception>
        public static object? ConvertTo(Type type, string[] strings)
        {
            if (strings == null)
                return null;

            var method = GenericConvertToMethods.GetOrAdd(type, BuildNonGenericConvertLambda);
            return method(strings);
        }

        /// <summary>
        /// Converts an array of strings to an array of the specified type.
        /// </summary>
        /// <typeparam name="TResult">The type resulting from the conversion of each
        /// element of <paramref name="strings"/>.</typeparam>
        /// <param name="strings">The array to convert.</param>
        /// <returns>An array of <typeparamref name="TResult" />.</returns>
        /// <exception cref="StringConversionException">The conversion of at least one
        /// of the elements of <paramref name="strings"/>was not successful.</exception>
        public static TResult[]? ConvertTo<TResult>(string[] strings)
        {
            if (strings == null)
                return null;

            var converter = TypeDescriptor.GetConverter(typeof(TResult));
            var result = new TResult[strings.Length];
            var i = 0;
            try
            {
                foreach (var str in strings)
                    result[i++] = (TResult)converter.ConvertFromInvariantString(str);
            }
            catch (Exception e) when (!e.IsCriticalException())
            {
                throw new StringConversionException(typeof(TResult), e);
            }

            return result;
        }

        /// <summary>
        /// Converts a expression, if the type can be converted to string, to a new expression including
        /// the conversion to string.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="str">The string.</param>
        /// <returns>A new expression where the previous expression is converted to string.</returns>
        public static Expression? ConvertExpressionTo(Type type, Expression str)
        {
            var converter = TypeDescriptor.GetConverter(type);

            return converter.CanConvertFrom(typeof(string))
                ? Expression.Convert(
                    Expression.Call(Expression.Constant(converter), ConvertFromInvariantStringMethod, str),
                    type)
                : null;
        }

        private static Func<string[], (bool Success, object Result)> BuildNonGenericTryConvertLambda(Type type)
        {
            var methodInfo = TryConvertToInternalMethod.MakeGenericMethod(type);
            var parameter = Expression.Parameter(typeof(string[]));
            var body = Expression.Call(methodInfo, parameter);
            var lambda = Expression.Lambda<Func<string[], (bool Success, object Result)>>(body, parameter);
            return lambda.Compile();
        }

        private static (bool Success, object? Result) TryConvertToInternal<TResult>(string[] strings)
        {
            var converter = TypeDescriptor.GetConverter(typeof(TResult));
            if (!converter.CanConvertFrom(typeof(string)))
                return (false, null);

            var result = new TResult[strings.Length];
            var i = 0;

            try
            {
                foreach (var str in strings)
                    result[i++] = (TResult)converter.ConvertFromInvariantString(str);

                return (true, result);
            }
            catch (Exception e) when (!e.IsCriticalException())
            {
                return (false, null);
            }
        }

        private static Func<string[], object> BuildNonGenericConvertLambda(Type type)
        {
            var methodInfo = ConvertToInternalMethod.MakeGenericMethod(type);
            var parameter = Expression.Parameter(typeof(string[]));
            var body = Expression.Call(methodInfo, parameter);
            var lambda = Expression.Lambda<Func<string[], object>>(body, parameter);

            return lambda.Compile();
        }

        private static object ConvertToInternal<TResult>(string[] strings)
        {
            var converter = TypeDescriptor.GetConverter(typeof(TResult));
            var result = new TResult[strings.Length];
            var i = 0;

            try
            {
                foreach (var str in strings)
                    result[i++] = (TResult)converter.ConvertFromInvariantString(str);

                return result;
            }
            catch (Exception e) when (!e.IsCriticalException())
            {
                throw new StringConversionException(typeof(TResult), e);
            }
        }
    }
}
