// Copyright (c) 1996-2002 Brian D. Carlstrom

package bdc.util;

import java.lang.Character; // OK for javadoc bug
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
    String Utilities
*/
public final class StringUtil
{
    /**
        Determine if a String is null or empty.

        @param string a String object to check
        @return <b>true</b> if <B>string</B> is null or empty,
        <b>false</b> otherwise
    */
    public static boolean nullOrEmptyString (String string)
    {
        return (string == null) || (string.equals(""));
    }

    /**
        Determine if a string is null or empty or entirely spaces.

        @param string a String object to check
        @return <b>true</b> if <B>string</B> is null, empty, or all
        spaces; false otherwise
    */
    public static boolean nullOrEmptyOrBlankString (String string)
    {
        int length = (string != null) ? string.length() : 0;
        if (length > 0) {
            for (int i = 0; i < length; i++) {
                if (!Character.isWhitespace(string.charAt(i))) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
        Performs a binary search on a sorted string array.

        @param keys a sorted array of string keys to search
        @param key a key to search for

        @return the index of the <B>key</B> in <B>keys</B>, or -1 if
        it's not found.
    */
    public static final int stringArrayIndex (String[] keys, String key)
    {
        int lo = 0;
        int hi = keys.length - 1;
        int probe;
        int result;
        while (lo <= hi) {
            probe = (lo + hi) >> 1;
            result = key.compareTo(keys[probe]);
            if (result == 0) {
                return probe;
            }
            else if (result > 0) {
                lo = probe + 1;
            }
            else {
                hi = probe - 1;
            }
        }

        return -1;
    }

    /*
        Checks if a string matches a pattern.

        Example: stringMatchesRegexp("foo", "f.o") returns true.
    */
    public static boolean stringMatchesPattern (String string, String patternString)
    {
        try {
            Pattern pattern = Pattern.compile(patternString);
            Matcher matcher = pattern.matcher(string);
            return matcher.matches();
        }
        catch (PatternSyntaxException ex) {
            Assert.that(false,
                        "Malformed Pattern: %s",
                        SystemUtil.stackTrace(ex));
            /* NOTREACHED */
            return false;
        }
    }

    
    /**
        Search an unsorted array of strings.

        @param keys an array of string keys to search
        @param key a key to search for using the == operator

        @return the index of the <B>key</B> in <B>keys</B>, or -1 if
        it's not found.
    */
    public static final int unorderedStringArrayIndexIdentical (
        String[] keys, String key)
    {
        for (int i = 0; i < keys.length; i++) {
            if (keys[i] == key) {
                return i;
            }
        }
        return -1;
    }

    /**
        Search an unsorted array of strings.

        @param keys an array of string keys to search
        @param key a key to search for using the equals() method

        @return the index of the <B>key</B> in <B>keys</B>, or -1 if
        it's not found.
    */
    public static final int unorderedStringArrayIndex (
        String[] keys, String key)
    {
        for (int i = 0; i < keys.length; i++) {
            if (keys[i].equals(key)) {
                return i;
            }
        }
        return -1;
    }

    /**
        Tokenize a String with a specified separator character and
        return an array.

        <p>
        Takes a string in the form "Foo.Bar.Baz" with a
        delimiter of '.' and turns it into an array of strings:
        <p>
        <pre>
        path[0] = "Foo"
        path[1] = "Bar"
        path[2] = "Baz"
        </pre>

        @param str a string to tokenize
        @param delimiter the delimiter to use when tokenizing

        @return an array of Strings containing each substring but
        excluding the delimiter character

    */
    public static String[] delimitedStringToArray (String str, char delimiter)
    {
        int limit = occurs(str, delimiter);
        String[] array = new String[limit+1];

        int start = 0;
        int nextEnd;
        for (int i = 0; i < limit; i++) {
            nextEnd = str.indexOf(delimiter, start);
            array[i] = str.substring(start, nextEnd);
            start = nextEnd + 1;
        }

        array[limit] = str.substring(start, str.length());
        return array;
    }

    /**
        Count the number of occurrences of a character in a string.

        @param str the String to count in
        @param ch the character to count the occurrences of

        @return the number of times the character <B>ch</B> occurs in
        the string <B>str</B>.
    */
    public static int occurs (String str, char ch)
    {
        int count = 0;
        int offset = str.indexOf(ch, 0);
        while (offset != -1) {
            count++;
            offset = str.indexOf(ch, offset + 1);
        }
        return(count);
    }

    /**
        Count the number of occurrences of a substring in a string.

        @param str the String to count in
        @param substr the substring to count the occurrences of

        @return the number of times the String <B>substr</B> occurs in
        the string <B>str</B>. Throws a FatalAssertionException if
        <B>substr</B> is an empty string.
    */
    public static int occurs (String str, String substr)
    {
        int count = 0;
        int substrLen = substr.length();
        Assert.that(substrLen != 0, "Check substring argument.");
        int offset = str.indexOf(substr, 0);
        while (offset != -1) {
            count++;
            offset += substrLen;
            offset = str.indexOf(substr, offset);
        }
        return(count);
    }

    /**
        Count the number of occurrences of a string in an array of
        Strings.

        @param array an array of Strings to search in. There may not
        be null elements in the array.
        @param string the String to match against

        @return the number of times the String <B>string</B> occurs in
        the String array <B>array</B>.
    */
    public static int occurs (String[] array, String string)
    {
        int count = 0;
        for (int i = 0; i < array.length; i++) {
            if (array[i].equals(string)) {
                count++;
            }
        }
        return count;
    }

    /**
        Case insensitive string search for a substring.

        @param string the String to search in
        @param phrase the String to search for

        @return <b>true</b> if <B>phrase</B> is found anywhere within
        <B>string</B>, ignoring case; <b>false</b> otherwise. Returns
        true if <B>phrase</B> is empty.

    */
    public static boolean contains (String string, String phrase)
    {
        int stringLen = string.length();
        int subStringLen = phrase.length();
        for (int i = 0; i <= stringLen - subStringLen; i++) {
            if (string.regionMatches(true, i, phrase, 0, subStringLen)) {
                return true;
            }
        }
        return false;
    }

    /**
        Search for any of an array of substrings inside of the
        specified string.

        @param string the String to search in
        @param substrings the strings to search for

        @return the index of the first substring that is found inside
        the string or <code>-1</code> if the string is not found.

    */
    public static int indexOf (String string, String[] substrings)
    {
        for (int i=0; i < substrings.length; i++) {
            if (string.indexOf(substrings[i]) != -1) {
                return i;
            }
        }
        return -1;
    }

    /**
        Replace all occurrences of a character in a String with another
        character. Replaces each occurrence of <B>marker</B> in the
        string <B>string</B> with the character <B>replace</B>.

        @param string the string to do do the replacing on
        @param marker the character to remove
        @param replace the character to replace the removed character
        <b>marker</b> with

        @return a string with all occurrences of <b>marker</b> replaced
        by <b>replace</b>
    */
    public static String replaceCharByChar (
        String string, char marker, char replace)
    {
        int offset = string.indexOf(marker, 0);
        if (offset == -1) {
            return string;
        }
        FastStringBuffer buf = new FastStringBuffer(string);
        while (offset != -1) {
            buf.setCharAt(offset, replace);
            offset = string.indexOf(marker, offset+1);
        }

        return buf.toString();
    }

    /**
        Replace all occurrences of a character in a String with a
        String. Replaces each occurrence of <B>marker</B> in the string
        <B>string</B> with the String <B>replace</B>.

        @param string the string to do do the replacing on
        @param marker the character to remove
        @param replace the String to replace the removed character
        <b>marker</b> with

        @return a string with all occurrences of <b>marker</b> replaced
        by <b>replace</b>
    */
    public static String replaceCharByString (
        String string, char marker, String replace)
    {
        int replaceLength = replace.length();
        int stringLength = string.length();
        if (replaceLength == 1) {
            return (replaceCharByChar(string, marker, replace.charAt(0)));
        }
        int occurences = StringUtil.occurs(string, marker);
        if (occurences == 0) {
            return(string);
        }
        int newLength = stringLength + (occurences * (replaceLength - 1));
        FastStringBuffer buf = new FastStringBuffer(newLength);

        int oldoffset = 0;
        int offset = string.indexOf(marker, 0);
        while (offset != -1) {
            buf.appendStringRange(string, oldoffset, offset);
            buf.append(replace);
            offset += 1;
            oldoffset = offset;
            offset = string.indexOf(marker, offset);
        }
        buf.appendStringRange(string, oldoffset, stringLength);

        return buf.toString();
    }

    /**
        Replace all repeating instances of a space in a String with a single
        space.  Replaces each repeating occurrence of the space character in
        the string <B>string</B> with exactly one space character.

        @param string the string to be compressed

        @return a string with all repeating occurrences of the space character
        replaced with a single space
    */
    public static String compressSpaces (String s)
    {
        char spaceChar = ' ';
        int offset = s.indexOf(spaceChar);
        if (offset == -1) {
            return s;
        }

        int stringLength = s.length();
        FastStringBuffer buf = new FastStringBuffer();
        char currChar;
        char prevChar = s.charAt(0);
        buf.append(prevChar);
        for (int i = 1; i < stringLength; i++) {
            currChar = s.charAt(i);
            if (!Character.isSpaceChar(prevChar) ||
                !Character.isSpaceChar(currChar)) {

                buf.append(currChar);
            }

            prevChar = currChar;
        }

        return buf.toString();
    }

    /**
        Compare a character array to a string without allocating a
        temporary string.

        @param chars the characters to check against the String
        @param s the string to compare to the characters

        @return true if the char[] array <B>chars</B> would be equal
        to the String <B>s</B> were the char[] array to be a String;
        <b>false</b> is returned otherwise
    */
    public static boolean charsEqualString (char[] chars, String s)
    {
        int charLen = chars.length;
        int stringLen = s.length();
        if (charLen != stringLen) {
            return false;
        }
            // go backwards in string since our strings seem to differ
            // more at the end than the front. Many begin with
            // "bdc.util"
        for (int i = stringLen-1; i >= 0; i--)
        {
            if (chars[i] != s.charAt(i)) {
                return false;
            }
        }
        return true;
    }

    /**
        Tokenize a string separated by any separator characters into a
        List.

        Breaks up the specified string <B>s</B> into lines, and returns all
        lines in a vector. Blank lines may be optionally ignored.

        @param s the String to tokenize
        @param ignoreBlanks if <b>true</b> blank lines or lines that
        are all spaces are ignored; if <b>false</b> they are included.
        @param breakChars a string containing all characters to use as
        separator characters for the tokenize

        @return a List of the substrings, excluding the
        <b>breakChars</b>.
    */
    public static List stringToStringsListUsingBreakChars (
        String s, String breakChars)
    {
        List l = new ArrayList();
        StringTokenizer st = new StringTokenizer(s, breakChars);
        while (st.hasMoreTokens()) {
            l.add(st.nextToken());
        }
        return l;
    }

    /**
        @return a string, created by joining the contents
        of the string array, separated by the joinString
    */
    public static String join (String[] strings, String joinString)
    {
        if (strings == null) {
            return "";
        }
        if (joinString == null) {
            joinString = "";
        }

        FastStringBuffer buf = new FastStringBuffer();
        boolean isFirstTime = true;

        for (int i = 0; i < strings.length; i++) {
            if (! isFirstTime) {
                buf.append(joinString);
            }
            else {
                isFirstTime = false;
            }

            buf.append(strings[i]);
        }

        return buf.toString();
    }

    /**
        @return a string, created by joining the contents
        of the List, separated by the joinString
    */
    public static String join (List list, String joinString)
    {
        FastStringBuffer buf = new FastStringBuffer();

        for (int i = 0, s = list.size(); i < s; i++) {
            if (i != 0) {
                buf.append(joinString);
            }
            buf.append(list.get(i));
        }
        return buf.toString();
    }

    /**
        Check if a string is a legal java identifier.
        @param identifier The string to check

        @return true if identifier is legal, false otherwise.  If
        identifier is null or empty, it is considered invalid
    */
    public static boolean isJavaIdentifier (String identifier)
    {
        if (nullOrEmptyString(identifier)) {
            return false;
        }

        char[] chars = identifier.toCharArray();

        if (!Character.isJavaIdentifierStart(chars[0])) {
            return false;
        }

        for (int ii = 1; ii < chars.length; ii++) {
            if (!Character.isJavaIdentifierPart(chars[ii])) {
                return false;
            }
        }

        return true;
    }

    /**
        Check if a string starts with a specific substring in a case
        insensitive manner.  Similar to String.startsWith but ignores
        case.

        @param string the string to search in
        @param other the string to check for

        @return <b>true</b> if the first characters of <b>string</b>
        are equal to the string <b>other</b> in a case insensitive
        comparison; <b>false</b> otherwise. Returns <b>true</b> if
        <b>other</b> is an empty string.

        @see java.lang.String#startsWith
        @see java.lang.String#regionMatches(boolean, int, String, int, int)
    */
    public static boolean startsWithIgnoreCase (String string, String other)
    {
        return string.regionMatches(true, 0, other, 0, other.length());
    }

    /**
        Check if a String is composed entirely of digits using Character.isDigit

        @param string the String to check

        @return <b>true</b> if <b>string</b> is made entirely up of
        digits or is empty, <b>false</b> otherwise.

        @see java.lang.Character#isDigit
    */
    public static boolean isAllDigits (String string)
    {
        char[] chars = string.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            if (!Character.isDigit(chars[i])) {
                return false;
            }
        }
        return true;
    }

    /*
        BIT operations implemented in strings.  This is useful if you need
        more then 32 bits in a bit operations.  You might think it is only
        needed if you need more then 64 but since SQL Server doesn't do
        bit operations on numerics (only int's), you are limited to 32 if
        you plan on doing SQL operations.
    */

    /**
        Bit Set  - sets the bit with the given position in the given string
        Passing pos = 0 and value = "100" would yield "101".
    */
    public static final String bitSet (String value, int pos)
    {
        return bitOper(value, pos, false);
    }

    /**
        Bit Clear  - clear the bit with the given position in the given string
        Passing pos = 0 and value = "101" would yield "100".
    */
    public static final String bitClear (String value, int pos)
    {
        return bitOper(value, pos, true);
    }

    /**
        Bit Test  - test the bit for the given position in the given string
        Passing pos = 0 and value = "100" would yield false
        Passing pos = 1 and value = "100" would yield false
        Passing pos = 2 and value = "100" would yield true
    */
    public static final boolean bitTest (String value, int pos)
    {
        int valueLength = value.length();
        if (pos >= valueLength) {
            return false;
        }
        int charPos = valueLength - pos - 1;
        char bit = value.charAt(charPos);
        return (bit == '1');
    }

    private static final String bitOper (String value, int pos, boolean clear)
    {
        FastStringBuffer fbuf = new FastStringBuffer();
        if (value == null || value.length() == 0) {
            if (clear) {
                return "";
            }
            fbuf.append('1');
            for (int i = 0; i < pos; i++) {
                fbuf.append('0');
            }
        }
        else {
            int len = value.length();
            int i = 0;
            int indx = 0;

            if (pos < len) {
                for (i = 0; i < len - (pos + 1); i++) {
                    fbuf.append(value.charAt(i));
                }
                if (clear) {
                    fbuf.append('0');
                }
                else {
                    fbuf.append('1');
                }
                indx = i + 1;
            }
            else {
                if (clear) {
                    return value;
                }
                else {
                    fbuf.append('1');
                }
                for (i = 0; i < pos - len; i++) {
                    fbuf.append('0');
                }
                indx = 0;
            }
            for (i = indx; i < len; i++) {
                fbuf.append(value.charAt(i));
            }
        }
        return fbuf.toString();
    }
     
    /**
        Create the appropriate bit string for use in a LIKE clause for
        a string bit mask.
    */
    public static final String bit (int pint)
    {
        FastStringBuffer fbuf = new FastStringBuffer();
        fbuf.append('*');
        fbuf.append('1');
        for (int i = 0; i < pint; i++) {
            fbuf.append('_');
        }
        return fbuf.toString();
    }
    
  
    /*-- String Concatenation -----------------------------------------------*/
     
    private static final Object StrCatLock = new Object();
    private static FastStringBuffer SharedStrCatBuffer;
     
    private static FastStringBuffer buffer ()
    {
        FastStringBuffer buffer = null;
        synchronized (StrCatLock) {
            buffer = SharedStrCatBuffer;
            SharedStrCatBuffer = null;
        }
        if (buffer == null) {
            buffer = new FastStringBuffer(1024);
        }
        return buffer;
    }

    private static String finalizeBuffer (FastStringBuffer buffer)
    {
        String string = buffer.toString();
        buffer.truncateToLength(0);
        SharedStrCatBuffer = buffer;
        return string;
    }
     
    public static String strcat (String s1, String s2)
    {
        FastStringBuffer buffer = buffer();
        buffer.append(s1);
        buffer.append(s2);
        return finalizeBuffer(buffer);
    }
     
    public static String strcat (String s1, String s2, String s3)
    {
        FastStringBuffer buffer = buffer();
        buffer.append(s1);
        buffer.append(s2);
        buffer.append(s3);
        return finalizeBuffer(buffer);
    }
     
    public static String strcat (String s1, String s2, String s3, String s4)
    {
        FastStringBuffer buffer = buffer();
        buffer.append(s1);
        buffer.append(s2);
        buffer.append(s3);
        buffer.append(s4);
        return finalizeBuffer(buffer);
    }
     
    public static String strcat (String s1, String s2, String s3, String s4, String s5)
    {
        FastStringBuffer buffer = buffer();
        buffer.append(s1);
        buffer.append(s2);
        buffer.append(s3);
        buffer.append(s4);
        buffer.append(s5);
        return finalizeBuffer(buffer);
    }
     
    public static String strcat (String s1, String s2, String s3, String s4, String s5,
                                 String s6)
    {
        FastStringBuffer buffer = buffer();
        buffer.append(s1);
        buffer.append(s2);
        buffer.append(s3);
        buffer.append(s4);
        buffer.append(s5);
        buffer.append(s6);
        return finalizeBuffer(buffer);
    }

    public static String strcat (String s1, String s2, String s3, String s4, String s5,
                                 String s6, String s7)
    {
        FastStringBuffer buffer = buffer();
        buffer.append(s1);
        buffer.append(s2);
        buffer.append(s3);
        buffer.append(s4);
        buffer.append(s5);
        buffer.append(s6);
        buffer.append(s7);
        return finalizeBuffer(buffer);
    }
     
    public static String strcat (String s1, String s2, String s3, String s4, String s5,
                                 String s6, String s7, String s8)
    {
        FastStringBuffer buffer = buffer();
        buffer.append(s1);
        buffer.append(s2);
        buffer.append(s3);
        buffer.append(s4);
        buffer.append(s5);
        buffer.append(s6);
        buffer.append(s7);
        buffer.append(s8);
        return finalizeBuffer(buffer);
    }
     
    public static String strcat (String s1, String s2, String s3, String s4, String s5,
                                 String s6, String s7, String s8, String s9)
    {
        FastStringBuffer buffer = buffer();
        buffer.append(s1);
        buffer.append(s2);
        buffer.append(s3);
        buffer.append(s4);
        buffer.append(s5);
        buffer.append(s6);
        buffer.append(s7);
        buffer.append(s8);
        buffer.append(s9);
        return finalizeBuffer(buffer);
    }
    
    /**
        To remove duplicate elements from a String array <B>s</B>

        @param s a String array to unique
                
        @return a String array of unique elements
    */
    public static String[] removeDuplicates (String[] s)
    {
        return(removeDuplicates(s, null));
    }

    /**
        List.toArray needs an actual array, instead of a Class.
        stupid. See usage in removeDuplicates
    */
    private static final String[] EmptyStringArray = new String[0];
    /**
        To remove duplicate elements from a String array <B>s</B>

        @param s a String array to unique
        @param duplicateArray a list of duplicate array elements found
        in String array <B>s</B>
        
        @return a String array of unique elements
    */
    public static String[] removeDuplicates (String[] s,
                                             List     duplicateList)
    {
        Assert.that(s != null,
                    "removeDuplicates called with null s");
        
        List uniqueList = new ArrayList();
        // use hashtable for lookup for performance reasons and simultaneously 
        // construct unique StringArray 
        Map uniqueMap = new HashMap();
        Map duplicateMap = new HashMap();
        
        for (int i=0; i<s.length; i++) {
            if (!uniqueMap.containsKey(s[i])) {
                uniqueMap.put(s[i],s[i]);
                uniqueList.add(s[i]);
            }
            else {
                if (duplicateList != null) {
                    if (!duplicateMap.containsKey(s[i])) {
                        duplicateMap.put(s[i],s[i]);
                        duplicateList.add(s[i]);
                    }
                }
            }
        }
        return (String[])uniqueList.toArray(EmptyStringArray);
    }

    /**
        prevent people from creating instances of this class
    */
    private StringUtil ()
    {
    }
}
