/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */

package net.ranides.assira.text;

import java.io.IOException;
import java.io.StringReader;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;

import lombok.experimental.UtilityClass;
import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.functional.special.CharPredicate;
import net.ranides.assira.lexer.CharStreamer;
import net.ranides.assira.math.MathUtils;
import net.ranides.assira.reflection.util.ReflectUtils;
import net.ranides.assira.trace.ExceptionUtils;
import net.ranides.assira.trace.TraceUtils;

/**
 * Static utility methods for string processing.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@SuppressWarnings("PMD.TooManyPublicMethods")
@UtilityClass
public class StringUtils {

    /**
     * System dependent line separator.
     */
    public static final String ENDL = String.format("%n");
    
    private static final Pattern  REGEX_SPECIAL = Pattern.compile("([\\\\*+\\[\\](){}\\$.?\\^|])");
    
    private static final int[] CP_EMPTY = new int[0];

    private enum CharsType { STR, OCT }
    
    private static final char[] C_ESC_SEQ	= new char[]{'\033','['};
	private static final char[] C_ESC		= new char[]{'\033'};
	private static final char[] C_SLASH		= new char[]{'\\'};
	private static final char[] C_QUOTE		= new char[]{'"'};
	private static final char[] C_BKSPACE	= new char[]{'\b'};
	private static final char[] C_FORM		= new char[]{'\f'};
	private static final char[] C_TAB		= new char[]{'\t'};
	private static final char[] C_CR		= new char[]{'\r'};
	private static final char[] C_NL		= new char[]{'\n'};

    /**
     * Replaces all unsafe chars inside text by 4-digit hexcode prefixed by "quote" char.
     *
     * Parameter "chars" contains all unsafe characters.
     *
     * @param value value
     * @param quote quote
     * @param chars chars
     * @return text
     */
    public static String escape(String value, char quote, String chars) {
	    return escape(value, quote, CharPredicate.of(chars));
    }

    /**
     * Replaces all unsafe chars inside text by 4-digit hexcode prefixed by "quote" char.
     *
     * Parameter "chars" is converted to CharPredicate and used to detect unsafe characters.
     *
     * @param value value
     * @param quote quote
     * @param chars chars
     * @return text
     */
    public static String escape(String value, char quote, Pattern chars) {
        return escape(value, quote, CharPredicate.of(chars));
    }

    /**
     * Replaces all unsafe chars inside text by 4-digit hexcode prefixed by "quote" char.
     *
     * Parameter "chars" is used to detect unsafe characters.
     *
     * @param value value
     * @param quote quote
     * @param chars chars
     * @return text
     */
    public static String escape(String value, char quote, CharPredicate chars) {
        StrBuilder sb = new StrBuilder(value.length());
        for(int i=0, n=value.length(); i<n; i++) {
            char c = value.charAt(i);
            if(chars.test(c) || c==quote) {
                sb.append(quote);
                FormatHex.appendChar(sb, c);
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
    }

    /**
     * Replaces all 4-digit hexcodes prefixed by "quote" char by decoded char.
     *
     * @param value value
     * @param quote quote
     * @return text
     */
    public static String unescape(String value, char quote) {
        StrBuilder sb = new StrBuilder(value.length());
        for(int i=0, n=value.length(); i<n; i++) {
            char c = value.charAt(i);
            if(c==quote) {
                sb.append( FormatHex.parseChar(value, i+1) );
                i+=4;
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
    }

    /**
     * Puts text between quote characters: "
     * Replaces all " characters by \" sequence
     * Replaces all \ characters by \\ sequence
     *
     * @param value value
     * @return text
     */
	public static String quote(String value) {
        if (null == value || value.isEmpty()) {
            return "\"\"";
        }
        
        int offset = 1, size = quoteCount(value);
        char c;
        char[] result = new char[size];

        for(int i=0, n=value.length(); i<n; i++) {
            c = value.charAt(i);
            if( c=='\\' || c=='"') { result[offset++] = '\\'; }
            result[offset++] = c;
        }
        result[0] = '"';
        result[size-1] = '"';
        return new String(result);
    }
	
	private static int quoteCount(String value) {
        if(null == value) {
            return 0;
        }
        int size = value.length() + 2;
        char c;
        for(int i=0, n=value.length(); i<n; i++) {
            c = value.charAt(i);
            if( c=='\\' || c=='"') { size++; }
        }
        return size;
    }

    /**
     * Removes quote characters " around text
     * Replaces all \" sequences by " character
     * Replaces all \\ sequences by \ character
     *
     * @param value value
     * @return text
     */
	public static String unquote(String value) {
        String test = unquoteCheck(value);
        if(null != test) { return test; }

        // tak, obie funkcje są "brzydkie" ale kod działa
        // 5-10 razy szybciej od String#replaceAll
        // 2 razy szybciej od apache.commons.StrBuilder#replaceAll
        char[] result = new char[value.length()-2];
        return new String(result, 0, removeQuotes(result, removeSlashes(result, value) ) );
    }
    
    private static String unquoteCheck(String value) {
        if (null == value || value.isEmpty() ) {
            return "";
        }
        if("\"".equals(value)) {
            throw new IllegalArgumentException("string quotes must be opened and closed but only single quote found.");
        }
        boolean as = value.charAt(0)=='"';
        boolean at = value.charAt(value.length()-1)=='"';
        if( at!=as) {
            throw new IllegalArgumentException("string quotes must be opened and closed.");
        } else {
            return !at ? value : null;
        }
    }
    
    private static int removeSlashes(char[] buffer, String source) {
        int position    = 0;
        boolean skip    = false;

        for(int i=1, length=source.length()-1; i<length; i++) {
            char recent = source.charAt(i);
            if(recent=='\\') {
                if(skip) { 
                    skip = false; 
                } else {
                    buffer[position++] = recent;
                    skip = true;
                }
            } else {
                buffer[position++] = recent;
                skip = false;
            }
        }
        return position;
    }
    
    private static int removeQuotes(char[] buffer, int length) {
        int position    = 0;
        boolean skip       = false;
        
        for(int i=0; i<length; i++) {
            char c = buffer[i];
            if(c=='"') {
                if(skip) { 
                    skip = false; 
                    buffer[position-1] = '"';
                } else {
                    buffer[position++] = c;
                    skip = true;
                }
            } else {
                buffer[position++] = c;
                skip = (c=='\\');
            }
        }
        return position;
    }
	
	/**
     * Splits text into fragments using space character.
     * Does not split fragments inside quotes " ".
	 *
	 * @param text text
	 * @return fragments
	 */
	public static CQuery<String> split(String text) {
        if(null == text) {
            return CQuery.empty();
        }
        List<String> result = new ArrayList<>();
        boolean inquote = false;
        boolean escape = false;
        int last = 0;
        for(int i=0; i<text.length(); i++) {
            if( !escape && '\\' == text.charAt(i) ) { 
                escape = true;
                continue;
            }
            if(escape) {
                escape = false;
                continue;
            }
            if( '"' == text.charAt(i) ) { 
                inquote = !inquote; 
                continue;
            }
            if( ' ' == text.charAt(i) && !inquote ) {
                if( last-i != 0 ) {
                    result.add( unquote(text.substring(last, i)) );
                }
                last = i+1;
            }
        }
        String tail = text.substring(last);
        if(!tail.trim().isEmpty()) {
            result.add( unquote(tail) );
        }
        return CQuery.from().collection(result);
    }

    /**
     * Splits text into fragments using provided separator.
     * Does not split fragments inside quotes " ".
     *
     * @param text text
     * @param separator separator
     * @return fragments
     */
    public static CQuery<String> split(String text, String separator) {
        return CQuery.from().array(Pattern.compile(Pattern.quote(separator)).split(text));
    }

    /**
     * Replaces all well-known sequences by literal characters.
     * Especially, replaces \\ sequence with \ character.
     * Usefull, if you want to process some input string in the same way like java compiler does in source files.
     *
     * Supported sequences:
     *      \n      - NL
     *      \r      - CR
     *      \t      - TAB
     *      \f      - FORM FEED
     *      \b      - backspace
     *      \\      - \
     *      \"      - "
     *      \\e     - ESC
     *      \\^     - ESC [
     *      \\nnn   - octal sequence
     *
     * @param input input
     * @return text
     */
	public static String compile(String input) {
        if(null == input) {
            return "";
        }
		StringBuilder builder = new StringBuilder();
        
        CharStreamer<CharsType> stream = new CharStreamer<>((type, data) -> {
			switch(type) {
				case STR: 
					builder.append(data); 
					break;
				case OCT: 
					builder.append( (char)Integer.parseInt(new String(data),8) );
					break;
				default: 
					throw new AssertionError("Unsupported type: " + type);
			}
		});
        stream.put(input.toCharArray());
        
        while( !stream.end() ) {
            char c = stream.peek();
            if( c=='\\' ) {
                stream.send(CharsType.STR);
                stream.forward();
                processEsc(stream);
            } else {
                stream.forward();
            }
        }
        stream.send(CharsType.STR);
        stream.close();
        return builder.toString();
    }
    
    @SuppressWarnings("PMD.CyclomaticComplexity")
    private static void processEsc(CharStreamer<CharsType> stream) {
        if( stream.end() ) {
            throw new IllegalArgumentException("unclosed escape sequence");
        } 
        
        char code = stream.get();
        switch(code) {
            case 'n':
                stream.mark();
                stream.send(CharsType.STR, C_NL);
                break;
            case 'r':
                stream.mark();
                stream.send(CharsType.STR, C_CR);
                break;
            case 't':
                stream.mark();
                stream.send(CharsType.STR, C_TAB);
                break;
            case 'f':
                stream.mark();
                stream.send(CharsType.STR, C_FORM);
                break;
            case 'b':
                stream.mark();
                stream.send(CharsType.STR, C_BKSPACE);
                break;
            case '\\':
                stream.mark();
                stream.send(CharsType.STR, C_SLASH);
                break;
            case '"':
                stream.mark();
                stream.send(CharsType.STR, C_QUOTE);
                break;
            case 'e':
                stream.mark();
                stream.send(CharsType.STR, C_ESC);
                break;
            case '^':
                stream.mark();
                stream.send(CharsType.STR, C_ESC_SEQ);
                break;
            default:
                if(Character.isDigit(code)) {
                    processOct(stream);
                } else {
                    throw new IllegalArgumentException("unknown escape sequence: '\\" + code+"'");
                }
                break;
        } 
    }
	    
    private static boolean isOct(char ch) {
        return (ch>='0' && ch<='7');
    }
        
    private static void processOct(CharStreamer<CharsType> stream) {
        if( isOct(stream.peek()) ) { stream.forward(); }
        if( isOct(stream.peek()) ) { stream.forward(); }
        stream.send(CharsType.OCT, 1,0);
    }

    /**
     * Creates string from Unicode code points.
     * Java "char" is not capable to store all code points: some codepoints are converted to surrogate pairs.
     *
     * @param codepoints codepoints
     * @return text
     */
    public static String fromCodePoints(int[] codepoints) {
        if(null == codepoints || codepoints.length==0) {
            return "";
        }
        return new String(codepoints, 0, codepoints.length);
    }

    /**
     * Creates array with Unicode code points.
     * Java "char" is not capable to store all code points: some surrogate pairs are converted to one codepoint.
     *
     * @param value value
     * @return int[]
     */
    public static int[] toCodePoints(String value) {
        if(null == value) {
            return CP_EMPTY;
        }
        int[] a = new int[ value.codePointCount(0, value.length())];
        for(int i=0; i<a.length; i++) {
            a[i] = value.codePointAt(i);
        }
        return a;
    }

    /**
     * Removes from "text" all occurrences of "fragment"
     *
     * @param text text
     * @param fragment fragment
     * @return string
     */
    public static String remove(String text, String fragment) {
		if(null == text) {
            return "";
        }
        if(StringTraits.isEmpty(fragment)) {
			return text;
		}
		StringBuilder result = new StringBuilder( text.length() );
		int n = fragment.length();
		int p = 0;
		int c;
		while(-1 != (c = indexOf(text,fragment,p))) {
			result.append(text, p, c);
			p = c + n;
		}
		return result.append(text, p, text.length()).toString();
    }

    /**
     * Removes from "text" all occurrences of character.
     *
     * @param text text
     * @param value value
     * @return string
     */
    public static String remove(String text, char value) {
        if(null == text) {
            return "";
        }
        final int size = text.length();
        final char[] buffer = new char[size];
        char c;
        int position = 0;
        for(int i=0; i<size; i++) {
            c = text.charAt(i);
            if(c!=value) { buffer[position++] = c; }
        }
        return new String(buffer, 0, position);
    }

    /**
     * Removes "prefix" if text starts with it.
     *
     * @param text text
     * @param prefix prefix
     * @return String
     */
    public static String removePrefix(String text, String prefix) {
        if(null == text) {
            return "";
        }
        if(StringTraits.isEmpty(prefix)) {
            return text;
        }
        if(text.startsWith(prefix)) {
            return text.substring(prefix.length());
        }
        return text;
    }

    /**
     * Removes "suffix" if text ends with it.
     *
     * @param text text
     * @param suffix suffix
     * @return String
     */
    public static String removeSuffix(String text, String suffix) {
        if(null == text) {
            return "";
        }
        if(StringTraits.isEmpty(suffix)) {
            return text;
        }
        if(text.endsWith(suffix)) {
            return text.substring(0, text.length() - suffix.length());
        }
        return text;
    }

    /**
     * Replaces each group of "value" characters with only one occurence.
     *
     * @param text text
     * @param value value
     * @return String
     */
	public static String condense(String text, char value) {
        if(null == text) {
            return "";
        }
        final int size = text.length();
        final char[] buffer = new char[size];
        char c;
        boolean pc = false;
        int position = 0;
        for(int i=0; i<size; i++) {
            c = text.charAt(i);
            if(c == value) {
                if(!pc) {
                    buffer[position++] = c;
                    pc = true;
                }
            } else {
                buffer[position++] = c;
                pc = false;
            }
        }
        return new String(buffer, 0, position);
    }

    /**
     * Removes all leading whitespaces
     *
     * @param text text
     * @return String
     */
    public static String ltrim(String text) {
        return ltrim(text, Character::isWhitespace);
    }

    /**
     * Removes all leading "ch" characters
     *
     * @param text text
     * @param ch ch
     * @return String
     */
    public static String ltrim(String text, char ch) {
        return ltrim(text, c -> ch==c);
    }

    /**
     * Removes all leading characters matched by predicate
     *
     * @param text text
     * @param cp cp
     * @return String
     */
    public static String ltrim(String text, CharPredicate cp) {
        if(null == text) {
            return "";
        }
        int n = text.length(), offset = 0;
        while( offset<n && cp.test(text.charAt(offset)) ) { 
			offset++; 
		}
        return text.substring(offset);
    }

    /**
     * Removes all trailing whitespaces
     *
     * @param text text
     * @return String
     */
    public static String rtrim(String text) {
        return rtrim(text, Character::isWhitespace);
    }

    /**
     * Removes all trailing "ch" characters
     *
     * @param text text
     * @param ch ch
     * @return String
     */
    public static String rtrim(String text, char ch) {
        return rtrim(text, c -> c==ch);
    }

    /**
     * Removes all trailing characters matched by predicate
     *
     * @param text text
     * @param cp cp
     * @return String
     */
    public static String rtrim(String text, CharPredicate cp) {
        if(null == text) {
            return "";
        }
        int offset = text.length() - 1;
        while(offset >= 0 && cp.test(text.charAt(offset)) ) {
			offset--; 
		}
        return text.substring(0, offset+1);
    }

    /**
     * Removes all leading and trailing whitespaces
     *
     * @param text text
     * @return String
     */
    public static String trim(String text){
        if(null == text) {
            return "";
        }
        return text.trim();
    }

    /**
     * Removes all leading and trailing "ch" characters
     *
     * @param text text
     * @param ch ch
     * @return String
     */
    public static String trim(String text, char ch){
        return trim(text, c -> c==ch);
    }

    /**
     * Removes all leading and trailing characters matched by predicate
     *
     * @param text text
     * @param cp cp
     * @return String
     */
    public static String trim(String text, CharPredicate cp){
        if(null == text) {
            return "";
        }
        int begin = 0;
        int n = text.length();
        while( begin<n && cp.test(text.charAt(begin)) ) {
            begin++;
        }

        int end = text.length() - 1;
        while(end >= begin && cp.test(text.charAt(end)) ) {
            end--;
        }
        return text.substring(begin, end+1);
    }

    /**
     * Replaces all fragments matched by pattern by String generated by "converter".
     * "converter" function receives Matcher whith current matched fragment.
     *
     * @param text text
     * @param pattern pattern
     * @param converter converter
     * @return String
     */
	public static String replace(String text, Pattern pattern, Function<Matcher, String> converter) {
        if(null == text) {
            return "";
        }
        Matcher matcher = pattern.matcher(text);
        StringBuffer buffer = new StringBuffer();
        while(matcher.find()) {
            String str = converter.apply(matcher);
            if( null!=str ) { 
				matcher.appendReplacement(buffer, ""); 
				buffer.append(str); 
			}
        }
        matcher.appendTail(buffer);
        return buffer.toString();
    }

    /**
     * Replaces all occurences of character by another one.
     *
     * @param text text
     * @param search search
     * @param newval newval
     * @return String
     */
    public static String replace(String text, char search, char newval) {
        if(null == text) {
            return null;
        }
        final char[] buffer = text.toCharArray();
        for(int i=0; i<buffer.length; i++) {
            if(buffer[i]==search) { 
				buffer[i] = newval; 
			}
        }
        return new String(buffer);
    }

    /**
     * Replaces all occurences of character by string.
     *
     * @param text text
     * @param search search
     * @param newval newval
     * @return String
     */
    public static String replace(String text, char search, String newval) {
        if(StringTraits.isEmpty(text)) {
            return "";
        }
        int start = 0;
        int end = indexOf(text, search, start);
        if (end == -1) {
            return text;
        }
        int delta = (newval.length()-1);

        StringBuilder buf = new StringBuilder(text.length() + MathUtils.clip(16*delta, 0, 128) );
        while (end != -1) {
            buf.append(text, start, end).append(newval);
            start = end + 1;
            end = indexOf(text,search, start);
        }
        buf.append(text.substring(start));
        return buf.toString();
    }

    public static String replace(String text, int begin, int end, String newval) {
        if(begin == 0) {
            if(end == text.length()) {
                return newval;
            }
            return newval + text.substring(end);
        }
        if(end == text.length()) {
            if(begin == 0) {
                return newval;
            }
            return text.substring(0, begin) + newval;
        }
        return text.substring(0, begin) + newval + text.substring(end);
    }

    public static CharSequence replace(CharSequence text, int begin, int end, CharSequence newval) {
        if(begin == 0) {
            if(end == text.length()) {
                return newval;
            }
            return concat(newval, text.subSequence(end, text.length()));
        }
        if(end == text.length()) {
            if(begin == 0) {
                return newval;
            }
            return concat(text.subSequence(0, begin), newval);
        }
        return concat(text.subSequence(0, begin), newval, text.subSequence(end, text.length()));
    }

    /**
     * Concatenates all values using "separator".
     * Values are first converted to text using "toString" method.
     *
     * @param values values
     * @param separator separator
     * @param <T> T
     * @return String
     */
	public static <T> String join(Iterable<T> values, String separator) {
        return new StrBuilder().open("", "", separator).append(values).toString();
	}

    /**
     * Concatenates all values using "separator".
     * Values are first converted to text using provided "function"
     *
     * @param values values
     * @param separator separator
     * @param function function
     * @param <T> T
     * @return String
     */
	public static <T> String join(Iterable<? extends T> values, String separator, Function<? super T,String> function) {
        return new StrBuilder().open("", "", separator).append(values, function).toString();
	}

    /**
     * Concatenates all values using "separator".
     * Values are first converted to text using "toString" method.
     *
     * @param values values
     * @param separator separator
     * @param <T> T
     * @return String
     */
    public static <T> String join(T[] values, String separator) {
        return new StrBuilder().open("", "", separator).append(values).toString();
	}

    /**
     * Concatenates all values using "separator".
     * Values are first converted to text using provided "function"
     *
     * @param values values
     * @param separator separator
     * @param function function
     * @param <T> T
     * @return String
     */
    public static <T> String join(T[] values, String separator, Function<? super T,String> function) {
        return new StrBuilder().open("", "", separator).append(values, function).toString();
	}

    /**
     * Changes first character in text to upper case (using Locale.ROOT)
     *
     * @param text text
     * @return String
     */
	public static String capitalize(String text) {
        if(StringTraits.isEmpty(text)) {
            return "";
        }
        if( 1==text.length() ) {
            return text.toUpperCase(Locale.ROOT);
        }
        return text.substring(0, 1).toUpperCase(Locale.ROOT) + text.substring(1);
    }

    /**
     * Changes first character in text to lower case (using Locale.ROOT)
     *
     * @param text text
     * @return String
     */
    public static String uncapitalize(String text) {
        if(StringTraits.isEmpty(text)) {
            return "";
        }
        if( 1==text.length() ) {
            return text.toLowerCase(Locale.ROOT);
        }
        return text.substring(0, 1).toLowerCase(Locale.ROOT) + text.substring(1);
    }

    /**
     * Changes "text_separated_by underlines" into "textSepearatedByUnderlines"
     *
     * @param text text
     * @return String
     */
    public static String camelize(String text) {
        if(StringTraits.isEmpty(text)) {
            return "";
        }
        String[] parts = text.split("_+");
        if(0==parts.length) {
            return "";
        }
        if(1==parts.length) {
            return parts[0].toLowerCase(Locale.ROOT);
        }

        StrBuilder result = new StrBuilder(text.length()-parts.length);
		int i=0;
		for(;i<parts.length; i++) {
			if(!parts[i].isEmpty()) { break; }
		}
		result.append(parts[i++].toLowerCase(Locale.ROOT));
		
		for(;i<parts.length; i++) {
			result.append(parts[i].substring(0, 1).toUpperCase(Locale.ROOT));
			result.append(parts[i].substring(1).toLowerCase(Locale.ROOT));
		}
        return result.toString();
    }

    /**
     * Changes "textUsingCamelCase" into "text_using_camel_case"
     *
     * @param text text
     * @return String
     */
    public static String uncamelize(String text) {
        if(StringTraits.isEmpty(text)) {
            return "";
        }
        int max = text.length();
        char prv = text.charAt(0);
        boolean inu = Character.isUpperCase(prv);
        if(inu) {
            prv = Character.toLowerCase(prv);
        }
        
        CBuilder builder = new CBuilder(2*max, prv);
        
        for(int i=1; i<max; i++) {
            char c = text.charAt(i);
            if(Character.isUpperCase(c)) {
                if(inu) {
                    builder.next();
                } else {
                    builder.nexts();
                    inu = true;
                }
                builder.next(Character.toLowerCase(c));
            } else {
                if(inu) {
                    builder.separate();
                    inu = false;
                }
                builder.next();
                builder.next(c);
            }
            
        }
        return builder.end();
    }

    /**
     * Changes all lowercase character into upper and vice versa.
     *
     * @param text text
     * @return String
     */
    public static String swapcase(String text) {
        if(StringTraits.isEmpty(text)) {
            return "";
        }
        int[] array = toCodePoints(text);
        for(int i=0; i<array.length; i++) {
            if( Character.isUpperCase(array[i]) ) {
                array[i] = Character.toLowerCase(array[i]);
            }
            else if( Character.isLowerCase(array[i]) ) {
                array[i] = Character.toUpperCase(array[i]);
            }
        }
        return fromCodePoints(array);
    }
    
    static final class CBuilder {
        
        private final char[] buffer;
        private int index;
        private boolean separated;
        
        private char prv;
        
        public CBuilder(int size, char first) {
            this.buffer = new char[size];
            this.separated = true;
            this.prv = first;
        }
        
        public void next(char c) {
            this.prv = c;
        }
        
        public void separate() {
            if(!separated) {
                separated = true;
                buffer[index++] = '_';
            }
        }
        
        public void nexts() {
            buffer[index++] = prv;
            separate();
        }
        
        public void next() {
            buffer[index++] = prv;
            separated = false;
        }

        public String end() {
            buffer[index++] = prv;
            return new String(buffer, 0, index);
        }
        
    }

    /**
     * Returns substring with at most first "size" characters.
     * Semantic is identical to "limit", but accepts String.
     *
     * @param text text
     * @param size size
     * @return String
     */
	public static String clip(String text, int size) {
        if(StringTraits.isEmpty(text)) {
            return "";
        }
        return text.substring(0, MathUtils.clip(size, 0, text.length()));
    }

    /**
     * Returns subsequence with at most first "size" characters.
     * Semantic is identical to "clip", but accepts any CharSequence.
     *
     * @param text text
     * @param size size
     * @return CharSequence
     */
    public static CharSequence limit(CharSequence text, int size) {
        return new CSSub(text, 0, MathUtils.clip(size, 0, text.length()));
    }

    /**
     * Returns String left padded with "padd" character to specified size.
     *
     * @param text text
     * @param padd padd
     * @param size size
     * @return String
     */
    public static String lpadd(String text, char padd, int size) {
        if(null == text) {
            text = "";
        }
        int n = text.length();
        int d = size - n;
        if(d <= 0) {
            return text;
        }
        char[] buffer = new char[size];
        text.getChars(0, n, buffer, 0);
        for(int i=n; i<size; i++) { buffer[i] = padd; }
        return new String(buffer);
    }

    /**
     * Returns String right padded with "padd" character to specified size.
     *
     * @param text text
     * @param padd padd
     * @param size size
     * @return String
     */
    public static String rpadd(String text, char padd, int size) {
        if(null == text) {
            text = "";
        }
        int n = text.length();
        int d = size - n;
        if(d <= 0) {
            return text;
        }
        char[] buffer = new char[size];
        text.getChars(0, n, buffer, d);
        for(int i=0; i<d; i++) { buffer[i] = padd; }
        return new String(buffer);
    }

    /**
     * Returns common leading fragment of strings
     *
     * @param a a
     * @param b b
     * @return String
     */
    public static String commonPrefix(String a, String b) {
        if( StringTraits.isEmpty(a) || StringTraits.isEmpty(b) ) { 
            return ""; 
        }
        int n = Math.min(a.length(), b.length());
        for(int i=0; i<n; i++) {
            if( a.charAt(i)!=b.charAt(i) ) { return a.substring(0,i); }
        }
        return a.substring(0,n);
    }

    /**
     * Returns common leading fragment of strings
     *
     * @param values values
     * @return String
     */
    public static String commonPrefix(Collection<String> values) {
		if(values.isEmpty()) {
			return "";
		}
		return values.stream().reduce(values.iterator().next(), StringUtils::commonPrefix);
    }

    /**
     * Returns common trailing fragment of strings
     *
     * @param a a
     * @param b b
     * @return String
     */
	public static String commonSuffix(String a, String b) {
        if( StringTraits.isEmpty(a) || StringTraits.isEmpty(b) ) { 
            return ""; 
        }
		int as = a.length();
		int bs = b.length();
        int n = Math.min(as, bs);
        for(int i=1; i<=n; i++) {
            if( a.charAt(as-i)!=b.charAt(bs-i) ) { return a.substring(as-(i-1), as); }
        }
        return a.substring(as-n, as);
    }

    /**
     * Returns common trailing fragment of strings
     *
     * @param values values
     * @return String
     */
	public static String commonSuffix(Collection<String> values) {
		if(values.isEmpty()) {
			return "";
		}
		return values.stream().reduce(values.iterator().next(), StringUtils::commonSuffix);
    }

    /**
     * Returns common leading white-space fragment of strings
     *
     * @param a a
     * @param b b
     * @return String
     */
    public static String commonIndent(String a, String b) {
        if( StringTraits.isEmpty(a) || StringTraits.isEmpty(b) ) {
            return "";
        }
        int n = Math.min(a.length(), b.length());
        for(int i=0; i<n; i++) {
            if( a.charAt(i)!=b.charAt(i) || !Character.isWhitespace(a.charAt(i)) ) { return a.substring(0,i); }
        }
        return a.substring(0,n);
    }

    /**
     * Returns common leading white-space fragment of strings
     *
     * @param values values
     * @return String
     */
    public static String commonIndent(Collection<String> values) {
        if(values.isEmpty()) {
            return "";
        }
        return values.stream().filter(v -> !v.isEmpty()).reduce(values.iterator().next(), StringUtils::commonIndent);
    }

    /**
     * Returns reversed string
     *
     * @param text text
     * @return String
     */
    public static String reverse(String text) {
        return StringTraits.isEmpty(text) ? "" : new StringBuilder(text).reverse().toString();
    }

    /**
     * Returns substring between 0 and "end" fragment (excluding fragment itself).
     *
     * @param text text
     * @param end end
     * @return String
     */
    public static String before(String text, String end) {
        if(StringTraits.isEmpty(text)) {
            return "";
        }
        int iend = indexOf(text,end);
        return (-1 == iend) ? "" : text.substring(0, iend);
    }

    /**
     * Returns substring between "start" fragment (excluding fragment itself) and end.
     *
     * @param text text
     * @param start start
     * @return String
     */
    public static String after(String text, String start) {
        if(StringTraits.isEmpty(text)) {
            return "";
        }
        int istart = indexOf(text, start);
        return (-1 == istart) ? "" : text.substring(istart + start.length());
    }

    /**
     * Returns substring located between "start" and "end" fragments (exluding both of them)
     *
     * It starts after first occurrence of "start" and stops after first following "end" fragment.
     * In other words, it returns first and shortest substring matching condition.
     *
     * @param text text
     * @param start start
     * @param end end
     * @return String
     */
	public static String between(String text, String start, String end) {
        if(StringTraits.isEmpty(text)) {
            return "";
        }
        if(start == null) {
            return before(text, end);
        }
        if(end == null) {
            return after(text, start);
        }
        int istart = indexOf(text,start);
        if( istart == -1 ) {
            return "";
        }
        istart += start.length();
        int iend = indexOf(text, end, istart);
        if( iend == -1 ) {
            return "";
        }
        return text.substring(istart, iend);
    }
    
    /**
     * Extremely hacky method, which adds to Java language pseudo-support for raw string literals.
     * Usage is simple: {@code Strings.raw(/&#42;raw text here;&#42;/)}
     * <p>
     *     Comment inserted between brackes is extracted, stripped and returned by function.
     *     It works only if application has access to source files at runtime.
     *     So, essentially, it is useful in embedding data inside unit tests.
     * </p>
     * <p>
     *     Function returns normalized text. Please look to {@link #normalize(String)}.
     * </p>
     * @return text
     */
    public static String raw() {
        try {
            StackTraceElement frame = TraceUtils.getCallerFrame();
            String source = ReflectUtils.getSource(TraceUtils.getCallerType());
            int offset = 0;
            for (int i = 1, n = frame.getLineNumber(); i < n; i++) {
                offset = source.indexOf('\n', offset) + 1;
            }

            String content = source.substring(indexOf(source, "/*", offset) + 2, indexOf(source, "*/", offset));
            return normalize(content);
        } catch(IOException ioe) {
            throw ExceptionUtils.rethrow(ioe);
        }
    }

    /**
     * Function normalizes block of text in a way similar to HTML rules:
     *  - remove leading and trailing new lines
     *  - remove common indentation
     *  - returns text separated by unix new lines
     *
     * @param text text
     * @return normalized text
     */
    public static String normalize(String text) {
        String content = text;
        content = StringUtils.ltrim(content, c -> c == '\n' || c == '\r');
        content = StringUtils.rtrim(content, c -> c == '\n' || c == '\r' || Character.isWhitespace(c));
        String[] lines = content.split("(\r\n|\n|\r)");
        String indent = commonIndent(Arrays.asList(lines));
        for(int i=0; i<lines.length; i++) {
            if(!lines[i].isEmpty()) {
                lines[i] = rtrim(lines[i].substring(indent.length()));
            }
        }
        return join(lines, "\n");
    }

    /**
     * Creates CharSequence from provided array, without copying content.
     *
     * Later of modifications of array will be visible in CharSequence.
     *
     * @param data data
     * @return CharSequence
     */
	public static CharSequence wrap(char[] data) {
        return wrap(data, 0, data.length);
    }

    /**
     * Creates CharSequence from provided array, without copying content.
     * Returned sequence use characters from range [begin;end)
     *
     * Later of modifications of array will be visible in CharSequence.
     *
     * @param data data
     * @param begin begin
     * @param end end
     * @return CharSequence
     */
    public static CharSequence wrap(char[] data, int begin, int end) {
        return new CharWrapper(data, begin, end);
    }

    /**
     * Creates CharSequence with "text" repeated "n" times.
     *
     * @param n n
     * @param text text
     * @return CharSequence
     */
    public static CharSequence repeat(int n, String text) {
        if(StringTraits.isEmpty(text)) {
            return "";
        }
        return new NSSequence(n, text);
    }

    /**
     * Creates CharSequence with "chr" repeated "n" times.
     *
     * @param n n
     * @param chr chr
     * @return CharSequence
     */
    public static CharSequence repeat(int n, char chr) {
        return new NCSequence(n, chr);
    }

    public static CharSequence concat(CharSequence... fragments) {
        return new ConcatSequence(fragments);
    }

    /**
     * Returns length of the string, or zero if string is null.
     *
     * @param value value
     * @return String
     */
    public static int length(String value) {
		return null == value ? 0 : value.length();
	}

    /**
     * This implementation of {@link String#indexOf(String)} works with any CharSequence.
     * It does not convert arguments into Strings.
     * It uses Knuth–Morris–Pratt algorithm, so it guarantees complexity Θ(n)
     *
     * @param text text
     * @param pattern pattern
     * @return int
     */
    public static int indexOf(CharSequence text, CharSequence pattern) {
        return indexOf(text, pattern, 0);
    }
    
    /**
     * This implementation of {@link String#indexOf(String, int)} works with any CharSequence.
     * It does not convert arguments into Strings.
     * It uses Knuth–Morris–Pratt algorithm, so it guarantees complexity Θ(n)
     *
     * @param text text
     * @param pattern pattern
     * @param offset offset
     * @return int
     */
	public static int indexOf(CharSequence text, CharSequence pattern, int offset) {
        if(null == text) {
            text = "";
        }
        if(null == pattern || pattern.length()==0) {
            return 0;
        }
		int[] next = initKMP(pattern);
		
		int M = pattern.length();
		int N = text.length();
		int i, j;
		for (i = offset, j = 0; i < N && j < M; i++) {
			while (j >= 0 && text.charAt(i) != pattern.charAt(j)) {
				j = next[j];
			}
			j++;
		}
		return (j == M) ? i - M : -1;
	}

    /**
     * This implementation of {@link String#indexOf(int)} works with any CharSequence.
     *
     * @param text text
     * @param ch ch
     * @return int
     */
    public static int indexOf(CharSequence text, char ch) {
        return indexOf(text, ch, 0);
    }

    /**
     * This implementation of {@link String#indexOf(int, int)} works with any CharSequence.
     *
     * @param text text
     * @param ch ch
     * @param offset offset
     * @return int
     */
    public static int indexOf(CharSequence text, char ch, int offset) {
        if(null == text) {
            return -1;
        }
        for(int i=offset, n=text.length(); i<n; i++) {
            if(ch == text.charAt(i)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * This implementation of {@link String#contains(CharSequence)} works with any CharSequence.
     *
     * @param text text
     * @param pattern pattern
     * @return boolean
     */
	public static boolean contains(CharSequence text, CharSequence pattern) {
		return indexOf(text, pattern) >= 0;
	}

    /**
     * This implementation of {@link String#contains(CharSequence)} works with any CharSequence.
     *
     * @param text text
     * @param ch ch
     * @return boolean
     */
    public static boolean contains(CharSequence text, char ch) {
        return indexOf(text, ch) >= 0;
    }

    /**
     * It is null safe equivalent of {@code text.toUpperCase(Locale.ROOT)}
     *
     * @param text text
     * @return String
     */
    public static String upper(String text) {
        return null == text ? "" : text.toUpperCase(Locale.ROOT);
    }

    /**
     * It is null safe equivalent of {@code text.toLowerCase(Locale.ROOT)}
     *
     * @param text text
     * @return String
     */
    public static String lower(String text) {
        return null == text ? "" : text.toLowerCase(Locale.ROOT);
    }

    /**
     * Escapes all special characters used by regular expressions, by prefixing them with \
     *
     * Please note, that "Pattern.quote" uses special \Q \E delimiters, which is quite often enough.
     *
     * @param text text
     * @return String
     */
    public static String escapeRE(String text) {
        return REGEX_SPECIAL.matcher(text).replaceAll("\\\\$1");
    }

    /**
     * Splits string into lines.
     * newline format is supported.
     *
     * @param text text
     * @return lines
     */
    public static List<String> lines(String text) {
        try {
            return IOStrings.readLines(new StringReader(text));
        } catch (IOException e) {
            // it shouldn't happpen
            throw new UncheckedIOException(e);
        }
    }

    /**
     * Converts all newline sepearator to Unix style.
     *
     * @param text text
     * @return String
     */
    public static String asUnix(String text) {
        return text.replace("\r\n", "\n").replace('\r', '\n');
    }

    /**
     * Converts all newline sepearator to Windows/DOS style.
     *
     * @param text text
     * @return String
     */
    public static String asWindows(String text) {
        return text.replaceAll("([^\r])\n|\r([^\n])", "$1\r\n$2");
    }

    /**
     * Returns first non empty value.
     *
     * @param values values
     * @return String
     */
    public static String firstNotEmpty(String... values) {
	    for(String value : values) {
	        if(!StringTraits.isEmpty(value)) {
	            return value;
            }
        }
	    throw new NoSuchElementException();
    }

    /**
     * Returns first non blank value.
     *
     * @param values values
     * @return String
     */
    public static String firstNotBlank(String... values) {
        for(String value : values) {
            if(!StringTraits.isBlank(value)) {
                return value;
            }
        }
        throw new NoSuchElementException();
    }

    /**
     * Extracts 'length' characters from 'begin'. Both parameters accept negative values.
     * Negative value 'x' means "x before the end".
     *
     * @param text original string
     * @param begin allowed to be negative, then index is counted from the end
     * @param length allowed to negative, then length is counted as 'less than original length'
     * @return substring
     */
    public static String substr(String text, int begin, int length) {
        if(text == null || length==0) {
            return "";
        }
        if(begin < 0) {
            begin += text.length();
        }
        if(length < 0) {
            length += text.length();
        }
        int b = MathUtils.clip(begin, 0, text.length());
        int e = MathUtils.clip(begin + length, 0, text.length());
        return text.substring(b, e);
    }

    /**
     * Extracts 'length' characters from string.
     * If length is positive, extracts characters from begin.
     * If length is negative, extracts characters from the end.
     *
     * @param text original text
     * @param length number of characters
     * @return substring
     */
    public static String substr(String text, int length) {
        if (length < 0) {
            return substr(text, length, -length);
        } else {
            return substr(text, 0, length);
        }
    }

    public static Matcher lastFind(String text, String regex) {
        Pattern pattern = Pattern.compile(regex);
        Matcher hit = pattern.matcher(text);
        int start = -1;
        while(hit.find()) {
            start = hit.start();
        }
        if(start != -1) {
            hit = pattern.matcher(text);
            hit.find(start);
        }
        return hit;
    }

    static void checkIndex(int size, int index) {
        if( index<0 || index >= size ) {
            throw new StringIndexOutOfBoundsException(index);
        }
    }
    
    static void checkRange(int size, int begin, int end) {
        if (begin < 0) {
            throw new StringIndexOutOfBoundsException(begin);
        }
        if (end > size) {
            throw new StringIndexOutOfBoundsException(end);
        }
        if (end < begin) {
            throw new IllegalArgumentException("Start index (" + begin + ") is greater than end index (" + end + ")");
        }
    }

	private static int[] initKMP(CharSequence pattern) {
		int M = pattern.length();
		int[] next = new int[M];
		int j = -1;
		for (int i = 0; i < M; i++) {
			if (i == 0) {
				next[i] = -1;
			}
			else if (pattern.charAt(i) != pattern.charAt(j)) {
				next[i] = j;
			}
			else {
				next[i] = next[j];
			}
			while (j >= 0 && pattern.charAt(i) != pattern.charAt(j)) {
				j = next[j];
			}
			j++;
		}
		return next;
	}
    
    private static class NCSequence implements CharSequence {
        private final int count;
        private final char chr;

        public NCSequence(int count, char chr) {
            this.count = count;
            this.chr = chr;
        }
        @Override
        public char charAt(int index) {
            checkIndex(length(), index);
            return chr;
        }
		
        @Override
        public int length() {
            return count;
        }
		
        @Override
        public CharSequence subSequence(int start, int end) {
            checkRange(length(), start, end);
            return new NCSequence(end-start, chr);
        }

        @Override
        public String toString() {
            int n = length();
            char[] data = new char[n];
            Arrays.fill(data, chr);
            return new String(data);
        }
    }

    private static class NSSequence implements CharSequence {
        private final int start;
        private final int end;
        private final String text;

        public NSSequence(int count, String text) {
            this(text, 0, count * text.length());
        }

        public NSSequence(String text, int start, int end) {
            this.start  = start;
            this.end    = end;
            this.text   = text;
        }

        @Override
        public char charAt(int index) {
            checkIndex(length(), index);
            return text.charAt( (index+start) % text.length() );
        }

        @Override
        public int length() {
            return end-start;
        }

        @Override
        public CharSequence subSequence(int start, int end) {
			checkRange(length(), start, end);
            return new NSSequence(text, start, end);
        }

        @Override
        public String toString() {
            int n = length();
            char[] data = new char[n];
            for(int i=0; i<n; i++) { data[i] = charAt(i); }
            return new String(data);
        }

    }
    
	private static final class CharWrapper implements CharSequence {

        private final char[] data;
        private final int start;
        private final int end;

        public CharWrapper(char[] data, int start, int end) { // NOPMD wrapper
            checkRange(data.length, start, end);
            this.data = data;
            this.start = start;
            this.end = end;
        }

        @Override
        public int length() {
            return end - start;
        }

        @Override
        public char charAt(int index) {
            checkIndex(length(), index);
            return data[index + start];
        }

        @Override
        public CharSequence subSequence(int start, int end) {
            checkRange(length(), start, end);
            return new CharWrapper(data, this.start+start, this.start+end);
        }

        @Override
        public String toString() {
            return String.valueOf(data, start, length());
        }

    }
    
    private static final class CSSub implements CharSequence {
        
        private final CharSequence text;
        private final int begin;
        private final int end;
        
        public CSSub(CharSequence text, int begin, int end) {
            this.text = text;
            this.begin = begin;
            this.end = end;
        }

        @Override
        public int length() {
            return end - begin;
        }

        @Override
        public char charAt(int index) {
            return text.charAt(begin + index);
        }

        @Override
        public CharSequence subSequence(int begin, int end) {
            return new CSSub(text, this.begin+begin, this.begin+end);
        }

        @Override
        public String toString() {
            return text.subSequence(begin,end).toString();
        }
    }

    private static final class ConcatSequence implements CharSequence {
        private final CharSequence[] fragments;

        public ConcatSequence(CharSequence[] fragments) {
            this.fragments = fragments;
        }

        @Override
        public int length() {
            int out = 0;
            for(CharSequence s : fragments) {
                out += s.length();
            }
            return out;
        }

        @Override
        public char charAt(int index) {
            for(CharSequence s : fragments) {
                if(index < s.length()) {
                    return s.charAt(index);
                }
                index -= s.length();
            }
            throw new StringIndexOutOfBoundsException(index);
        }

        @Override
        public CharSequence subSequence(int start, int end) {
            return new CSSub(this, start, end);
        }

        @Override
        public String toString() {
            return StringUtils.join(fragments, "");
        }
    }
    
}
