// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package org.eclipse.che.ide.util;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArrayString;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/** Utility methods for string operations. */
public class StringUtils {

  /**
   * Convert milliseconds to human readable format like 1d:10h:15m:07s
   *
   * @param timeInMs time in milliseconds
   * @return formatted string
   */
  public static String timeMlsToHumanReadable(long timeInMs) {
    return timeSecToHumanReadable(timeInMs / 1000);
  }

  /**
   * Convert seconds to human readable format like 0d:00h:00m:07s
   *
   * @param timeInSec time in seconds
   * @return formatted string
   */
  public static String timeSecToHumanReadable(long timeInSec) {
    int ss = (int) timeInSec;
    int mm = 0;
    if (ss >= 60) {
      mm = ss / 60;
      ss = ss % 60;
    }
    int hh = 0;
    if (mm >= 60) {
      hh = mm / 60;
      mm = mm % 60;
    }
    int d = 0;
    if (hh >= 24) {
      d = hh / 24;
      hh = hh % 24;
    }

    if (d > 0)
      return String.valueOf(
          d
              + "d:"
              + getDoubleDigit(hh)
              + "h:"
              + getDoubleDigit(mm)
              + "m:"
              + getDoubleDigit(ss)
              + "s");

    if (d == 0 && hh > 0)
      return String.valueOf(hh + "h:" + getDoubleDigit(mm) + "m:" + getDoubleDigit(ss) + "s");

    if (d == 0 && hh == 0 && mm > 0) return String.valueOf(mm + "m:" + getDoubleDigit(ss) + "s");

    return String.valueOf(getDoubleDigit(ss) + "s");
  }

  /** Get a double digit int from a single, e.g. 1 = "01", 2 = "02". */
  public static String getDoubleDigit(int i) {
    final String doubleDigitI;
    switch (i) {
      case 0:
        doubleDigitI = "00";
        break;
      case 1:
        doubleDigitI = "01";
        break;
      case 2:
        doubleDigitI = "02";
        break;
      case 3:
        doubleDigitI = "03";
        break;
      case 4:
        doubleDigitI = "04";
        break;
      case 5:
        doubleDigitI = "05";
        break;
      case 6:
        doubleDigitI = "06";
        break;
      case 7:
        doubleDigitI = "07";
        break;
      case 8:
        doubleDigitI = "08";
        break;
      case 9:
        doubleDigitI = "09";
        break;
      default:
        doubleDigitI = Integer.toString(i);
    }
    return doubleDigitI;
  }

  /** Map [N] -> string of N spaces. Used by {@link #getSpaces} to cache strings of spaces. */
  private static final Map<Integer, String> cachedSpaces = new HashMap<>();
  /**
   * By default, this is a pure java implementation, but can be set to a more optimized version by
   * the client
   */
  private static Implementation implementation =
      GWT.isClient() || !GWT.isScript() ? new PureJavaImplementation() : new NativeImplementation();

  /** Sets the implementation for methods */
  public static void setImplementation(Implementation implementation) {
    StringUtils.implementation = implementation;
  }

  /** @return largest n such that {@code string1.substring(0, n).equals(string2.substring(0, n))} */
  public static int findCommonPrefixLength(String string1, String string2) {
    int limit = Math.min(string1.length(), string2.length());
    int result = 0;
    while (result < limit) {
      if (string2.charAt(result) != string1.charAt(result)) {
        break;
      }
      result++;
    }
    return result;
  };

  public static int countNumberOfOccurrences(String s, String pattern) {
    int count = 0;
    int i = 0;

    while ((i = s.indexOf(pattern, i)) >= 0) {
      count++;
      i += pattern.length();
    }

    return count;
  }

  /**
   * Check if a String ends with a specified suffix, ignoring case
   *
   * @param s the String to check, may be null
   * @param suffix the suffix to find, may be null
   * @return true if s ends with suffix or both s and suffix are null, false otherwise.
   */
  public static boolean endsWithIgnoreCase(String s, String suffix) {
    if (s == null || suffix == null) {
      return (s == null && suffix == null);
    }
    if (suffix.length() > s.length()) {
      return false;
    }
    return s.regionMatches(true, s.length() - suffix.length(), suffix, 0, suffix.length());
  }

