/*
 *
 * Copyright (c) 2008, 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.
 *
 * Portions of the code may be copied from Google Collections
 * or Apache Commons projects.
 */

package zemberek.core.io;

import java.util.Arrays;
import java.util.regex.Pattern;

/**
 * Helper methods for String operations. Some of the code is copied and slightly modified from
 * Apache commons-lang  2.3
 */
public final class Strings {

  /**
   * The empty String <code>""</code>.
   */
  public static final String EMPTY_STRING = "";

  /**
   * Zero length String array.
   */
  public static final String[] EMPTY_STRING_ARRAY = new String[0];

  /**
   * <p>The maximum size to which the padding constant(s) can expand.</p>
   */
  private static final int PAD_LIMIT = 8192;
  private static final Pattern MULTI_SPACE = Pattern.compile(" +");
  private static final Pattern WHITE_SPACE_EXCEPT_SPACE = Pattern.compile("[\\t\\n\\x0B\\f\\r]");
  private static final Pattern WHITE_SPACE = Pattern.compile("\\s");

  private Strings() {
  }

  /**
   * <p>Checks if a String is empty ("") or null.</p>
   * <p/>
   * <pre>
   * Strings.isEmpty(null)      = true
   * Strings.isEmpty("")        = true
   * Strings.isEmpty(" ")       = false
   * Strings.isEmpty("bob")     = false
   * Strings.isEmpty("  bob  ") = false
   * </pre>
   *
   * @param str the String to check, may be null
   * @return <code>true</code> if the String is empty or null
   */
  public static boolean isNullOrEmpty(String str) {
    return str == null || str.length() == 0;
  }

  /**
   * checks if a string has text content other than white space.
   *
   * @param s value to check
   * @return true if it is not null, or has content other than white space white space
   */
  public static boolean hasText(String s) {
    return s != null && s.length() > 0 && s.trim().length() > 0;
  }

  /**
   * checks if all of the Strings has text (NOT null, zero length or only whitespace)
   *
   * @param strings arbitrry number of Strings.
   * @return true if ALL strings contain text.
   */
  public static boolean allHasText(String... strings) {
    checkVarargString(strings);
    for (String s : strings) {
      if (!hasText(s)) {
        return false;
      }
    }
    return true;
  }

  private static void checkVarargString(String... strings) {
    if (strings == null) {
      throw new NullPointerException("Input array should be non null!");
    }
    if (strings.length == 0) {
      throw new IllegalArgumentException("At least one parameter is required.");
    }
  }

  // ContainsNone
  //-----------------------------------------------------------------------

  /**
   * checks if all of the Strings are empty (null or zero length)
   *
   * @param strings arbitrry number of Strings.
   * @return true if all Strings are empty
   */
  public static boolean allNullOrEmpty(String... strings) {
    checkVarargString(strings);
    for (String s : strings) {
      if (!isNullOrEmpty(s)) {
        return false;
      }
    }
    return true;
  }

  /**
   * Trims white spaces from left side.
   *
   * @param s input String
   * @return a new string left white spaces trimmed. null, if <code>s</code> is null
   */
  public static String leftTrim(String s) {
    if (s == null) {
      return null;
    }
    if (s.length() == 0) {
      return EMPTY_STRING;
    }
    int j = 0;
    for (int i = 0; i < s.length(); i++) {
      if (Character.isWhitespace(s.charAt(i))) {
        j++;
      } else {
        break;
      }
    }
    return s.substring(j);
  }

  /**
   * Trims white spaces from right side.
   *
   * @param str input String
   * @return a new string right white spaces trimmed. null, if <code>str</code> is null
   */
  public static String rightTrim(String str) {
    if (str == null) {
      return null;
    }
    if (str.length() == 0) {
      return EMPTY_STRING;
    }
    int j = str.length();
    for (int i = str.length() - 1; i >= 0; --i) {
      if (Character.isWhitespace(str.charAt(i))) {
        j--;
      } else {
        break;
      }
    }
    return str.substring(0, j);
  }

