/*
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * Copyright (c) 2007 - 2009 Pentaho Corporation and Contributors.  All rights reserved.
 */

package org.pentaho.reporting.libraries.base.util;

import java.text.Format;
import java.text.MessageFormat;
import java.util.HashSet;
import java.util.StringTokenizer;

/**
 * String utilities.
 *
 * @author Thomas Morgner.
 */
public class StringUtils
{
  // Constants used to convert a string to a boolean
  private static final String TRUE = "true";
  private static final String YES = "yes";
  private static final String ON = "on";

  /**
   * Private constructor prevents object creation.
   */
  private StringUtils()
  {
  }

  /**
   * Helper functions to query a strings start portion. The comparison is case insensitive.
   *
   * @param base  the base string.
   * @param start the starting text.
   * @return true, if the string starts with the given starting text.
   */
  public static boolean startsWithIgnoreCase(final String base, final String start)
  {
    if (base.length() < start.length())
    {
      return false;
    }
    return base.regionMatches(true, 0, start, 0, start.length());
  }

  /**
   * Helper functions to query a strings end portion. The comparison is case insensitive.
   *
   * @param base the base string.
   * @param end  the ending text.
   * @return true, if the string ends with the given ending text.
   */
  public static boolean endsWithIgnoreCase(final String base, final String end)
  {
    if (base.length() < end.length())
    {
      return false;
    }
    return base.regionMatches(true, base.length() - end.length(), end, 0, end.length());
  }

  /**
   * Queries the system properties for the line separator. If access
   * to the System properties is forbidden, the UNIX default is returned.
   *
   * @return the line separator.
   * @noinspection AccessOfSystemProperties
   */
  public static String getLineSeparator()
  {
    try
    {
      return System.getProperty("line.separator", "\n");
    }
    catch (Exception e)
    {
      return "\n";
    }
  }

  /**
   * Splits a given string on any whitespace character. Duplicate separators will be merged into a single separator
   * occurance. This implementation provides the same functionality as the REGEXP-based String.split(..) operation but
   * does not use Regular expressions and therefore it is faster and less memory consuming.
   *
   * @param string the text to be split.
   * @return the text elements as array.
   */
  public static String[] split(final String string)
  {
    return split(string, " \t\n\r\f");
  }

  /**
   * Splits a given string at the given separator string. Duplicate separators will be merged into a single separator
   * occurance.
   *
   * @param string    the text to be split.
   * @param separator the separator chacters used for the split.
   * @return the splitted array.
   */
  public static String[] split(final String string, final String separator)
  {
    if (separator == null)
    {
      throw new NullPointerException("Separator characters must not be null.");
    }
    if (string == null)
    {
      throw new NullPointerException("String to be split must not be null.");
    }

    final StringTokenizer strtok = new StringTokenizer(string, separator, false);
    final String[] tokens = new String[strtok.countTokens()];
    int i = 0;
    while (strtok.hasMoreTokens())
    {
      final String token = strtok.nextToken();
      tokens[i] = (token);
      i += 1;
    }
    return tokens;
  }

  /**
   * Splits a given string at the given separator string. Duplicate separators will be merged into a single separator
   * occurance.
   *
   * @param string    the text to be split.
   * @param separator the separator chacters used for the split.
   * @param quate     the quoting character.
   * @return the splitted array.
   */
  public static String[] split(final String string, final String separator, final String quate)
  {
    final CSVTokenizer strtok = new CSVTokenizer(string, separator, quate, false);
    final String[] tokens = new String[strtok.countTokens()];
    int i = 0;
    while (strtok.hasMoreTokens())
    {
      final String token = strtok.nextToken();
      if (token.length() > 0)
      {
        tokens[i] = (token);
        i += 1;
      }
    }
    if (i == tokens.length)
    {
      return tokens;
    }

    final String[] retval = new String[i];
    System.arraycopy(tokens, 0, retval, 0, i);
    return retval;
  }

  /**
   * Splits a given string at the given separator string. Duplicate separators will result in empty strings thus
   * preserving the number of fields specified in the original string.
   *
   * @param string    the text to be split.
   * @param separator the separator chacters used for the split.
   * @return the splitted array.
   */
  public static String[] splitCSV(final String string, final String separator)
  {
    return splitCSV(string, separator, null);
  }

  /**
   * Splits a given string at the given separator string. Duplicate separators will result in empty strings thus
   * preserving the number of fields specified in the original string.
   *
   * @param string    the text to be split.
   * @param separator the separator chacters used for the split.
   * @param quate     the quoting character.
   * @return the splitted array.
   */
  public static String[] splitCSV(final String string, final String separator, final String quate)
  {
    final CSVTokenizer strtok = new CSVTokenizer(string, separator, quate, false);
    final String[] tokens = new String[strtok.countTokens()];
    int i = 0;
    while (strtok.hasMoreTokens())
    {
      final String token = strtok.nextToken();
      tokens[i] = (token);
      i += 1;
    }
    return tokens;
  }

