using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Globalization;
using System.Security.Cryptography;
using System.ComponentModel.DataAnnotations;
using System.Text.Json;

namespace ExtensionMethods {
  public static class StringExtensions {

    /// <summary>
    /// SQL Like implementation
    /// </summary>
    /// <param name="text"></param>
    /// <param name="wildcardedText"></param>
    /// <returns></returns>
    public static bool Like(this string text, string wildcardedText) {
      text = text ?? "";
      wildcardedText = wildcardedText ?? "";

      return Regex.IsMatch(text, wildcardedText.WildcardToRegular(), RegexOptions.IgnoreCase | RegexOptions.Multiline);
    }

    /// <summary>
    /// Converts wildcarded string tgo regex
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    private static string WildcardToRegular(this string value) =>
      $"^{Regex.Escape(value).Replace("\\?", ".").Replace("\\*", ".*")}$";

    /// <summary>
    /// VB.Net Left implementation
    /// </summary>
    /// <param name="s"></param>
    /// <param name="count"></param>
    /// <returns></returns>
    public static string Left(this string s, int count) {
      return s.Substring(0, count);
    }

    /// <summary>
    /// VB.Net Right implementation
    /// </summary>
    /// <param name="s"></param>
    /// <param name="count"></param>
    /// <returns></returns>
    public static string Right(this string s, int count) {
      return s.Substring(s.Length - count, count);
    }

    /// <summary>
    /// VB.Net Mid implementation
    /// </summary>
    /// <param name="s"></param>
    /// <param name="index"></param>
    /// <param name="count"></param>
    /// <returns></returns>
    public static string Mid(this string s, int index, int count) {
      return s.Substring(index, count);
    }

    /// <summary>
    /// VB.Net ToInteger implementation
    /// </summary>
    /// <param name="s"></param>
    /// <returns></returns>
    public static int ToInteger(this string s) {
      var integerValue = 0;
      int.TryParse(s, out integerValue);
      return integerValue;
    }

    /// <summary>
    /// VB.Net IsInteger implementations
    /// </summary>
    /// <param name="s"></param>
    /// <returns></returns>
    public static bool IsInteger(this string s) => new Regex("^-[0-9]+$|^[0-9]+$").Match(s).Success;

    public static StringBuilder Prepend(this StringBuilder sb, string content) => sb.Insert(0, content);

    public static T ToEnum<T>(this string input) where T : struct {
      var result = default(T);

      if (string.IsNullOrWhiteSpace(input))
        return result;

      bool parseSucceeded = Enum.TryParse(input, true, out result);

      if (parseSucceeded)
        return result;

      // se fallisce il parse per valore proviamo ad usare il DisplayAttribute.Name
      var enumType = typeof(T);

      foreach (T enumItem in Enum.GetValues(enumType)) {
        var att = enumType.GetMember(enumItem.ToString())[0]
            .GetCustomAttributes(typeof(DisplayAttribute), false).SingleOrDefault();

        var displayName = ((DisplayAttribute)att).GetName();

        if (input.Equals(displayName, StringComparison.InvariantCultureIgnoreCase))
          return enumItem;
      }
      throw new NotSupportedException($"Cannot parse the value '{input}' for {enumType}");
    }

    public static T? ToNullableEnum<T>(this string input) where T : struct => !string.IsNullOrWhiteSpace(input) ? input.ToEnum<T>() : default(T?);

    public static string ToNull(this string s) => !string.IsNullOrWhiteSpace(s) ? s : null;

    public static string NullIfEmptyString(this string s) => s.ToNull();

    public static long ToLong(this string s) {
      if (!long.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out long result))
        result = !string.IsNullOrWhiteSpace(s) ? s.ToUpper().GetHashCode() : default;
      return result;
    }

    public static long? ToNullableLong(this string s) => !string.IsNullOrWhiteSpace(s) ? s.ToLong() : default(long?);