  /**
   * <p>Checks that the String does not contain certain characters.</p>
   * <p/>
   * <p>A <code>null</code> String will return <code>true</code>. A <code>null</code> invalid
   * character array will return <code>true</code>. An empty String ("") always returns true.</p>
   * <p/>
   * <pre>
   * Strings.containsNone(null, *)       = true
   * Strings.containsNone(*, null)       = true
   * Strings.containsNone("", *)         = true
   * Strings.containsNone("ab", "")      = true
   * Strings.containsNone("abab", "xyz") = true
   * Strings.containsNone("ab1", "xyz")  = true
   * Strings.containsNone("abz", "xyz")  = false
   * </pre>
   *
   * @param str the String to check, may be null
   * @param invalidCharsStr string containing invalid chars
   * @return true if it contains none of the invalid chars, or is null
   * @since 2.0
   */
  public static boolean containsNone(String str, String invalidCharsStr) {
    if (str == null || invalidCharsStr == null) {
      return true;
    }
    int strSize = str.length();
    int validSize = invalidCharsStr.length();
    for (int i = 0; i < strSize; i++) {
      char ch = str.charAt(i);
      for (int j = 0; j < validSize; j++) {
        if (invalidCharsStr.charAt(j) == ch) {
          return false;
        }
      }
    }
    return true;
  }

  /**
   * Checks if the input string only contains allowedCharacters.
   *
   * @param str input String.
   * @param allowedChars allowed characters.
   * @return if String contains only the allowed characters or input values are null, returns true.
   * false otherwise.
   */
  public static boolean containsOnly(String str, String allowedChars) {
    if (str == null || allowedChars == null) {
      return true;
    }
    char[] allowed = allowedChars.toCharArray();
    for (char c : str.toCharArray()) {
      boolean found = false;
      for (char v : allowed) {
        if (c == v) {
          found = true;
          break;
        }
      }
      if (!found) {
        return false;
      }
    }
    return true;
  }

  /**
   * Builds a {@link String} by repeating a character a specified number of times.
   * <p/>
   * Author Juan Antonio Ramirez
   *
   * @param c the character to use to compose the {@link String}
   * @param count how many times to repeat the character argument
   * @return a {@link String} composed of the <code>c</code> character repeated <code>count</code>
   * times. Empty if <code>count</code> is less then 1
   */
  public static String repeat(char c, int count) {
    if (count < 1) {
      return EMPTY_STRING;
    }
    char[] chars = new char[count];
    Arrays.fill(chars, c);
    return new String(chars);
  }

  /**
   * Builds a {@link String} by repeating a string a specified number of times. Author Juan Antonio
   * Ramirez
   *
   * @param str the string to use to compose the {@link String}
   * @param count how many times to repeat the string argument
   * @return a {@link String} composed of the <code>str</code> string repeated <code>count</code>
   * times. null, if <code>str</code> is null. Empty if <code>count</code> is less then 1.
   */
  public static String repeat(String str, int count) {
    if (str == null) {
      return null;
    }
    if (count < 1) {
      return EMPTY_STRING;
    }
    StringBuilder builder = new StringBuilder(str.length() * count);
    for (int i = 0; i < count; i++) {
      builder.append(str);
    }
    return builder.toString();
  }

  /**
   * reverses a string.
   *
   * @param str input string.
   * @return reversed string. null, if <code>str</code> is null
   */
  public static String reverse(String str) {
    if (str == null) {
      return null;
    }
    if (str.length() == 0) {
      return EMPTY_STRING;
    }
    return new StringBuilder(str).reverse().toString();
  }

  /**
   * inserts the <code>stringToInsert</code> with given <code>interval</code> starting from left.
   * </p>
   * <pre>("0123456", 2, "-") returns "01-23-45-6"</pre>
   *
   * @param str input string
   * @param interval : interval amount
   * @param stringToInsert : character to insert.
   * @return the formatted string. null, if <code>str</code> is null
   * @throws IllegalArgumentException if <code>interval</code> is negative
   */
  public static String insertFromLeft(String str, int interval, String stringToInsert) {
    if (interval < 0) {
      throw new IllegalArgumentException("interval value cannot be negative.");
    }
    if (str == null || interval == 0 || interval >= str.length() || isNullOrEmpty(stringToInsert)) {
      return str;
    }
    StringBuilder b = new StringBuilder();
    int i = 0;
    for (char c : str.toCharArray()) {
      b.append(c);
      i++;
      if (i % interval == 0 && i <= str.length() - 1) {
        b.append(stringToInsert);
      }
    }
    return b.toString();
  }