  public static boolean endsWithChar(CharSequence s, char suffix) {
    return s != null && s.length() != 0 && s.charAt(s.length() - 1) == suffix;
  }

  public static boolean looksLikeImage(String path) {
    String lowercase = path.toLowerCase();
    return lowercase.endsWith(".jpg")
        || lowercase.endsWith(".jpeg")
        || lowercase.endsWith(".ico")
        || lowercase.endsWith(".png")
        || lowercase.endsWith(".gif");
  }

  public static <T> String join(T[] items, String separator) {
    StringBuilder s = new StringBuilder();
    for (int i = 0; i < items.length; i++) {
      s.append(items[i]).append(separator);
    }
    s.setLength(s.length() - separator.length());

    return s.toString();
  }

  public static <T> String join(List<T> items, String separator) {
    StringBuilder s = new StringBuilder();
    for (int i = 0; i < items.size(); i++) {
      s.append(items.get(i)).append(separator);
    }
    s.setLength(s.length() - separator.length());

    return s.toString();
  }

  /**
   * Check that string starts with specified prefix.
   *
   * <p>
   *
   * <p>If {@code caseInsensitive == false} this check is equivalent to {@link
   * String#startsWith(String)}.
   *
   * <p>
   *
   * <p>Otherwise {@code prefix} should be lower-case and check ignores case of {@code string}.
   */
  public static boolean startsWith(String prefix, String string, boolean caseInsensitive) {
    if (caseInsensitive) {
      int prefixLength = prefix.length();
      if (string.length() < prefixLength) {
        return false;
      }
      return prefix.equals(string.substring(0, prefixLength).toLowerCase());
    } else {
      return string.startsWith(prefix);
    }
  }

  /**
   * Returns if the given {@link java.lang.String} named {@code first} contains the string {@code
   * second} ignoring the case.
   *
   * @param first the string to test.
   * @param second the sequence to search.
   * @return {@code true} if string {@code first} contains the string {@code second} ignoring the
   *     case, {@code false} otherwise.
   */
  public static boolean containsIgnoreCase(String first, String second) {
    return first.toLowerCase().contains(second.toLowerCase());
  }

  /**
   * @return the length of the starting whitespace for the line, or the string length if it is all
   *     whitespace
   */
  public static int lengthOfStartingWhitespace(String s) {
    int n = s.length();
    for (int i = 0; i < n; i++) {
      char c = s.charAt(i);
      // TODO: This currently only deals with ASCII whitespace.
      // Read until a non-space
      if (c != ' ' && c != '\t') {
        return i;
      }
    }

    return n;
  }

  /**
   * @return first character in the string that is not a whitespace, or {@code 0} if there is no
   *     such characters
   */
  public static char firstNonWhitespaceCharacter(String s) {
    for (int i = 0, n = s.length(); i < n; ++i) {
      char c = s.charAt(i);
      if (!isWhitespace(c)) {
        return c;
      }
    }
    return 0;
  }

  /**
   * @return last character in the string that is not a whitespace, or {@code 0} if there is no such
   *     characters
   */
  public static char lastNonWhitespaceCharacter(String s) {
    for (int i = s.length() - 1; i >= 0; --i) {
      char c = s.charAt(i);
      if (!isWhitespace(c)) {
        return c;
      }
    }
    return 0;
  }

  public static String nullToEmpty(String s) {
    return s == null ? "" : s;
  }

  public static boolean isNullOrEmpty(String s) {
    return s == null || s.isEmpty();
  }

  public static boolean isNullOrWhitespace(String s) {
    return s == null || "".equals(s.trim());
  }

  public static String trimNullToEmpty(String s) {
    return s == null ? "" : s.trim();
  }

  public static String trimStart(String s, String prefix) {
    if (s.startsWith(prefix)) {
      return s.substring(prefix.length());
    }
    return s;
  }

  public static String ensureNotEmpty(String s, String defaultStr) {
    return isNullOrEmpty(s) ? defaultStr : s;
  }

  public static boolean isWhitespace(char ch) {
    return (ch <= ' ');
  }

  public static boolean isAlpha(char ch) {
    return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z');
  }

  public static boolean isNumeric(char ch) {
    return ('0' <= ch && ch <= '9');
  }