  /**
   * Computes a unique name using the given known-names array as filter. This method is not intended for large
   * datasets.
   *
   * @param knownNames the list of known names.
   * @param pattern the name pattern, which should have one integer slot to create derived names.
   * @return the unique name or null, if no unqiue name could be created.
   */
  public static String makeUniqueName(final String[] knownNames, final String pattern)
  {
    final HashSet knownNamesSet = new HashSet(knownNames.length);
    for (int i = 0; i < knownNames.length; i++)
    {
      final String name = knownNames[i];
      knownNamesSet.add(name);
    }

    final MessageFormat message = new MessageFormat(pattern);
    final Object[] objects = {""};
    final String plain = message.format(objects);
    if (knownNamesSet.contains(plain) == false)
    {
      return plain;
    }

    final Format[] formats = message.getFormats();
    if (formats.length == 0)
    {
      // there is no variation in this name.
      return null;
    }

    int count = 1;
    while (count < 2000000)
    {
      objects[0] = String.valueOf(count);
      final String testFile = message.format(objects);
      if (knownNamesSet.contains(testFile) == false)
      {
        return testFile;
      }
      count += 1;
    }

    return null;
  }

  /**
   * Merges the contents of the first and second array returning a array that contains only unique strings.
   * The order of the returned array is undefined.
   *
   * @param first  the first array to be merged.
   * @param second the second array to be merged.
   * @return the merged araray.
   */
  public static String[] merge(final String[] first, final String[] second)
  {
    if (first.length == 0)
    {
      return (String[]) second.clone();
    }
    if (second.length == 0)
    {
      return (String[]) first.clone();
    }
    final HashSet total = new HashSet(first.length + second.length);
    for (int i = 0; i < first.length; i++)
    {
      total.add(first[i]);
    }
    for (int i = 0; i < second.length; i++)
    {
      total.add(second[i]);
    }
    return (String[]) total.toArray(new String[total.size()]);
  }

  /**
   * Returns <code>true</code> if the source string evaulates (case insensative and trimmed) to <code>true</code>,
   * <code>yes</code>, or <code>on</code>. It will return <code>false</code> otherwise (including <code>null</code>).
   *
   * @param source the string to check
   * @return <code>true</code> if the source string evaulates to <code>true</code> or similar value,
   *         <code>false</code> otherwise.
   */
  public static boolean toBoolean(final String source)
  {
    return toBoolean(source, false);
  }

  /**
   * Returns <code>true</code> if the source string evaulates (case insensative and trimmed) to <code>true</code>,
   * <code>yes</code>, or <code>on</code>. It will return <code>false otherwise. If the source string
   * is <code>null</code>, it will return the value of the default.
   *
   * @param source      the string to check
   * @param nullDefault to value to return if the source string is <code>null</code>
   * @return <code>true</code> if the source string evaulates to <code>true</code> or similar value,
   *         <code>false</code> otherwise.
   */
  public static boolean toBoolean(String source, final boolean nullDefault)
  {
    // If the source is null, use the default
    if (source == null)
    {
      return nullDefault;
    }

    // Check for valid values
    source = source.trim().toLowerCase();
    return (TRUE.equals(source) || YES.equals(source) || ON.equals(source));
  }

  /**
   * Determines if the string is empty or <code>null</code>.
   *
   * @param source the string to check
   * @return <code>true</code> if the source string is <code>null</code> or an emptry string,
   *         <code>false</code> otherwise.
   */
  public static boolean isEmpty(final String source)
  {
    return isEmpty(source, true);
  }


  /**
   * Determines if the string is empty or <code>null</code>. If the </code>trim</code> is <code>true</code>,
   * the string will be trimmed before checking for an empty string.
   *
   * @param source the string to check
   * @param trim   indicates if the string should be trimmed before checking for an empty string.
   * @return <code>true</code> if the source string is <code>null</code> or an emptry string,
   *         <code>false</code> otherwise.
   */
  public static boolean isEmpty(final String source, final boolean trim)
  {
    if (source == null)
    {
      return true;
    }
    if (source.length() == 0)
    {
      return true;
    }
    if (trim == false)
    {
      return false;
    }
    final char[] chars = source.toCharArray();
    for (int i = 0; i < chars.length; i++)
    {
      final char c = chars[i];
      if (c > ' ')
      {
        return false;
      }
    }
    return true;
  }

  /**
   * Determines if the two Strings are equals (taking nulls into account).
   * 
   * @param s1 the first string to compare.
   * @param s2 the second string to compare.
   * @return <code>true</code> if both string are null or the contain the same value, <code>false</code>otherwise
   */
  public static boolean equals(final String s1, final String s2)
  {
    return ((s1 == null && s2 == null) || (s1 != null && s1.equals(s2)));
  }

  /**
   * Determines if the two Strings are equals ingnoring case sensitivity (taking nulls into account).
   *
   * @param s1 the first string to compare.
   * @param s2 the second string to compare.
   * @return <code>true</code> if both string are null or the contain the same case-insensitive value, <code>false</code>otherwise
   */
  public static boolean equalsIgnoreCase(final String s1, final String s2)
  {
    return ((s1 == null && s2 == null) || (s1 != null && s1.equalsIgnoreCase(s2)));
  }
}
