using System;
using System.IO;

namespace Volpe.Cafe.Utils
{
    /// <summary>
    /// Provides helper methods for processing strings (such as fuzzy comparing, etc.).
    /// </summary>
    [Serializable]
    public static class StringHelper
    {
        /// <summary>
        /// Determines whether the two specified strings are equivalent, optionally ignoring case and using simple or
        /// fuzzy logic.
        /// </summary>
        /// <param name="value1">The first value to compare.</param>
        /// <param name="value2">The second value to compare.</param>
        /// <param name="ignoreCase">Specifies whether to ignore case during comparison.</param>
        /// <param name="fuzzy">
        ///     Specifies whether to use fuzzy logic during comparison. When using fuzzy logic, any character that is
        ///     not a letter or a digit is discarded.
        /// </param>
        /// <returns>
        /// <c>true</c>, if the two values are equivalent; <c>false</c>, otherwise.
        /// </returns>
        public static bool Equals(string value1, string value2, bool ignoreCase = true, bool fuzzy = false)
        {
            if (fuzzy) {
                // if using fuzzy logic, remove all chars that are not letters or digits
                value1 = FormatFuzzy(value1);
                value2 = FormatFuzzy(value2);
            }

            return string.Equals(value1, value2, ignoreCase ? StringComparison.InvariantCultureIgnoreCase
                                                            : StringComparison.InvariantCulture);
        }
        /// <summary>
        /// Determines whether the specified value is equivalent to any of the provided comparands, optionally ignoring
        /// case and using simple or fuzzy logic.
        /// </summary>
        /// <param name="value">The value to compare.</param>
        /// <param name="comparands">An array of comparands to compare against.</param>
        /// <param name="ignoreCase">Specifies whether to ignore case during comparison.</param>
        /// <param name="fuzzy">
        ///     Specifies whether to use fuzzy logic during comparison. When using fuzzy logic, any character that is
        ///     not a letter or a digit is discarded.
        /// </param>
        /// <returns>
        /// <c>true</c>, if the value is equivalent to any of the comparands; <c>false</c>, otherwise.
        /// </returns>
        public static bool EqualsAny(string value, string[] comparands, bool ignoreCase = true, bool fuzzy = false)
        {
            return EqualsAny(value, comparands, ignoreCase, fuzzy, out _);
        }
        /// <summary>
        /// Determines whether the specified value is equivalent to any of the provided comparands, optionally ignoring
        /// case and using simple or fuzzy logic.
        /// </summary>
        /// <param name="value">The value to compare.</param>
        /// <param name="comparands">An array of comparands to compare against.</param>
        /// <param name="ignoreCase">Specifies whether to ignore case during comparison.</param>
        /// <param name="fuzzy">
        ///     Specifies whether to use fuzzy logic during comparison. When using fuzzy logic, any character that is
        ///     not a letter or a digit is discarded.
        /// </param>
        /// <param name="index">
        ///     When this method returns, contains the index of the first element in the comparands array that matches
        ///     the specified value, or -1, if no match was found.
        /// </param>
        /// <returns>
        /// <c>true</c>, if the value is equivalent to any of the comparands; <c>false</c>, otherwise.
        /// </returns>
        public static bool EqualsAny(string value, string[] comparands, bool ignoreCase, bool fuzzy, out int index)
        {
            index = -1;
            if (comparands == null || comparands.Length == 0) {
                return value == null;
            }

            // examine each comparand -- if at least one matches, return true
            for (int i = 0; i < comparands.Length; i++) {
                if (Equals(value, comparands[i], ignoreCase, fuzzy)) {
                    index = i;
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Returns the zero-based index of the first occurrence of the specified substring within the given value,
        /// optionally ignoring case and using simple or fuzzy logic.
        /// </summary>
        /// <param name="value">The value to examine.</param>
        /// <param name="substring">The substring to look for in value.</param>
        /// <param name="ignoreCase">Specifies whether to ignore case during comparison.</param>
        /// <param name="fuzzy">
        ///     Specifies whether to use fuzzy logic during comparison. When using fuzzy logic, any character that is
        ///     not a letter or a digit is discarded.
        /// </param>
        /// <returns>
        /// The zero-based index of the first occurrence of the specified substring within the given value; or, -1, if
        /// substring is not contained in value.
        /// </returns>
        public static int IndexOf(string value, string substring, bool ignoreCase = true, bool fuzzy = false)
        {
            if (fuzzy) {
                // if using fuzzy logic, remove all chars that are not letters or digits
                value = FormatFuzzy(value);
                substring = FormatFuzzy(substring);
            }

            return value.IndexOf(substring, ignoreCase ? StringComparison.InvariantCultureIgnoreCase
                                                       : StringComparison.InvariantCulture);
        }
        /// <summary>
        /// Returns the zero-based indexes of the first occurrences of each of the specified substrings within the
        /// given value, optionally ignoring case and using simple or fuzzy logic.
        /// </summary>
        /// <param name="value">The value to examine.</param>
        /// <param name="substrings">An array of substrings to look for in value.</param>
        /// <param name="ignoreCase">Specifies whether to ignore case during comparison.</param>
        /// <param name="fuzzy">
        ///     Specifies whether to use fuzzy logic during comparison. When using fuzzy logic, any character that is
        ///     not a letter or a digit is discarded.
        /// </param>
        /// <returns>
        /// The zero-based indexes of the first occurrences of the specified substrings within the given value; or, -1,
        /// if substring is not contained in value.
        /// </returns>
        public static int[] IndexOfAll(string value, string[] substrings, bool ignoreCase = true, bool fuzzy = false)
        {
            if (substrings == null || substrings.Length == 0) {
                return null;
            }

            int[] result = new int[substrings.Length];
            for (int i = 0; i < substrings.Length; i++) {
                result[i] = IndexOf(value, substrings[i], ignoreCase, fuzzy);
            }
            return result;
        }

        /// <summary>
        /// Determines whether the specified substring is contained within the given value, optionally ignoring case
        /// and using simple or fuzzy logic.
        /// </summary>
        /// <param name="value">The value to examine.</param>
        /// <param name="substring">The substring to look for in value.</param>
        /// <param name="ignoreCase">Specifies whether to ignore case during comparison.</param>
        /// <param name="fuzzy">
        ///     Specifies whether to use fuzzy logic during comparison. When using fuzzy logic, any character that is
        ///     not a letter or a digit is discarded.
        /// </param>
        /// <returns><c>true</c>, if the specified substring is contained in value; <c>false</c>, otherwise.</returns>
        public static bool Contains(string value, string substring, bool ignoreCase = true, bool fuzzy = false)
        {
            return (IndexOf(value, substring, ignoreCase, fuzzy) != -1);
        }
        /// <summary>
        /// Determines whether any of the specified substrings are contained within the given value, optionally
        /// ignoring case and using simple or fuzzy logic.
        /// </summary>
        /// <param name="value">The value to examine.</param>
        /// <param name="substrings">An array of substrings to look for in value.</param>
        /// <param name="ignoreCase">Specifies whether to ignore case during comparison.</param>
        /// <param name="fuzzy">
        ///     Specifies whether to use fuzzy logic during comparison. When using fuzzy logic, any character that is
        ///     not a letter or a digit is discarded.
        /// </param>
        /// <returns>
        /// <c>true</c>, if any of the specified substrings are contained in value; <c>false</c>, otherwise.
        /// </returns>
        public static bool ContainsAny(string value, string[] substrings, bool ignoreCase = true, bool fuzzy = false)
        {
            return ContainsAnyOrAll(value, substrings, ignoreCase, fuzzy, true);
        }
        /// <summary>
        /// Determines whether all of the specified substrings are contained within the given value, optionally
        /// ignoring case and using simple or fuzzy logic.
        /// </summary>
        /// <param name="value">The value to examine.</param>
        /// <param name="substrings">An array of substrings to look for in value.</param>
        /// <param name="ignoreCase">Specifies whether to ignore case during comparison.</param>
        /// <param name="fuzzy">
        ///     Specifies whether to use fuzzy logic during comparison. When using fuzzy logic, any character that is
        ///     not a letter or a digit is discarded.
        /// </param>
        /// <returns>
        /// <c>true</c>, if all of the specified substrings are contained in value; <c>false</c>, otherwise.
        /// </returns>
        public static bool ContainsAll(string value, string[] substrings, bool ignoreCase = true, bool fuzzy = false)
        {
            return ContainsAnyOrAll(value, substrings, ignoreCase, fuzzy, false);
        }

        /// <summary>
        /// Helper method for <see cref="ContainsAny"/> and <see cref="ContainsAll"/>. See associated methods for
        /// description.<br/>
        /// matchAny: if <c>true</c>, behavior mimics <see cref="ContainsAny"/>;
        ///           if <c>false</c>, behavior mimics <see cref="ContainsAll"/>.
        /// </summary>
        private static bool ContainsAnyOrAll(
            string value, string[] substrings, bool ignoreCase, bool fuzzy, bool matchAny)
        {
            if (substrings == null || substrings.Length == 0) {
                return false;
            }

            // examine each substring:
            //  > when matching ANY -- if at least one is found within value, return true
            //  > when matching ALL -- if all are found within value, return true
            for (int i = 0; i < substrings.Length; i++) {
                if (Contains(value, substrings[i], ignoreCase, fuzzy) == matchAny) {
                    return matchAny;
                }
            }
            return !matchAny;
        }

        /// <summary>
        /// Determines whether the specified value starts with the given substring, optionally ignoring case and using
        /// simple or fuzzy logic.
        /// </summary>
        /// <param name="value">The value to examine.</param>
        /// <param name="substring">The substring to look for in value.</param>
        /// <param name="ignoreCase">Specifies whether to ignore case during comparison.</param>
        /// <param name="fuzzy">
        ///     Specifies whether to use fuzzy logic during comparison. When using fuzzy logic, any character that is
        ///     not a letter or a digit is discarded.
        /// </param>
        /// <returns><c>true</c>, if value starts with the specified substring; <c>false</c>, otherwise.</returns>
        public static bool StartsWith(string value, string substring, bool ignoreCase = true, bool fuzzy = false)
        {
            return (IndexOf(value, substring, ignoreCase, fuzzy) == 0);
        }
        /// <summary>
        /// Determines whether the specified value starts with any of the given substrings, optionally ignoring case
        /// and using simple or fuzzy logic.
        /// </summary>
        /// <param name="value">The value to examine.</param>
        /// <param name="substrings">An array of substrings to look for in value.</param>
        /// <param name="ignoreCase">Specifies whether to ignore case during comparison.</param>
        /// <param name="fuzzy">
        ///     Specifies whether to use fuzzy logic during comparison. When using fuzzy logic, any character that is
        ///     not a letter or a digit is discarded.
        /// </param>
        /// <returns>
        /// <c>true</c>, if value starts with any of the specified substrings; <c>false</c>, otherwise.
        /// </returns>
        public static bool StartsWithAny(string value, string[] substrings, bool ignoreCase = true, bool fuzzy = false)
        {
            if (substrings == null || substrings.Length == 0) {
                return false;
            }

            for (int i = 0; i < substrings.Length; i++) {
                if (StartsWith(value, substrings[i], ignoreCase, fuzzy)) {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Converts the specified string to "Title Case," where each first letter of a word is capitalized and each
        /// subsequent letter of the same word is lowercased.
        /// </summary>
        /// <param name="value">The string to convert.</param>
        /// <returns>A copy of the specified string converted to "Title Case."</returns>
        public static string GetTitleCase(string value)
        {
            if (value == null) {
                return null;
            }

            char[] chars = value.ToCharArray();
            for (int i = 0, count = value.Length; i < count; i++) {
                bool isTitleChar = (i == 0 || !char.IsLetterOrDigit(chars[i - 1]));
                char c = chars[i];

                if (isTitleChar && char.IsLower(c)) {
                    chars[i] = char.ToUpper(c);
                }
                else if (!isTitleChar && char.IsUpper(c)) {
                    chars[i] = char.ToLower(c);
                }
            }
            return new string(chars);
        }
        /// <summary>
        /// Converts the specified string to "Title Case," where each first letter of a word is capitalized and each
        /// subsequent letter of the same word is lowercased, if the length of the string is greater than or equal to
        /// <paramref name="minLength"/>.
        /// </summary>
        /// <param name="value">The string to convert.</param>
        /// <param name="minLength">The minimum length the string needs to be before being converted.</param>
        /// <returns>
        /// A copy of the specified string converted to "Title Case," if the length of the string is greater than or
        /// equal to <paramref name="minLength"/>; otherwise, the unmodified string will be returned.
        /// </returns>
        public static string GetTitleCase(string value, int minLength)
        {
            return (value == null || value.Length >= minLength)
                ? GetTitleCase(value)
                : value;
        }

        /// <summary>
        /// Converts the given value, specified in milliseconds, to an equivalent time string in the following format:
        /// "[D.]HH:MM:SS".
        /// </summary>
        /// <param name="value">The value, specified in milliseconds, to convert.</param>
        /// <returns>A string representation of the specified time value.</returns>
        public static string GetTimeString(long value)
        {
            string sign = "";
            if (value < 0) {
                value = -value;
                sign = "-";
            }

            long sec = value / 1000;
            long s = sec % 60;
            long m = sec / 60 % 60;
            long h = sec / 3600 % 24;
            long d = sec / 86400;

            string day = (d != 0) ? d.ToString() + "." : "";

            return $"{sign}{day}{h:00:}{m:00:}{s:00}";
        }
        /// <summary>
        /// Converts the given value to an equivalent time string in the following format: "[D.]HH:MM:SS".
        /// </summary>
        /// <param name="value">The value to convert.</param>
        /// <returns>A string representation of the specified time value.</returns>
        public static string GetTimeString(TimeSpan value)
        {
            return GetTimeString(value.Ticks / 10000);
        }

        /// <summary>
        /// Removes all special characters, which cannot be handled by the windows file systems, from the specified
        /// string and replaces them with the provided character.
        /// </summary>
        /// <param name="value">The string from which to remove special characters.</param>
        /// <param name="c">The character with which to replace the special characters.</param>
        /// <returns>The modified string.</returns>
        public static string RemoveSpecialChars(string value, char c)
        {
            if (value == null) {
                return null;
            }

            char[] invalidChars = Path.GetInvalidFileNameChars();
            for (int i = 0; i < invalidChars.Length; i++) {
                if (value.IndexOf(invalidChars[i]) != -1) {
                    value = value.Replace(invalidChars[i], c);
                }
            }
            return value;
        }

        //----- helper methods -----
        private static string FormatFuzzy(string value)
        {
            if (value == null) {
                return null;
            }

            char[] newChars = new char[value.Length];
            int j = 0;
            for (int i = 0; i < value.Length; i++) {
                char c = value[i];
                if (char.IsLetterOrDigit(c)) {
                    newChars[j++] = c;
                }
            }
            return new string(newChars, 0, j);
        }

    }
}