  /**
   * inserts the <code>stringToInsert</code> with given <code>interval</code> starting from right.
   * </p>
   * <pre>("0123456", 2, "-") returns "0-12-34-56"</pre>
   *
   * @param str input string
   * @param interval : interval amount
   * @param stringToInsert : character to insert.
   * @return the formatted string. null, if <code>str</code> is null
   * @throws IllegalArgumentException if <code>interval</code> is negative
   */
  public static String insertFromRight(String str, int interval, String stringToInsert) {
    if (interval < 0) {
      throw new IllegalArgumentException("interval value cannot be negative.");
    }
    if (str == null || interval == 0 || interval >= str.length() || isNullOrEmpty(stringToInsert)) {
      return str;
    }
    StringBuilder b = new StringBuilder();
    int j = 0;
    for (int i = str.length() - 1; i >= 0; i--) {
      b.append(str.charAt(i));
      j++;
      if (j % interval == 0 && j <= str.length() - 1) {
        b.append(stringToInsert);
      }
    }
    return reverse(b.toString());
  }

  /**
   * <p>Right pad a String with spaces (' ').</p>
   * <p/>
   * <p>The String is padded to the size of <code>size</code>.</p>
   * <p/>
   * <pre>
   * StringUtils.rightPad(null, *)   = null
   * StringUtils.rightPad("", 3)     = "   "
   * StringUtils.rightPad("bat", 3)  = "bat"
   * StringUtils.rightPad("bat", 5)  = "bat  "
   * StringUtils.rightPad("bat", 1)  = "bat"
   * StringUtils.rightPad("bat", -1) = "bat"
   * </pre>
   *
   * @param str the String to pad out, may be null
   * @param size the size to pad to
   * @return right padded String or original String if no padding is necessary, <code>null</code> if
   * null String input
   */
  public static String rightPad(String str, int size) {
    return rightPad(str, size, ' ');
  }

  /**
   * <p>Right pad a String with a specified character.</p>
   * <p/>
   * <p>The String is padded to the size of <code>size</code>.</p>
   * <p/>
   * <pre>
   * StringUtils.rightPad(null, *, *)     = null
   * StringUtils.rightPad("", 3, 'z')     = "zzz"
   * StringUtils.rightPad("bat", 3, 'z')  = "bat"
   * StringUtils.rightPad("bat", 5, 'z')  = "batzz"
   * StringUtils.rightPad("bat", 1, 'z')  = "bat"
   * StringUtils.rightPad("bat", -1, 'z') = "bat"
   * </pre>
   *
   * @param str the String to pad out, may be null
   * @param size the size to pad to
   * @param padChar the character to pad with
   * @return right padded String or original String if no padding is necessary, <code>null</code> if
   * null String input
   * @since 2.0
   */
  public static String rightPad(String str, int size, char padChar) {
    if (str == null) {
      return null;
    }
    int pads = size - str.length();
    if (pads <= 0) {
      return str; // returns original String when possible
    }
    if (pads > PAD_LIMIT) {
      return rightPad(str, size, String.valueOf(padChar));
    }
    return str.concat(repeat(padChar, pads));
  }

  /**
   * <p>Right pad a String with a specified String.</p>
   * <p/>
   * <p>The String is padded to the size of <code>size</code>.</p>
   * <p/>
   * <pre>
   * StringUtils.rightPad(null, *, *)      = null
   * StringUtils.rightPad("", 3, "z")      = "zzz"
   * StringUtils.rightPad("bat", 3, "yz")  = "bat"
   * StringUtils.rightPad("bat", 5, "yz")  = "batyz"
   * StringUtils.rightPad("bat", 8, "yz")  = "batyzyzy"
   * StringUtils.rightPad("bat", 1, "yz")  = "bat"
   * StringUtils.rightPad("bat", -1, "yz") = "bat"
   * StringUtils.rightPad("bat", 5, null)  = "bat  "
   * StringUtils.rightPad("bat", 5, "")    = "bat  "
   * </pre>
   *
   * @param str the String to pad out, may be null
   * @param size the size to pad to
   * @param padStr the String to pad with, null or empty treated as single space
   * @return right padded String or original String if no padding is necessary, <code>null</code> if
   * null String input
   */
  public static String rightPad(String str, int size, String padStr) {
    if (str == null) {
      return null;
    }
    if (isNullOrEmpty(padStr)) {
      padStr = " ";
    }
    int padLen = padStr.length();
    int strLen = str.length();
    int pads = size - strLen;
    if (pads <= 0) {
      return str; // returns original String when possible
    }
    if (padLen == 1 && pads <= PAD_LIMIT) {
      return rightPad(str, size, padStr.charAt(0));
    }

    if (pads == padLen) {
      return str.concat(padStr);
    } else if (pads < padLen) {
      return str.concat(padStr.substring(0, pads));
    } else {
      char[] padding = new char[pads];
      char[] padChars = padStr.toCharArray();
      for (int i = 0; i < pads; i++) {
        padding[i] = padChars[i % padLen];
      }
      return str.concat(new String(padding));
    }
  }

