/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.util;

import icy.math.MathUtil;

import java.util.Comparator;

/**
 * @author stephane
 */
public class StringUtil
{
    /*
     * The Alphanum Algorithm is an improved sorting algorithm for strings
     * containing numbers. Instead of sorting numbers in ASCII order like
     * a standard sort, this algorithm sorts numbers in numeric order.
     * 
     * The Alphanum Algorithm is discussed at http://www.DaveKoelle.com
     * 
     * This library is free software; you can redistribute it and/or
     * modify it under the terms of the GNU Lesser General Public
     * License as published by the Free Software Foundation; either
     * version 2.1 of the License, or any later version.
     * 
     * This library 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.
     * 
     * You should have received a copy of the GNU Lesser General Public
     * License along with this library; if not, write to the Free Software
     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
     */
    /**
     * This is an updated version of Alphanum Algorithm Comparator
     * with enhancements made by Daniel Migowski, Andre Bogus, and David Koelle
     */
    public static class AlphanumComparator implements Comparator<String>
    {
        /** Length of string is passed in for improved efficiency (only need to calculate it once) **/
        private final String getChunk(String s, int slength, int index)
        {
            int marker = index;
            StringBuilder chunk = new StringBuilder();
            char c = s.charAt(marker);
            chunk.append(c);
            marker++;
            if (Character.isDigit(c))
            {
                while (marker < slength)
                {
                    c = s.charAt(marker);
                    if (!Character.isDigit(c))
                        break;
                    chunk.append(c);
                    marker++;
                }
            }
            else
            {
                while (marker < slength)
                {
                    c = s.charAt(marker);
                    if (Character.isDigit(c))
                        break;
                    chunk.append(c);
                    marker++;
                }
            }
            return chunk.toString();
        }

        @Override
        public int compare(String s1, String s2)
        {
            int thisMarker = 0;
            int thatMarker = 0;
            int s1Length = s1.length();
            int s2Length = s2.length();

            while (thisMarker < s1Length && thatMarker < s2Length)
            {
                String thisChunk = getChunk(s1, s1Length, thisMarker);
                thisMarker += thisChunk.length();

                String thatChunk = getChunk(s2, s2Length, thatMarker);
                thatMarker += thatChunk.length();

                // If both chunks contain numeric characters, sort them numerically
                int result = 0;
                if (Character.isDigit(thisChunk.charAt(0)) && Character.isDigit(thatChunk.charAt(0)))
                {
                    // Simple chunk comparison by length.
                    int thisChunkLength = thisChunk.length();
                    result = thisChunkLength - thatChunk.length();
                    // If equal, the first different number counts
                    if (result == 0)
                    {
                        for (int i = 0; i < thisChunkLength; i++)
                        {
                            result = thisChunk.charAt(i) - thatChunk.charAt(i);

                            if (result != 0)
                                return result;
                        }
                    }
                }
                else
                    result = thisChunk.compareTo(thatChunk);

                if (result != 0)
                    return result;
            }

            return s1Length - s2Length;
        }
    }

    /**
     * Return defaultValue if value is empty
     */
    public static String getValue(String value, String defaultValue)
    {
        if (StringUtil.isEmpty(value))
            return defaultValue;

        return value;
    }

    /**
     * Returns the next number found from specified <code>startIndex</code> in specified string.<br>
     * Returns an empty string if no number was found.
     */
    public static CharSequence getNextNumber(CharSequence text, int index)
    {
        final int len = text.length();

        // get starting digit char index
        final int st = getNextDigitCharIndex(text, index);

        // we find a digit char ?
        if (st >= 0)
        {
            // get ending digit char index
            int end = StringUtil.getNextNonDigitCharIndex(text, st);
            if (end < 0)
                end = len;

            // get value
            return text.subSequence(st, end);
        }

        return "";
    }

    /**
     * Return the index of previous digit char from specified index in specified string<br>
     * return -1 if not found
     */
    public static int getPreviousDigitCharIndex(CharSequence value, int from)
    {
        final int len = value.length();

        if (from >= len)
            return -1;

        int index = from;
        while (index >= 0)
        {
            if (Character.isDigit(value.charAt(index)))
                return index;
            index--;
        }

        return -1;
    }