    public static int ToInt(this string s) {
      if (!int.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out int result))
        result = !string.IsNullOrWhiteSpace(s) ? s.ToUpper().GetHashCode() : default;
      return result;
    }

    public static int? ToNullableInt(this string s) => !string.IsNullOrWhiteSpace(s) ? s.ToInt() : default(int?);

    public static uint ToUint(this string s) {
      if (!uint.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out uint result))
        result = !string.IsNullOrWhiteSpace(s) ? (uint)s.ToUpper().GetHashCode() : default;
      return result;
    }

    public static uint? ToNullableUint(this string s) => !string.IsNullOrWhiteSpace(s) ? s.ToUint() : default(uint?);

    public static decimal ToDecimal(this string s) {
      if (!int.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out int result))
        result = !string.IsNullOrWhiteSpace(s) ? s.ToUpper().GetHashCode() : default;
      return result;
    }

    public static decimal? ToNullableDecimal(this string s) => !string.IsNullOrWhiteSpace(s) ? s.ToDecimal() : default(decimal?);

    public static double ToDouble(this string s) {
      if (!double.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out double result))
        result = !string.IsNullOrWhiteSpace(s) ? s.ToUpper().GetHashCode() : default;
      return result;
    }

    public static double? ToNullableDouble(this string s) => !string.IsNullOrWhiteSpace(s) ? s.ToDouble() : default(double?);

    #region DateTime
    public static DateTime ToDate(this string s, string[] formats) {
      return DateTime.TryParseExact(s, formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime datetime)
            ? DateTime.SpecifyKind(datetime, DateTimeKind.Utc)
            : throw new ApplicationException($"the date [{s}] is not in required format: [{formats[0]}]");
    }

    public static DateTime ToDate(this string s) => s.ToDate(new string[] { "dd/MM/yyyy" });

    public static DateTime? ToNullableDate(this string s) => !string.IsNullOrEmpty(s) ? s.ToDate() : default(DateTime?);

    public static DateTime? ToNullableDate(this string s, string[] formats) => !string.IsNullOrEmpty(s) ? s.ToDate(formats) : default(DateTime?);

    public static DateTime ToDateTime(this string s, string[] formats) {
      if (s == "Now")
        return DateTime.Now;
      else if (s == "Today")
        return DateTime.Today;

      DateTime result;

      if (!DateTime.TryParseExact(s, formats, new DateTimeFormatInfo(), DateTimeStyles.None, out result))
        throw new ApplicationException(string.Format("unable to parse exact date from value [{0}] with formats [{1}]", s, string.Join(", ", formats)));

      return DateTime.SpecifyKind(result, DateTimeKind.Utc);
    }

    public static DateTime ToDateTime(this string s) => s.ToDateTime(new string[] { "dd/MM/yyyy", "dd/MM/yyyy HH:mm:ss", "dd/MM/yyyy HH:mm", "yyyy-MM-dd'T'HH:mm:ss'Z'" });

    public static DateTime? ToNullableDateTime(this string s) => !string.IsNullOrWhiteSpace(s) ? s.ToDateTime() : default(DateTime?);

    public static DateTime? ToNullableDateTime(this string s, string[] formats) => !string.IsNullOrWhiteSpace(s) ? s.ToDateTime(formats) : default(DateTime?);
    #endregion
    public static bool ToBool(this string s) => new string[] { "ok", "yes", "y", "true", "1" }.Any(x => string.Equals(x, s, StringComparison.InvariantCultureIgnoreCase));

    public static bool? ToNullableBool(this string s) => !string.IsNullOrWhiteSpace(s) ? s.ToBool() : default(bool?);

    public static Guid ToGuid(this string text) {
      if (Guid.TryParse(text, out var value))
        return value;

      using var md5 = MD5.Create();
      byte[] hash = md5.ComputeHash(Encoding.Default.GetBytes(text.ToUpper()));
      return new Guid(hash);
    }

    public static Guid? ToNullableGuid(this string s) => !string.IsNullOrWhiteSpace(s) ? s.ToGuid() : default(Guid?);

    public static string[] StringSplit(this string s, char c) {
      return s.Split(c).Select(x => x.Trim()).ToArray();
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="s"></param>
    /// <returns></returns>
    public static string ToTitle(this string s) {
      if (!string.IsNullOrWhiteSpace(s)) {
        var tmp = s.ToCharArray();
        tmp[0] = char.ToUpper(tmp[0]);
        s = new string(tmp);
      }

      return s;
    }

    public static IEnumerable<Uri> ExtractUrls(this string s) {
      var urls = new List<Uri>();

      foreach (Match match in Regex.Matches(s, @"(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])")) {
        if (Uri.TryCreate(match.Value, UriKind.Absolute, out Uri uri)) {
          urls.Add(uri);
        }
      }

      return urls.Distinct();
    }

    public static string Format(this string s, params object[] args) => string.Format(s, args);

    /// <summary>
    /// Adds elipsis
    /// </summary>
    /// <param name="s"></param>
    /// <param name="length"></param>
    /// <returns></returns>
    public static string Excerpt(this string s, int length = 60) =>
      string.IsNullOrEmpty(s) || s.Length < length ? s : s.Substring(0, length - 3) + "...";

    /// <summary>
    /// Converts JSON string to object
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="s"></param>
    /// <returns></returns>
    public static T? ToObject<T>(this string s) => JsonSerializer.Deserialize<T>(s, new JsonSerializerOptions {
      PropertyNameCaseInsensitive = true
    });

    /// <summary>
    /// Adapted from .Net [EmailAddres] model validation attribute
    /// https://referencesource.microsoft.com/#System.ComponentModel.DataAnnotations/DataAnnotations/EmailAddressAttribute.cs,17f9a2457ddc8d93
    /// </summary>
    /// <param name="s"></param>
    /// <returns></returns>
    public static bool IsValidEmail(this string? s) {
      Regex CreateRegEx() {
        const string pattern = @"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$";
        const RegexOptions options = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture;

        // Set explicit regex match timeout, sufficient enough for email parsing
        // Unless the global REGEX_DEFAULT_MATCH_TIMEOUT is already set
        TimeSpan matchTimeout = TimeSpan.FromSeconds(2);

        try {
          if (AppDomain.CurrentDomain.GetData("REGEX_DEFAULT_MATCH_TIMEOUT") == null) {
            return new Regex(pattern, options, matchTimeout);
          }
        }
        catch {
          // Fallback on error
        }

        // Legacy fallback (without explicit match timeout)
        return new Regex(pattern, options);
      }

      // This attribute provides server-side email validation equivalent to jquery validate,
      // and therefore shares the same regular expression.  See unit tests for examples.
      var _regex = CreateRegEx();

      if (s == null)
        return false;

      return s != null && _regex.Match(s).Length > 0;
    }

    /// <summary>
    /// Convert HTML string to Plain Text without external libraries
    /// https://docs.microsoft.com/en-us/answers/questions/594274/convert-html-text-to-plain-text-in-c.html
    /// </summary>
    /// <param name="htmlCode"></param>
    /// <returns></returns>
    public static string HTMLToPlainText(this string htmlCode) {

      // Remove new lines since they are not visible in HTML
      htmlCode = htmlCode.Replace("\n", " ");

      // Remove tab spaces
      htmlCode = htmlCode.Replace("\t", " ");

      // Remove multiple white spaces from HTML
      htmlCode = Regex.Replace(htmlCode, "\\s+", " ");

      // Remove HEAD tag
      htmlCode = Regex.Replace(htmlCode, "<head.*?</head>", "", RegexOptions.IgnoreCase | RegexOptions.Singleline);

      // Remove any JavaScript
      htmlCode = Regex.Replace(htmlCode, "<script.*?</script>", "", RegexOptions.IgnoreCase | RegexOptions.Singleline);

      // Replace special characters like &, <, >, " etc.
      StringBuilder sbHTML = new StringBuilder(htmlCode);
      
      // Note: There are many more special characters, these are just
      // most common. You can add new characters in this arrays if needed
      string[] OldWords = {"&nbsp;", "&amp;", "&quot;", "&lt;", "&gt;", "&reg;", "&copy;", "&bull;", "&trade;","&#39;"};
      string[] NewWords = { " ", "&", "\"", "<", ">", "Â®", "Â©", "â€¢", "â„¢", "\'" };
     
      for (int i = 0; i < OldWords.Length; i++) {
        sbHTML.Replace(OldWords[i], NewWords[i]);
      }
      
      // Check if there are line breaks (<br>) or paragraph (<p>)
      sbHTML.Replace("<br>", "\n<br>");
      sbHTML.Replace("<br ", "\n<br ");
      sbHTML.Replace("<p ", "\n<p ");

      // Finally, remove all HTML tags and return plain text
      var plainText = Regex.Replace(sbHTML.ToString(), "<[^>]*>", "");

      // Remove external new lines
      plainText = plainText.Trim();

      return plainText;
    }
  }
}