  /**
   * <p>Left pad a String with spaces (' ').</p>
   * <p/>
   * <p>The String is padded to the size of <code>size<code>.</p>
   * <p/>
   * <pre>
   * StringUtils.leftPad(null, *)   = null
   * StringUtils.leftPad("", 3)     = "   "
   * StringUtils.leftPad("bat", 3)  = "bat"
   * StringUtils.leftPad("bat", 5)  = "  bat"
   * StringUtils.leftPad("bat", 1)  = "bat"
   * StringUtils.leftPad("bat", -1) = "bat"
   * </pre>
   *
   * @param str the String to pad out, may be null
   * @param size the size to pad to
   * @return left padded String or original String if no padding is necessary, <code>null</code> if
   * null String input
   */
  public static String leftPad(String str, int size) {
    return leftPad(str, size, " ");
  }

  /**
   * <p>Left pad a String with a specified character.</p>
   * <p/>
   * <p>Pad to a size of <code>size</code>.</p>
   * <p/>
   * <pre>
   * StringUtils.leftPad(null, *, *)     = null
   * StringUtils.leftPad("", 3, 'z')     = "zzz"
   * StringUtils.leftPad("bat", 3, 'z')  = "bat"
   * StringUtils.leftPad("bat", 5, 'z')  = "zzbat"
   * StringUtils.leftPad("bat", 1, 'z')  = "bat"
   * StringUtils.leftPad("bat", -1, 'z') = "bat"
   * </pre>
   *
   * @param str the String to pad out, may be null
   * @param size the size to pad to
   * @param padChar the character to pad with
   * @return left padded String or original String if no padding is necessary, <code>null</code> if
   * null String input
   * @since 2.0
   */
  public static String leftPad(String str, int size, char padChar) {
    if (str == null) {
      return null;
    }
    int pads = size - str.length();
    if (pads <= 0) {
      return str; // returns original String when possible
    }
    if (pads > PAD_LIMIT) {
      return leftPad(str, size, String.valueOf(padChar));
    }
    return repeat(padChar, pads).concat(str);
  }

  /**
   * returns the initial part of a string until the first occurance of a given string. </p>
   * <pre>
   * ("hello","lo") -> hel
   * ("hello", "zo") -> hello
   * ("hello", "hello") -> "" empty string.
   * ("hello",null)-> hello
   * (null,"hello")-> null
   * (null,null)-> null
   * </pre>
   *
   * @param str input string
   * @param s string to search first occurance.
   * @return the substring
   */
  public static String subStringUntilFirst(String str, String s) {
    if (isNullOrEmpty(str) || isNullOrEmpty(s)) {
      return str;
    }
    final int pos = str.indexOf(s);
    if (pos < 0) {
      return str;
    } else {
      return str.substring(0, pos);
    }
  }

  /**
   * returns the initial part of a string until the last occurance of a given string. </p>
   * <pre>
   * ("hellohello","lo") -> hellohel
   * ("hellohello","el") -> helloh
   * ("hellolo", "zo") -> hellolo
   * ("hello", "hello") -> "" empty string.
   * ("hello",null)-> hello
   * (null,"hello")-> null
   * (null,null)-> null
   * </pre>
   *
   * @param str input string
   * @param s string to search last occurance.
   * @return the substring
   */
  public static String subStringUntilLast(String str, String s) {
    if (isNullOrEmpty(str) || isNullOrEmpty(s)) {
      return str;
    }
    final int pos = str.lastIndexOf(s);
    if (pos < 0) {
      return str;
    }
    return str.substring(0, pos);
  }

  /**
   * <p>returns the last part of a string after the first occurance of a given string.</p> </p>
   * <pre>
   * ("hello","el") -> lo
   * ("hellohello","el") -> lohello
   * ("hello", "zo") -> hello
   * ("hello", "hello") -> "" empty string.
   * ("hello",null)-> hello
   * (null,"hello")-> null
   * (null,null)-> null
   * </pre>
   *
   * @param str input string
   * @param s string to search first occurance.
   * @return the substring
   */
  public static String subStringAfterFirst(String str, String s) {
    if (isNullOrEmpty(str) || isNullOrEmpty(s)) {
      return str;
    }
    final int pos = str.indexOf(s);
    if (pos < 0) {
      return str;
    } else {
      return str.substring(pos + s.length());
    }
  }

