// 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 com.google.collide.shared.util;

import javax.annotation.Nonnull;

import com.google.collide.json.shared.JsonArray;
import com.google.collide.json.shared.JsonIntegerMap;

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

  /**
   * Map [N] -> string of N spaces. Used by {@link #getSpaces} to cache strings
   * of spaces.
   */
  private static final JsonIntegerMap<String> cachedSpaces = JsonCollections.createIntegerMap();

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

  private static class PureJavaImplementation implements Implementation {
    @Override
    public JsonArray<String> split(String string, String separator) {
      JsonArray<String> result = JsonCollections.createArray();

      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;
      }
    }
  }

  /**
   * By default, this is a pure java implementation, but can be set to a more
   * optimized version by the client
   */
  private static Implementation implementation = new PureJavaImplementation();

  /**
   * 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 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(JsonArray<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>If {@code caseInsensitive == false} this check is equivalent
   * to {@link String#startsWith(String)}.
   *
   * <p>Otherwise {@code prefix} should be lower-case and check ignores
   * case of {@code string}.
   */
  public static boolean startsWith(
      @Nonnull String prefix, @Nonnull 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);
    }
  }

  /**
   * @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 || "".equals(s);
  }

  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 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>More specifically: empty segments will be produced for adjacent and
   * trailing separators.
   *
   * <p>If an empty string is used as the separator, the string is split
   * between each character.
   *
   * <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 JsonArray<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>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.hasKey(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>If {@code index} is the low surrogate of a unicode character,
   * the method returns {@code index - 1}. Otherwise, {@code index} is
   * returned.
   *
   * <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>The string will never be split between a [high, low] surrogate pair,
   * as defined by {@link Character#isHighSurrogate} and
   * {@link Character#isLowSurrogate}.
   *
   * <p>If {@code begin} or {@code end} are the low surrogate of a unicode
   * character, it will be offset by -1.
   *
   * <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>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));
  }
}
