#pragma warning disable IDE0046 // Convert to conditional expression

using System;
using System.Globalization;

namespace Volpe.Cafe.Utils
{
    /// <summary>
    /// Provides helper and extension methods for parsing numeric values.
    /// </summary>
    [Serializable]
    public static class NumHelper
    {
        #region <<< IsNumeric/IsInteger methods >>>

        /// <summary>
        /// Determines whether the specified object is an integer or can be converted to an integer.
        /// </summary>
        /// <param name="value">The value to check.</param>
        /// <returns><c>true</c>, if the specified object is an integer; <c>false</c>, otherwise.</returns>
        /// <remarks>
        /// Any object up to a 64-bit integer, or can be parsed as such, is treated as an integer.
        /// </remarks>
        public static bool IsInteger(this object value)
        {
            if (IsIntegerType(value)) {
                return true;
            }

            return (value != null) && IsInteger(value.ToString());
        }

        /// <inheritdoc cref="IsInteger(object)"/>
        public static bool IsInteger(this string value)
        {
            return long.TryParse(value, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out _);
        }

        /// <summary>
        /// Determines whether the specified object is a numeric value or can be converted to a numeric value. 
        /// </summary>
        /// <param name="value">The value to check.</param>
        /// <returns><c>true</c>, if the specified object is a numeric value; <c>false</c>, otherwise.</returns>
        /// <remarks>
        /// Any object up to a 64-bit integer or up to a double-precision floating point number, or can be parsed as
        /// such, is treated as numeric. Additionally, the object is considered numeric if it is any of the following
        /// strings (case-insensitive): "NaN", "-∞", "-Inf", "-Infinity", "∞", "Inf", or "Infinity".
        /// </remarks>
        public static bool IsNumeric(this object value)
        {
            if (IsNumericType(value)) {
                return true;
            }

            return (value != null) && IsNumeric(value.ToString());
        }

        /// <inheritdoc cref="IsNumeric(object)"/>
        public static bool IsNumeric(this string value)
        {
            return IsNanOrInfinity(value, out _)
                || double.TryParse(value, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out _);
        }

        private static bool IsIntegerType(object value)
        {
            return value is sbyte || value is byte
                || value is short || value is ushort
                || value is int   || value is uint
                || value is long  || value is ulong;
        }

        private static bool IsNumericType(object value)
        {
            return value is double
                || value is float
                || IsIntegerType(value);
        }

        #endregion

        #region <<< Integer conversion >>>

        /// <summary>
        /// Converts the specified value to its <see cref="sbyte"/> equivalent.
        /// </summary>
        /// <param name="value">The value to convert.</param>
        /// <param name="defaultValue">
        ///     The default value to return if the specified value could not be converted.
        /// </param>
        /// <returns>The <see cref="sbyte"/> equivalent of the specified value.</returns>
        public static sbyte GetSByte(this object value, sbyte defaultValue = 0)
        {
            return (value is sbyte num) ? num
                 : (value == null) ? defaultValue
                 : GetSByte(value.ToString(), defaultValue);
        }
        /// <inheritdoc cref="GetSByte(object, sbyte)"/>
        public static sbyte GetSByte(this string value, sbyte defaultValue = 0)
        {
            if (sbyte.TryParse(value, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out sbyte result)) {
                return result;
            }
            return defaultValue;
        }

        /// <summary>
        /// Converts the specified value to its <see cref="byte"/> equivalent.
        /// </summary>
        /// <param name="value">The value to convert.</param>
        /// <param name="defaultValue">
        ///     The default value to return if the specified value could not be converted.
        /// </param>
        /// <returns>The <see cref="byte"/> equivalent of the specified value.</returns>
        public static byte GetByte(this object value, byte defaultValue = 0)
        {
            return (value is byte num) ? num
                 : (value == null) ? defaultValue
                 : GetByte(value.ToString(), defaultValue);
        }
        /// <inheritdoc cref="GetByte(object, byte)"/>
        public static byte GetByte(this string value, byte defaultValue = 0)
        {
            if (byte.TryParse(value, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out byte result)) {
                return result;
            }
            return defaultValue;
        }