  public static boolean isQuote(char ch) {
    return ch == '\'' || ch == '\"';
  }

  public static boolean isAlphaNumOrUnderscore(char ch) {
    return isAlpha(ch) || isNumeric(ch) || ch == '_';
  }

  public static long toLong(String longStr) {
    return longStr == null ? 0 : Long.parseLong(longStr);
  }

  /** @return true, if both strings are empty or {@code null}, or if they are equal */
  public static boolean equalStringsOrEmpty(String a, String b) {
    return nullToEmpty(a).equals(nullToEmpty(b));
  }

  /** @return true, if the strings are not empty or {@code null}, and equal */
  public static boolean equalNonEmptyStrings(String a, String b) {
    return !isNullOrEmpty(a) && a.equals(b);
  }

  /**
   * Splits with the contract of the contract of the JavaScript String.split().
   *
   * <p>
   *
   * <p>More specifically: empty segments will be produced for adjacent and trailing separators.
   *
   * <p>
   *
   * <p>If an empty string is used as the separator, the string is split between each character.
   *
   * <p>
   *
   * <p>Examples:
   *
   * <ul>
   *   <li>{@code split("a", "a")} should produce {@code ["", ""]}
   *   <li>{@code split("a\n", "\n")} should produce {@code ["a", ""]}
   *   <li>{@code split("ab", "")} should produce {@code ["a", "b"]}
   * </ul>
   */
  public static List<String> split(String s, String separator) {
    return implementation.split(s, separator);
  }

  /** @return the number of editor lines this text would take up */
  public static int countNumberOfVisibleLines(String text) {
    return countNumberOfOccurrences(text, "\n") + (text.endsWith("\n") ? 0 : 1);
  }

  public static String capitalizeFirstLetter(String s) {
    if (!isNullOrEmpty(s)) {
      s = s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
    }
    return s;
  }

  public static String ensureStartsWith(String s, String startText) {
    if (isNullOrEmpty(s)) {
      return startText;
    } else if (!s.startsWith(startText)) {
      return startText + s;
    } else {
      return s;
    }
  }

  /**
   * Like {@link String#substring(int)} but allows for the {@code count} parameter to extend past
   * the string's bounds.
   */
  public static String substringGuarded(String s, int position, int count) {
    int sLength = s.length();
    if (sLength - position <= count) {
      return position == 0 ? s : s.substring(position);
    } else {
      return s.substring(position, position + count);
    }
  }

  public static String repeatString(String s, int count) {
    StringBuilder builder = new StringBuilder();
    for (int i = 0; i < count; i++) {
      builder.append(s);
    }
    return builder.toString();
  }

  /**
   * Gets a {@link String} consisting of the given number of spaces.
   *
   * <p>
   *
   * <p>NB: The result is cached in {@link #cachedSpaces}.
   *
   * @param size the number of spaces
   * @return a {@link String} consisting of {@code size} spaces
   */
  public static String getSpaces(int size) {
    if (cachedSpaces.containsKey(size)) {
      return cachedSpaces.get(size);
    }

    char[] fill = new char[size];
    for (int i = 0; i < size; i++) {
      fill[i] = ' ';
    }

    String spaces = new String(fill);
    cachedSpaces.put(size, spaces);
    return spaces;
  }

  /**
   * If this given string is of length {@code maxLength} or less, it will be returned as-is.
   * Otherwise it will be trucated to {@code maxLength}, regardless of whether there are any space
   * characters in the String. If an ellipsis is requested to be appended to the truncated String,
   * the String will be truncated so that the ellipsis will also fit within maxLength. If no
   * truncation was necessary, no ellipsis will be added.
   *
   * @param source the String to truncate if necessary
   * @param maxLength the maximum number of characters to keep
   * @param addEllipsis if true, and if the String had to be truncated, add "..." to the end of the
   *     String before returning. Additionally, the ellipsis will only be added if maxLength is
   *     greater than 3.
   * @return the original string if it's length is less than or equal to maxLength, otherwise a
   *     truncated string as mentioned above
   */
  public static String truncateAtMaxLength(String source, int maxLength, boolean addEllipsis) {

    if (source.length() <= maxLength) {
      return source;
    }
    if (addEllipsis && maxLength > 3) {
      return unicodePreservingSubstring(source, 0, maxLength - 3) + "...";
    }
    return unicodePreservingSubstring(source, 0, maxLength);
  }

