#nullable enable
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Swan.Formatters;
namespace Swan {
  /// 
  /// String related extension methods.
  /// 
  public static class StringExtensions {
    #region Private Declarations
    private const RegexOptions StandardRegexOptions = RegexOptions.Multiline | RegexOptions.Compiled | RegexOptions.CultureInvariant;
    private static readonly String[] ByteSuffixes = { "B", "KB", "MB", "GB", "TB" };
    private static readonly Lazy SplitLinesRegex = new Lazy(() => new Regex("\r\n|\r|\n", StandardRegexOptions));
    private static readonly Lazy UnderscoreRegex = new Lazy(() => new Regex(@"_", StandardRegexOptions));
    private static readonly Lazy CamelCaseRegEx = new Lazy(() => new Regex(@"[a-z][A-Z]", StandardRegexOptions));
    private static readonly Lazy SplitCamelCaseString = new Lazy(() => m => {
      String x = m.ToString();
      return x[0] + " " + x[1..];
    });
    private static readonly Lazy InvalidFilenameChars = new Lazy(() => Path.GetInvalidFileNameChars().Select(c => c.ToString()).ToArray());
    #endregion
    /// 
    /// Returns a string that represents the given item
    /// It tries to use InvariantCulture if the ToString(IFormatProvider)
    /// overload exists.
    /// 
    /// The item.
    /// A  that represents the current object.
    public static String ToStringInvariant(this Object? @this) {
      if(@this == null) {
        return String.Empty;
      }
      Type itemType = @this.GetType();
      return itemType == typeof(String) ? @this as String ?? String.Empty : Definitions.BasicTypesInfo.Value.ContainsKey(itemType) ? Definitions.BasicTypesInfo.Value[itemType].ToStringInvariant(@this) : @this.ToString()!;
    }
    /// 
    /// Returns a string that represents the given item
    /// It tries to use InvariantCulture if the ToString(IFormatProvider)
    /// overload exists.
    /// 
    /// The type to get the string.
    /// The item.
    /// A  that represents the current object.
    public static String ToStringInvariant(this T item) => typeof(String) == typeof(T) ? item as String ?? String.Empty : ToStringInvariant(item as Object);
    /// 
    /// Removes the control characters from a string except for those specified.
    /// 
    /// The input.
    /// When specified, these characters will not be removed.
    /// 
    /// A string that represents the current object.
    /// 
    /// input.
    public static String RemoveControlCharsExcept(this String value, params Char[]? excludeChars) {
      if(value == null) {
        throw new ArgumentNullException(nameof(value));
      }
      if(excludeChars == null) {
        excludeChars = Array.Empty();
      }
      return new String(value.Where(c => Char.IsControl(c) == false || excludeChars.Contains(c)).ToArray());
    }
    /// 
    /// Removes all control characters from a string, including new line sequences.
    /// 
    /// The input.
    /// A  that represents the current object.
    /// input.
    public static String RemoveControlChars(this String value) => value.RemoveControlCharsExcept(null);
    /// 
    /// Outputs JSON string representing this object.
    /// 
    /// The object.
    /// if set to true format the output.
    /// A  that represents the current object.
    public static String ToJson(this Object @this, Boolean format = true) => @this == null ? String.Empty : Json.Serialize(@this, format);
    /// 
    /// Returns text representing the properties of the specified object in a human-readable format.
    /// While this method is fairly expensive computationally speaking, it provides an easy way to
    /// examine objects.
    /// 
    /// The object.
    /// A  that represents the current object.
    public static String Stringify(this Object @this) {
      if(@this == null) {
        return "(null)";
      }
      try {
        String jsonText = Json.Serialize(@this, false, "$type");
        Object? jsonData = Json.Deserialize(jsonText);
        return new HumanizeJson(jsonData, 0).GetResult();
      } catch {
        return @this.ToStringInvariant();
      }
    }
    /// 
    /// Retrieves a section of the string, inclusive of both, the start and end indexes.
    /// This behavior is unlike JavaScript's Slice behavior where the end index is non-inclusive
    /// If the string is null it returns an empty string.
    /// 
    /// The string.
    /// The start index.
    /// The end index.
    /// Retrieves a substring from this instance.
    public static String Slice(this String @this, Int32 startIndex, Int32 endIndex) {
      if(@this == null) {
        return String.Empty;
      }
      Int32 end = endIndex.Clamp(startIndex, @this.Length - 1);
      return startIndex >= end ? String.Empty : @this.Substring(startIndex, end - startIndex + 1);
    }
    /// 
    /// Gets a part of the string clamping the length and startIndex parameters to safe values.
    /// If the string is null it returns an empty string. This is basically just a safe version
    /// of string.Substring.
    /// 
    /// The string.
    /// The start index.
    /// The length.
    /// Retrieves a substring from this instance.
    public static String SliceLength(this String @this, Int32 startIndex, Int32 length) {
      if(@this == null) {
        return String.Empty;
      }
      Int32 start = startIndex.Clamp(0, @this.Length - 1);
      Int32 len = length.Clamp(0, @this.Length - start);
      return len == 0 ? String.Empty : @this.Substring(start, len);
    }
    /// 
    /// Splits the specified text into r, n or rn separated lines.
    /// 
    /// The text.
    /// 
    /// An array whose elements contain the substrings from this instance 
    /// that are delimited by one or more characters in separator.
    /// 
    public static String[] ToLines(this String @this) => @this == null ? Array.Empty() : SplitLinesRegex.Value.Split(@this);
    /// 
    /// Humanizes (make more human-readable) an identifier-style string 
    /// in either camel case or snake case. For example, CamelCase will be converted to 
    /// Camel Case and Snake_Case will be converted to Snake Case.
    /// 
    /// The identifier-style string.
    /// A  humanized.
    public static String Humanize(this String value) {
      if(value == null) {
        return String.Empty;
      }
      String returnValue = UnderscoreRegex.Value.Replace(value, " ");
      returnValue = CamelCaseRegEx.Value.Replace(returnValue, SplitCamelCaseString.Value);
      return returnValue;
    }
    /// 
    /// Humanizes (make more human-readable) an boolean.
    /// 
    /// if set to true [value].
    /// A  that represents the current boolean.
    public static String Humanize(this Boolean value) => value ? "Yes" : "No";
    /// 
    /// Humanizes (make more human-readable) the specified value.
    /// 
    /// The value.
    /// A  that represents the current object.
    public static String Humanize(this Object value) =>
            value switch
            {
              String stringValue => stringValue.Humanize(),
              Boolean boolValue => boolValue.Humanize(),
              _ => value.Stringify()
            };
    /// 
    /// Indents the specified multi-line text with the given amount of leading spaces
    /// per line.
    /// 
    /// The text.
    /// The spaces.
    /// A  that represents the current object.
    public static String Indent(this String value, Int32 spaces = 4) {
      if(value == null) {
        value = String.Empty;
      }
      if(spaces <= 0) {
        return value;
      }
      String[] lines = value.ToLines();
      StringBuilder builder = new StringBuilder();
      String indentStr = new String(' ', spaces);
      foreach(String line in lines) {
        _ = builder.AppendLine($"{indentStr}{line}");
      }
      return builder.ToString().TrimEnd();
    }
    /// 
    /// Gets the line and column number (i.e. not index) of the
    /// specified character index. Useful to locate text in a multi-line
    /// string the same way a text editor does.
    /// Please not that the tuple contains first the line number and then the
    /// column number.
    /// 
    /// The string.
    /// Index of the character.
    /// A 2-tuple whose value is (item1, item2).
    public static Tuple TextPositionAt(this String value, Int32 charIndex) {
      if(value == null) {
        return Tuple.Create(0, 0);
      }
      Int32 index = charIndex.Clamp(0, value.Length - 1);
      Int32 lineIndex = 0;
      Int32 colNumber = 0;
      for(Int32 i = 0; i <= index; i++) {
        if(value[i] == '\n') {
          lineIndex++;
          colNumber = 0;
          continue;
        }
        if(value[i] != '\r') {
          colNumber++;
        }
      }
      return Tuple.Create(lineIndex + 1, colNumber);
    }
    /// 
    /// Makes the file name system safe.
    /// 
    /// The s.
    /// 
    /// A string with a safe file name.
    /// 
    /// s.
    public static String ToSafeFilename(this String value) => value == null ? throw new ArgumentNullException(nameof(value)) : InvalidFilenameChars.Value.Aggregate(value, (current, c) => current.Replace(c, String.Empty)).Slice(0, 220);
    /// 
    /// Formats a long into the closest bytes string.
    /// 
    /// The bytes length.
    /// 
    /// The string representation of the current Byte object, formatted as specified by the format parameter.
    /// 
    public static String FormatBytes(this Int64 bytes) => ((UInt64)bytes).FormatBytes();
    /// 
    /// Formats a long into the closest bytes string.
    /// 
    /// The bytes length.
    /// 
    /// A copy of format in which the format items have been replaced by the string 
    /// representations of the corresponding arguments.
    /// 
    public static String FormatBytes(this UInt64 bytes) {
      Int32 i;
      Double dblSByte = bytes;
      for(i = 0; i < ByteSuffixes.Length && bytes >= 1024; i++, bytes /= 1024) {
        dblSByte = bytes / 1024.0;
      }
      return $"{dblSByte:0.##} {ByteSuffixes[i]}";
    }
    /// 
    /// Truncates the specified value.
    /// 
    /// The value.
    /// The maximum length.
    /// 
    /// Retrieves a substring from this instance.
    /// The substring starts at a specified character position and has a specified length.
    /// 
    public static String? Truncate(this String value, Int32 maximumLength) => Truncate(value, maximumLength, String.Empty);
    /// 
    /// Truncates the specified value and append the omission last.
    /// 
    /// The value.
    /// The maximum length.
    /// The omission.
    /// 
    /// Retrieves a substring from this instance.
    /// The substring starts at a specified character position and has a specified length.
    /// 
    public static String? Truncate(this String value, Int32 maximumLength, String omission) => value == null ? null : value.Length > maximumLength ? value.Substring(0, maximumLength) + (omission ?? String.Empty) : value;
    /// 
    /// Determines whether the specified  contains any of characters in
    /// the specified array of .
    /// 
    /// 
    /// true if  contains any of ;
    /// otherwise, false.
    /// 
    /// 
    /// A  to test.
    /// 
    /// 
    /// An array of  that contains characters to find.
    /// 
    public static Boolean Contains(this String value, params Char[] chars)  => chars?.Length == 0 || !String.IsNullOrEmpty(value) && chars != null && value.IndexOfAny(chars) > -1;
    /// 
    /// Replaces all chars in a string.
    /// 
    /// The value.
    /// The replace value.
    /// The chars.
    /// The string with the characters replaced.
    public static String ReplaceAll(this String value, String replaceValue, params Char[] chars) => chars.Aggregate(value, (current, c) => current.Replace(new String(new[] { c }), replaceValue));
    /// 
    /// Convert hex character to an integer. Return -1 if char is something
    /// other than a hex char.
    /// 
    /// The c.
    /// Converted integer.
    public static Int32 Hex2Int(this Char value) => value >= '0' && value <= '9' ? value - '0' : value >= 'A' && value <= 'F' ? value - 'A' + 10 : value >= 'a' && value <= 'f' ? value - 'a' + 10 : -1;
  }
}