    /**
     * Return the index of previous letter char from specified index in specified string<br>
     * return -1 if not found
     */
    public static int getPreviousLetterCharIndex(CharSequence value, int from)
    {
        final int len = value.length();

        if (from >= len)
            return -1;

        int index = from;
        while (index >= 0)
        {
            if (Character.isLetter(value.charAt(index)))
                return index;
            index--;
        }

        return -1;
    }

    /**
     * Return the index of previous non digit char from specified index in specified string<br>
     * return -1 if not found
     */
    public static int getPreviousNonDigitCharIndex(CharSequence value, int from)
    {
        final int len = value.length();

        if (from >= len)
            return -1;

        int index = from;
        while (index >= 0)
        {
            if (!Character.isDigit(value.charAt(index)))
                return index;
            index--;
        }

        return -1;
    }

    /**
     * Return the index of previous non letter char from specified index in specified string<br>
     * Return -1 if not found.
     */
    public static int getPreviousNonLetterCharIndex(CharSequence value, int from)
    {
        final int len = value.length();

        if (from >= len)
            return -1;

        int index = from;
        while (index >= 0)
        {
            if (!Character.isLetter(value.charAt(index)))
                return index;
            index--;
        }

        return -1;
    }

    /**
     * Return the index of next digit char from specified index in specified string<br>
     * return -1 if not found
     */
    public static int getNextDigitCharIndex(CharSequence value, int from)
    {
        final int len = value.length();

        if (from < 0)
            return -1;

        int index = from;
        while (index < len)
        {
            if (Character.isDigit(value.charAt(index)))
                return index;
            index++;
        }

        return -1;
    }

    /**
     * Return the index of next letter char from specified index in specified string<br>
     * return -1 if not found
     */
    public static int getNextLetterCharIndex(CharSequence value, int from)
    {
        final int len = value.length();

        if (from < 0)
            return -1;

        int index = from;
        while (index < len)
        {
            if (Character.isDigit(value.charAt(index)))
                return index;
            index++;
        }

        return -1;
    }

    /**
     * Return the index of next non digit char from specified index in specified string<br>
     * return -1 if not found
     */
    public static int getNextNonDigitCharIndex(CharSequence value, int from)
    {
        final int len = value.length();

        if (from < 0)
            return -1;

        int index = from;
        while (index < len)
        {
            if (!Character.isDigit(value.charAt(index)))
                return index;
            index++;
        }

        return -1;
    }

    /**
     * Return the index of next non letter char from specified index in specified string<br>
     * return -1 if not found
     */
    public static int getNextNonLetterCharIndex(CharSequence value, int from)
    {
        final int len = value.length();

        if (from < 0)
            return -1;

        int index = from;
        while (index < len)
        {
            if (!Character.isLetter(value.charAt(index)))
                return index;
            index++;
        }

        return -1;
    }

    /**
     * Return the index of next control char from specified <code>startIndex</code> in specified
     * string.<br>
     * return -1 if no control character found.
     */
    public static int getNextCtrlCharIndex(CharSequence value, int startIndex)
    {
        final int len = value.length();

        if (startIndex < 0)
            return -1;

        int index = startIndex;
        while (index < len)
        {
            if (Character.isISOControl(value.charAt(index)))
                return index;
            index++;
        }

        return -1;
    }

    /**
     * Limit the length of the specified string to maxlen.
     */
    public static String limit(String value, int maxlen, boolean tailLimit)
    {
        if (value == null)
            return null;

        final int len = value.length();

        if (len > maxlen)
        {
            // simple truncation
            if (tailLimit || (maxlen <= 8))
                return value.substring(0, maxlen - 2).trim() + "...";

            // cut center
            final int cut = (maxlen - 3) / 2;
            return value.substring(0, cut).trim() + "..." + value.substring(len - cut).trim();
        }

        return value;
    }

    /**
     * Limit the length of the specified string to maxlen.
     */
    public static String limit(String value, int maxlen)
    {
        return limit(value, maxlen, false);
    }