        /// <summary>
        /// Converts the specified value to its <see cref="short"/> equivalent.
        /// </summary>
        /// <param name="value">The value to convert.</param>
        /// <param name="defaultValue">
        ///     The default value to return if the specified value could not be converted.
        /// </param>
        /// <returns>The <see cref="short"/> equivalent of the specified value.</returns>
        public static short GetInt16(this object value, short defaultValue = 0)
        {
            return (value is short num) ? num
                 : (value == null) ? defaultValue
                 : GetInt16(value.ToString(), defaultValue);
        }
        /// <inheritdoc cref="GetInt16(object, short)"/>
        public static short GetInt16(this string value, short defaultValue = 0)
        {
            if (short.TryParse(value, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out short result)) {
                return result;
            }
            return defaultValue;
        }

        /// <summary>
        /// Converts the specified value to its <see cref="ushort"/> equivalent.
        /// </summary>
        /// <param name="value">The value to convert.</param>
        /// <param name="defaultValue">
        ///     The default value to return if the specified value could not be converted.
        /// </param>
        /// <returns>The <see cref="ushort"/> equivalent of the specified value.</returns>
        public static ushort GetUInt16(this object value, ushort defaultValue = 0)
        {
            return (value is ushort num) ? num
                 : (value == null) ? defaultValue
                 : GetUInt16(value.ToString(), defaultValue);
        }
        /// <inheritdoc cref="GetUInt16(object, ushort)"/>
        public static ushort GetUInt16(this string value, ushort defaultValue = 0)
        {
            if (ushort.TryParse(value, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out ushort result)) {
                return result;
            }
            return defaultValue;
        }

        /// <summary>
        /// Converts the specified value to its <see cref="int"/> equivalent.
        /// </summary>
        /// <param name="value">The value to convert.</param>
        /// <param name="defaultValue">
        ///     The default value to return if the specified value could not be converted.
        /// </param>
        /// <returns>The <see cref="int"/> equivalent of the specified value.</returns>
        public static int GetInt32(this object value, int defaultValue = 0)
        {
            return (value is int num) ? num
                 : (value == null) ? defaultValue
                 : GetInt32(value.ToString(), defaultValue);
        }
        /// <inheritdoc cref="GetInt32(object, int)"/>
        public static int GetInt32(this string value, int defaultValue = 0)
        {
            if (int.TryParse(value, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out int result)) {
                return result;
            }
            return defaultValue;
        }

        /// <summary>
        /// Converts the specified value to its <see cref="uint"/> equivalent.
        /// </summary>
        /// <param name="value">The value to convert.</param>
        /// <param name="defaultValue">
        ///     The default value to return if the specified value could not be converted.
        /// </param>
        /// <returns>The <see cref="uint"/> equivalent of the specified value.</returns>
        public static uint GetUInt32(this object value, uint defaultValue = 0)
        {
            return (value is uint num) ? num
                 : (value == null) ? defaultValue
                 : GetUInt32(value.ToString(), defaultValue);
        }
        /// <inheritdoc cref="GetUInt32(object, uint)"/>
        public static uint GetUInt32(this string value, uint defaultValue = 0)
        {
            if (uint.TryParse(value, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out uint result)) {
                return result;
            }
            return defaultValue;
        }

        /// <summary>
        /// Converts the specified value to its <see cref="long"/> equivalent.
        /// </summary>
        /// <param name="value">The value to convert.</param>
        /// <param name="defaultValue">
        ///     The default value to return if the specified value could not be converted.
        /// </param>
        /// <returns>The <see cref="long"/> equivalent of the specified value.</returns>
        public static long GetInt64(this object value, long defaultValue = 0)
        {
            return (value is long num) ? num
                 : (value == null) ? defaultValue
                 : GetInt64(value.ToString(), defaultValue);
        }
        /// <inheritdoc cref="GetInt64(object, long)"/>
        public static long GetInt64(this string value, long defaultValue = 0)
        {
            if (long.TryParse(value, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out long result)) {
                return result;
            }
            return defaultValue;
        }