  /**
   * returns the last part of a string after the last occurance of a given string. </p>
   * <pre>
   * ("hello","el") -> lo
   * ("hellohello","el") -> lo
   * ("hello", "zo") -> hello
   * ("hello", "hello") -> "" empty string.
   * ("hello",null)-> hello
   * (null,"hello")-> null
   * (null,null)-> null
   * </pre>
   *
   * @param str input string
   * @param s string to search first occurance.
   * @return the substring
   */
  public static String subStringAfterLast(String str, String s) {
    if (isNullOrEmpty(str) || isNullOrEmpty(s)) {
      return str;
    }
    final int pos = str.lastIndexOf(s);
    if (pos < 0) {
      return str;
    } else {
      return str.substring(pos + s.length());
    }
  }

  /**
   * <p>Left pad a String with a specified String.</p>
   * <p/>
   * <p>Pad to a size of <code>size</code>.</p>
   * <p/>
   * <pre>
   * StringUtils.leftPad(null, *, *)      = null
   * StringUtils.leftPad("", 3, "z")      = "zzz"
   * StringUtils.leftPad("bat", 3, "yz")  = "bat"
   * StringUtils.leftPad("bat", 5, "yz")  = "yzbat"
   * StringUtils.leftPad("bat", 8, "yz")  = "yzyzybat"
   * StringUtils.leftPad("bat", 1, "yz")  = "bat"
   * StringUtils.leftPad("bat", -1, "yz") = "bat"
   * StringUtils.leftPad("bat", 5, null)  = "  bat"
   * StringUtils.leftPad("bat", 5, "")    = "  bat"
   * </pre>
   *
   * @param str the String to pad out, may be null
   * @param size the size to pad to
   * @param padStr the String to pad with, null or empty treated as single space
   * @return left padded String or original String if no padding is necessary, <code>null</code> if
   * null String input
   */
  public static String leftPad(String str, int size, String padStr) {
    if (str == null) {
      return null;
    }
    if (isNullOrEmpty(padStr)) {
      padStr = " ";
    }
    int padLen = padStr.length();
    int strLen = str.length();
    int pads = size - strLen;
    if (pads <= 0) {
      return str; // returns original String when possible
    }
    if (pads == padLen) {
      return padStr.concat(str);
    } else if (pads < padLen) {
      return padStr.substring(0, pads).concat(str);
    } else {
      char[] padding = new char[pads];
      char[] padChars = padStr.toCharArray();
      for (int i = 0; i < pads; i++) {
        padding[i] = padChars[i % padLen];
      }
      return new String(padding).concat(str);
    }
  }

  /**
   * Converts all white spaces to single space. Also eliminates multiple spaces, </p>
   * <pre>
   * "  a  aaa \t \n    a\taa  " -> " a aaa a aa "
   * </pre>
   *
   * @param str input string.
   * @return all white spaces are converted to space character and multiple space chars reduced to
   * single space. returns null if <code>str<code> is null
   */
  public static String whiteSpacesToSingleSpace(String str) {
    if (str == null) {
      return null;
    }
    if (str.isEmpty()) {
      return str;
    }
    return MULTI_SPACE.matcher(WHITE_SPACE_EXCEPT_SPACE.matcher(str).replaceAll(" "))
        .replaceAll(" ");
  }

  /**
   * Eliminates all white spaces.
   *
   * @param str input string.
   * @return returns the string after all white spaces are stripped. null, if str is null
   */
  public static String eliminateWhiteSpaces(String str) {
    if (str == null) {
      return null;
    }
    if (str.isEmpty()) {
      return str;
    }
    return WHITE_SPACE.matcher(str).replaceAll("");
  }

  /**
   * Generates 'gram' Strings from a given String. Such as, </p>
   * <pre>
   * for ("hello",2) it returns ["he","el","ll","lo"]
   * for ("hello",3) it returns ["hel","ell","llo"]
   * </pre>
   *
   * @param word input String
   * @param gramSize size of the gram.
   * @return the grams as an array. if the gram size is larger than the word itself, it retuns an
   * empty array. gram size cannot be smaller than 1
   * @throws IllegalArgumentException if gram size is smaller than 1
   */
  public static String[] separateGrams(String word, int gramSize) {
    if (gramSize < 1) {
      throw new IllegalArgumentException("Gram size cannot be smaller than 1");
    }
    if (gramSize > word.length()) {
      return EMPTY_STRING_ARRAY;
    }
    String[] grams = new String[word.length() - gramSize + 1];
    for (int i = 0; i <= word.length() - gramSize; i++) {
      grams[i] = word.substring(i, i + gramSize);
    }
    return grams;
  }

}