    /**
     * Truncate the text to a specific size, according a keyword.<br>
     * The text will be truncated around the place where the keyword is found.<br>
     * If the string is found at the beginning, the text will be like this:<br/>
     * <b><center>Lorem ipsum dolor sit amet, consec...</center><b/>
     * 
     * @param fullText
     *        : text to be truncated.
     * @param keyword
     *        : string to be found in the text and truncated around.
     * @param maxSize
     *        : max size of the string
     */
    public static String trunc(String fullText, String keyword, int maxSize)
    {
        int idx = fullText.toLowerCase().indexOf(keyword.toLowerCase());

        // key not found
        if (idx == -1)
            return "";

        String toReturn = fullText;
        int fullTextSize = fullText.length();

        if (fullTextSize > maxSize)
        {
            int firstSpaceAfter;
            String textBeforeWord;
            int lastSpaceBefore;

            // extract the full word from the text
            firstSpaceAfter = fullText.indexOf(' ', idx);
            firstSpaceAfter = firstSpaceAfter == -1 ? fullTextSize : firstSpaceAfter;

            textBeforeWord = fullText.substring(0, idx);
            lastSpaceBefore = textBeforeWord.lastIndexOf(' ');
            lastSpaceBefore = lastSpaceBefore == -1 ? 0 : lastSpaceBefore;

            // determine if we are at the beginning, the end, or at the middle
            if (idx <= maxSize / 2)
            {
                toReturn = fullText.substring(0, maxSize);
                toReturn = toReturn.trim() + "...";
            }
            else if ((fullTextSize - idx) <= maxSize / 2)
            {
                toReturn = fullText.substring(fullTextSize - maxSize, fullTextSize);
                toReturn = "..." + toReturn.trim();
            }
            else
            {
                int beginIndex = idx - maxSize / 2;
                int endIndex = idx + maxSize / 2;
                if (endIndex > fullTextSize)
                    System.out.println(endIndex);
                // beginIndex = beginIndex < 0 ? 0 : beginIndex;
                // endIndex = endIndex > fullTextSize ? fullTextSize : endIndex;
                toReturn = "..." + fullText.substring(beginIndex, endIndex).trim() + "...";
            }
        }

        return toReturn;
    }

    /**
     * Return true if the specified String are exactly the same.
     * 
     * @param trim
     *        if true then string are trimmed before comparison
     */
    public static boolean equals(String s1, String s2, boolean trim)
    {
        if (isEmpty(s1, trim))
            return isEmpty(s2, trim);
        else if (isEmpty(s2, trim))
            return false;

        if (trim)
            return s1.trim().equals(s2.trim());

        return s1.equals(s2);
    }

    /**
     * Return true if the specified String are exactly the same
     */
    public static boolean equals(String s1, String s2)
    {
        return equals(s1, s2, false);
    }

    /**
     * Return true if the specified String is empty.
     * 
     * @param trim
     *        trim the String before doing the empty test
     */
    public static boolean isEmpty(String value, boolean trim)
    {
        if (value != null)
        {
            if (trim)
                return value.trim().length() == 0;

            return value.length() == 0;
        }

        return true;
    }

    /**
     * Return true if the specified String is empty.
     * The String is trimed by default before doing the test
     */
    public static boolean isEmpty(String value)
    {
        return isEmpty(value, true);
    }

    /**
     * Try to parse a boolean from the specified String and return it.
     * Return 'def' is we can't parse any boolean from the string.
     */
    public static boolean parseBoolean(String s, boolean def)
    {
        if (s == null)
            return def;

        final String value = s.toLowerCase();

        if (value.equals(Boolean.toString(true)))
            return true;
        if (value.equals(Boolean.toString(false)))
            return false;

        return def;
    }

    /**
     * Try to parse a integer from the specified String and return it.
     * Return 'def' is we can't parse any integer from the string.
     */
    public static int parseInt(String s, int def)
    {
        try
        {
            return Integer.parseInt(s);
        }
        catch (NumberFormatException E)
        {
            return def;
        }
    }

    /**
     * Try to parse a long integer from the specified String and return it.
     * Return 'def' is we can't parse any integer from the string.
     */
    public static long parseLong(String s, long def)
    {
        try
        {
            return Long.parseLong(s);
        }
        catch (NumberFormatException E)
        {
            return def;
        }
    }