  /**
   * Normalizes {@code index} such that it respects Unicode character boundaries in {@code str}.
   *
   * <p>
   *
   * <p>If {@code index} is the low surrogate of a unicode character, the method returns {@code
   * index - 1}. Otherwise, {@code index} is returned.
   *
   * <p>
   *
   * <p>In the case in which {@code index} falls in an invalid surrogate pair (e.g. consecutive low
   * surrogates, consecutive high surrogates), or if if it is not a valid index into {@code str},
   * the original value of {@code index} is returned.
   *
   * @param str the String
   * @param index the index to be normalized
   * @return a normalized index that does not split a Unicode character
   */
  private static int unicodePreservingIndex(String str, int index) {
    if (index > 0 && index < str.length()) {
      if (Character.isHighSurrogate(str.charAt(index - 1))
          && Character.isLowSurrogate(str.charAt(index))) {
        return index - 1;
      }
    }
    return index;
  }

  /**
   * Returns a substring of {@code str} that respects Unicode character boundaries.
   *
   * <p>
   *
   * <p>The string will never be split between a [high, low] surrogate pair, as defined by {@link
   * Character#isHighSurrogate} and {@link Character#isLowSurrogate}.
   *
   * <p>
   *
   * <p>If {@code begin} or {@code end} are the low surrogate of a unicode character, it will be
   * offset by -1.
   *
   * <p>
   *
   * <p>This behavior guarantees that {@code str.equals(StringUtil.unicodePreservingSubstring(str,
   * 0, n) + StringUtil.unicodePreservingSubstring(str, n, str.length())) } is true for all {@code
   * n}. </pre>
   *
   * <p>
   *
   * <p>This means that unlike {@link String#substring(int, int)}, the length of the returned
   * substring may not necessarily be equivalent to {@code end - begin}.
   *
   * @param str the original String
   * @param begin the beginning index, inclusive
   * @param end the ending index, exclusive
   * @return the specified substring, possibly adjusted in order to not split unicode surrogate
   *     pairs
   * @throws IndexOutOfBoundsException if the {@code begin} is negative, or {@code end} is larger
   *     than the length of {@code str}, or {@code begin} is larger than {@code end}
   */
  private static String unicodePreservingSubstring(String str, int begin, int end) {
    return str.substring(unicodePreservingIndex(str, begin), unicodePreservingIndex(str, end));
  }

  /**
   * Indicates whether {@code c} is one of the twenty-six uppercase ASCII alphabetic characters
   * between {@code 'A'} and {@code 'Z'} inclusive. All others (including non-ASCII characters)
   * return {@code false}.
   */
  public static boolean isUpperCase(char c) {
    return (c >= 'A') && (c <= 'Z');
  }

  /**
   * Interface that defines string utility methods used by shared code but have differing client and
   * server implementations.
   */
  public interface Implementation {
    List<String> split(String string, String separator);
  }

  private static class PureJavaImplementation implements Implementation {
    @Override
    public List<String> split(String string, String separator) {
      List<String> result = new ArrayList<>();

      int sepLength = separator.length();
      if (sepLength == 0) {
        for (int i = 0, n = string.length(); i < n; i++) {
          result.add(string.substring(i, i + 1));
        }
        return result;
      }

      int position = 0;
      while (true) {
        int index = string.indexOf(separator, position);
        if (index == -1) {
          result.add(string.substring(position));
          return result;
        }
        result.add(string.substring(position, index));
        position = index + sepLength;
      }
    }
  }

  private static class NativeImplementation implements Implementation {
    // call to native JS Split
    public static native JsArrayString nativeSplit(String s, String separator) /*-{
            return s.split(separator);
        }-*/;

    @Override
    public List<String> split(String string, String separator) {
      JsArrayString jsArrayString = nativeSplit(string, separator);

      List<String> result = new ArrayList<>();
      for (int i = 0; i < jsArrayString.length(); i++) {
        result.add(jsArrayString.get(i));
      }
      return result;
    }
  }
}