        /// <summary>
        /// Converts the specified value to its <see cref="ulong"/> equivalent.
        /// </summary>
        /// <param name="value">The value to convert.</param>
        /// <param name="defaultValue">
        ///     The default value to return if the specified value could not be converted.
        /// </param>
        /// <returns>The <see cref="ulong"/> equivalent of the specified value.</returns>
        public static ulong GetUInt64(this object value, ulong defaultValue = 0)
        {
            return (value is ulong num) ? num
                 : (value == null) ? defaultValue
                 : GetUInt64(value.ToString(), defaultValue);
        }
        /// <inheritdoc cref="GetUInt64(object, ulong)"/>
        public static ulong GetUInt64(this string value, ulong defaultValue = 0)
        {
            if (ulong.TryParse(value, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out ulong result)) {
                return result;
            }
            return defaultValue;
        }

        #endregion

        #region <<< Floating-point conversion >>>

        /// <summary>
        /// Converts the specified value to its <see cref="float"/> equivalent.
        /// </summary>
        /// <param name="value">The value to convert.</param>
        /// <param name="defaultValue">
        ///     The default value to return if the specified value could not be converted.
        /// </param>
        /// <returns>The <see cref="float"/> equivalent of the specified value.</returns>
        /// <remarks>
        /// In addition to parsing the numeric values (e.g., 1.23), the following special strings will be converted to
        /// their <see cref="float"/> equivalent:
        ///     "NaN" converts to <see cref="float.NaN"/>;
        ///     "-∞", "-Inf", or "-Infinity" converts to <see cref="float.NegativeInfinity"/>; and
        ///     "∞", "Inf", or "Infinity" converts to <see cref="float.PositiveInfinity"/>.
        /// </remarks>
        public static float GetSingle(this object value, float defaultValue = 0)
        {
            return (value is float num) ? num
                 : (value == null) ? defaultValue
                 : GetSingle(value.ToString(), defaultValue);
        }
        /// <inheritdoc cref="GetSingle(object, float)"/>
        public static float GetSingle(this string value, float defaultValue = 0)
        {
            if (IsNanOrInfinity(value, out double d)) {
                return (float)d;
            }
            if (float.TryParse(value, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out float result)) {
                return result;
            }
            return defaultValue;
        }

        /// <summary>
        /// Converts the specified value to its <see cref="double"/> equivalent.
        /// </summary>
        /// <param name="value">The value to convert.</param>
        /// <param name="defaultValue">
        ///     The default value to return if the specified value could not be converted.
        /// </param>
        /// <returns>The <see cref="double"/> equivalent of the specified value.</returns>
        /// <remarks>
        /// In addition to parsing the numeric values (e.g., 1.23), the following special strings will be converted to
        /// their <see cref="double"/> equivalent:
        ///     "NaN" converts to <see cref="double.NaN"/>;
        ///     "-∞", "-Inf", or "-Infinity" converts to <see cref="double.NegativeInfinity"/>; and
        ///     "∞", "Inf", or "Infinity" converts to <see cref="double.PositiveInfinity"/>.
        /// </remarks>
        public static double GetDouble(this object value, double defaultValue = 0)
        {
            return (value is double num) ? num
                 : (value == null) ? defaultValue
                 : GetDouble(value.ToString(), defaultValue);
        }
        /// <inheritdoc cref="GetDouble(object, double)"/>
        public static double GetDouble(this string value, double defaultValue = 0)
        {
            if (IsNanOrInfinity(value, out double result)) {
                return result;
            }
            if (double.TryParse(value, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) {
                return result;
            }
            return defaultValue;
        }

        /// <summary>
        /// Returns <c>true</c>, if value is NaN, -∞, -inf, -infinity, ∞, inf, or infinity; <c>false</c>, otherwise.
        /// </summary>
        private static bool IsNanOrInfinity(string value, out double result)
        {
            switch (value.Trim().ToLower()) {
                case "nan":
                    result = double.NaN;
                    return true;

                case "-∞":
                case "-inf":
                case "-infinity":
                    result = double.NegativeInfinity;
                    return true;

                case "∞":
                case "inf":
                case "infinity":
                    result = double.PositiveInfinity;
                    return true;

                default:
                    result = 0;
                    return false;
            }
        }

        #endregion
    }
}