    /**
     * Try to parse a float from the specified String and return it.
     * Return 'def' is we can't parse any float from the string.
     */
    public static float parseFloat(String s, float def)
    {
        try
        {
            return Float.parseFloat(s);
        }
        catch (NumberFormatException E)
        {
            return def;
        }
    }

    /**
     * Try to parse a double from the specified String and return it.
     * Return 'def' is we can't parse any double from the string.
     */
    public static double parseDouble(String s, double def)
    {
        try
        {
            return Double.parseDouble(s);
        }
        catch (NumberFormatException E)
        {
            return def;
        }
    }

    /**
     * Try to parse a array of byte from the specified String and return it.
     * Return 'def' is we can't parse any array of byte from the string.
     */
    public static byte[] parseBytes(String s, byte[] def)
    {
        if (s == null)
            return def;

        return s.getBytes();
    }

    /**
     * Returns a <tt>String</tt> object representing the specified
     * boolean. If the specified boolean is <code>true</code>, then
     * the string {@code "true"} will be returned, otherwise the
     * string {@code "false"} will be returned.
     */
    public static String toString(boolean value)
    {
        return Boolean.toString(value);
    }

    /**
     * Returns a <code>String</code> object representing the specified integer.
     */
    public static String toString(int value)
    {
        return Integer.toString(value);
    }

    /**
     * Returns a <code>String</code> object representing the specified integer.<br>
     * If the returned String is shorter than specified length<br>
     * then leading '0' are added to the string.
     */
    public static String toString(int value, int minSize)
    {
        String result = Integer.toString(value);

        while (result.length() < minSize)
            result = "0" + result;

        return result;
    }

    /**
     * Returns a <code>String</code> object representing the specified <code>long</code>.
     */
    public static String toString(long value)
    {
        return Long.toString(value);
    }

    /**
     * Returns a string representation of the <code>float</code> argument.
     */
    public static String toString(float value)
    {
        return Float.toString(value);
    }

    /**
     * Returns a string representation of the <code>double</code> argument.
     */
    public static String toString(double value)
    {
        final int i = (int) value;

        if (i == value)
            return toString(i);

        return Double.toString(value);
    }

    /**
     * Returns a string representation of the <code>double</code> argument
     * with specified number of decimal.
     */
    public static String toString(double value, int numDecimal)
    {
        return Double.toString(MathUtil.round(value, numDecimal));
    }

    /**
     * Returns a string representation of the <code>double</code> argument with specified size :<br>
     * <code>toString(1.23456, 5)</code> --> <code>"1.2345"</code><br>
     * <code>toString(123.4567, 4)</code> --> <code>"123.4"</code><br>
     * <code>toString(1234.567, 2)</code> --> <code>"1234"</code> as we never trunk integer part.<br>
     * <code>toString(1234.5, 10)</code> --> <code>"1234.5"</code> as we never trunk integer part.<br>
     */
    public static String toStringEx(double value, int size)
    {
        final int i = (int) value;

        if (i == value)
            return toString(i);

        return Double.toString(MathUtil.roundSignificant(value, size, true));
    }

    /**
     * Return a string representation of the byte array argument.
     */
    public static String toString(byte[] value)
    {
        return new String(value);
    }

    /**
     * Returns a string representation of the integer argument as an
     * unsigned integer in base 16.
     */
    public static String toHexaString(int value)
    {
        return Integer.toHexString(value);
    }

    /**
     * Returns a string representation of the integer argument as an
     * unsigned integer in base 16.<br>
     * Force the returned string to have the specified size :<br>
     * If the string is longer then only last past is kept.<br>
     * If the string is shorter then leading 0 are added to the string.
     */
    public static String toHexaString(int value, int size)
    {
        String result = Integer.toHexString(value);

        if (result.length() > size)
            return result.substring(result.length() - size);

        while (result.length() < size)
            result = "0" + result;
        return result;
    }

    /**
     * Remove <code>count</code> characters from the end of specified string.
     */
    public static String removeLast(String value, int count)
    {
        if (value == null)
            return null;

        final int l = value.length();

        if (l < 2)
            return "";

        return value.substring(0, l - count);
    }

    /**
     * Creates a flattened version of the provided String. The flattening operation splits the
     * string by inserting spaces between words starting with an upper case letter, and converts
     * upper case letters to lower case (with the exception of the first word). Note that
     * <b>consecutive upper case letters will remain grouped</b>, as they are considered to
     * represent an acronym.<br/>
     * <br/>
     * <u>NOTE:</u> This method is optimized for class names that follow the Java naming convention. <br/>
     * Examples:<br/>
     * MyGreatClass -> "My great class"<br/>
     * MyXYZClass -> "My XYZ class"
     * 
     * @param string
     *        the string to flatten
     * @return a flattened (i.e. pretty-printed) String based on the name of the string
     */
    public static String getFlattened(String string)
    {
        String[] words = string.split("(?=[A-Z])");

        String output = words[0];
        if (words.length > 1)
        {
            // words[0] is always empty here
            output = words[1];

            for (int i = 2; i < words.length; i++)
            {
                String word = words[i];
                if (word.length() == 1)
                {
                    // single letter
                    if (words[i - 1].length() == 1)
                    {
                        // append to the previous letter (acronym)
                        output += word;
                    }
                    else
                    {
                        // new isolated letter or acronym
                        output += " " + word;
                    }
                }
                else
                    output += " " + word.toLowerCase();
            }
        }

        return output;
    }

    /**
     * Replace all C line break sequence : <code>"\n", "\r", "\r\n"</code><br>
     * from the specified <code>text</code> by <code>str</code>.
     */
    public static String replaceCR(String text, String str)
    {
        return text.replaceAll("(\r\n|\n\r|\r|\n)", str);
    }

    /**
     * Remove all C line break sequence : <code>"\n", "\r", "\r\n"</code><br>
     * from the specified text.
     */
    public static String removeCR(String text)
    {
        return replaceCR(text, "");
    }

    /**
     * Convert the C line break sequence : <code>"\n", "\r", "\r\n"</code><br>
     * to HTML line break sequence.
     */
    public static String toHtmlCR(String text)
    {
        return replaceCR(text, "<br>").replaceAll("(<BR>|<br/>|<BR/>)", "<br>");
    }

    /**
     * Return true if the specified text contains HTML line break sequence.
     */
    public static boolean containHtmlCR(String text)
    {
        return (text.indexOf("<br>") != -1) || (text.indexOf("<BR>") != -1) || (text.indexOf("<br/>") != -1)
                || (text.indexOf("<BR/>") != -1);
    }

    /**
     * Bold (inserting HTML bold tag) the specified keyword in the text.
     */
    public static String htmlBoldSubstring(String text, String keyword, boolean ignoreCase)
    {
        // right now we just ignore 'b' keyword with produce error because of the <b> sequence.
        if (!isEmpty(text) && !isEmpty(keyword) && !keyword.toLowerCase().equals("b"))
        {
            final int keywordLen = keyword.length();
            final String key;

            if (ignoreCase)
                key = keyword.toLowerCase();
            else
                key = keyword;

            String result = text;
            int index;

            if (ignoreCase)
                index = result.toLowerCase().indexOf(key);
            else
                index = result.indexOf(key);

            while (index != -1)
            {
                result = result.substring(0, index) + "<b>" + result.substring(index, index + keywordLen) + "</b>"
                        + result.substring(index + keywordLen);

                if (ignoreCase)
                    index = result.toLowerCase().indexOf(key, index + keywordLen + 6);
                else
                    index = result.indexOf(key, index + keywordLen + 6);
            }

            return result;
        }

        return text;
    }

    /**
     * Converts wildcard to regular expression.
     * 
     * @param wildcard
     * @return regex
     */
    public static String wildcardToRegex(String wildcard)
    {
        final StringBuffer s = new StringBuffer(wildcard.length());

        s.append('^');
        for (int i = 0, is = wildcard.length(); i < is; i++)
        {
            char c = wildcard.charAt(i);
            switch (c)
            {
                case '*':
                    s.append(".*");
                    break;
                case '?':
                    s.append(".");
                    break;
                case '(':
                case ')':
                case '[':
                case ']':
                case '$':
                case '^':
                case '.':
                case '{':
                case '}':
                case '|':
                case '\\':
                    s.append("\\");
                    s.append(c);
                    break;
                default:
                    s.append(c);
                    break;
            }
        }
        s.append('$');

        return (s.toString());
    }
}