/*
 * @author Ranides Atterwim <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.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.collection.query.CQueryBuilder;
import net.ranides.assira.functional.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;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
@SuppressWarnings("PMD.TooManyPublicMethods")
public final class StringUtils {
    
    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'};

	private StringUtils() {
		/* utility class */
	}
	
	public static String quote(String value) {
        if (null == value || value.isEmpty()) {
            return "\"\"";
        }
        
        int offset = 1, size = quoteCount(value);
        char c, 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;
    }
	
	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;
    }
	
	/**
	 * Rozbija tekst jako separatora używając spacji. Fragmenty wzięte w " " traktuje jako całość.
	 * @param text
	 * @return 
	 */
	public static CQuery<String> split(String text) {
        if(null == text) {
            return CQueryBuilder.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 CQueryBuilder.fromCollection(result);
    }
    
    public static CQuery<String> split(String text, String separator) {
        return CQueryBuilder.fromArray(Pattern.compile(Pattern.quote(separator)).split(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.open(input.toCharArray());
        
        while( !stream.end() ) {
            char c = stream.peek();
            if( c=='\\' ) {
                stream.flush(CharsType.STR);
                stream.skip();
                processEsc(stream);
            } else {
                stream.skip();
            }
        }
        stream.flush(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.flush();
                stream.send(CharsType.STR, C_NL);
                break;
            case 'r':
                stream.flush();
                stream.send(CharsType.STR, C_CR);
                break;
            case 't':
                stream.flush();
                stream.send(CharsType.STR, C_TAB);
                break;
            case 'f':
                stream.flush();
                stream.send(CharsType.STR, C_FORM);
                break;
            case 'b':
                stream.flush();
                stream.send(CharsType.STR, C_BKSPACE);
                break;
            case '\\':
                stream.flush();
                stream.send(CharsType.STR, C_SLASH);
                break;
            case '"':
                stream.flush();
                stream.send(CharsType.STR, C_QUOTE);
                break;
            case 'e':
                stream.flush();
                stream.send(CharsType.STR, C_ESC);
                break;
            case '^':
                stream.flush();
                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.skip(); } 
        if( isOct(stream.peek()) ) { stream.skip(); } 
        stream.flush(CharsType.OCT, 1,0);
    }
	
    public static String fromCodePoints(int[] codepoints) {
        if(null == codepoints || codepoints.length==0) {
            return "";
        }
        return new String(codepoints, 0, codepoints.length);
    }
    
    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;
    }
    
    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();
    }

    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);
    }
	
    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;
    }

    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;
    }
	
	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);
    }
	
    public static String ltrim(String text) {
        return ltrim(text, Character::isWhitespace);
    }
    
    public static String ltrim(String text, char ch) {
        return ltrim(text, c -> ch==c);
    }
    
    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);
    }
    
    public static String rtrim(String text) {
        return rtrim(text, Character::isWhitespace);
    }
    
    public static String rtrim(String text, char ch) {
        return rtrim(text, c -> c==ch);
    }

    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);
    }

    public static String trim(String text){
        if(null == text) {
            return "";
        }
        return text.trim();
    }
	
	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();
    }

    public static String replace(String text, char search, char newval) {
        if(null == text) {
            return "";
        }
        final char[] buffer = text.toCharArray();
        for(int i=0; i<buffer.length; i++) {
            if(buffer[i]==search) { 
				buffer[i] = newval; 
			}
        }
        return new String(buffer);
    }

    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.substring(start, end)).append(newval);
            start = end + 1;
            end = indexOf(text,search, start);
        }
        buf.append(text.substring(start));
        return buf.toString();
    }
	
	public static <T> String join(Iterable<T> values, String separator) {
        return new StrBuilder().open("", "", separator).append(values).toString();
	}
	
	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();
	}
	
	public static <T> String join(T[] values, String separator) {
        return new StrBuilder().open("", "", separator).append(values).toString();
	}
	
	public static <T> String join(T[] values, String separator, Function<? super T,String> function) {
        return new StrBuilder().open("", "", separator).append(values, function).toString();
	}
	
	public static String capitalize(String text) {
        if(StringTraits.isEmpty(text)) {
            return "";
        }
        if( 1==text.length() ) {
            return text.toUpperCase(Locale.ROOT);
        }
        return new StringBuilder(text.length())
            .append( text.substring(0, 1).toUpperCase(Locale.ROOT) )
            .append(text.substring(1))
            .toString();
    }
	
    public static String uncapitalize(String text) {
        if(StringTraits.isEmpty(text)) {
            return "";
        }
        if( 1==text.length() ) {
            return text.toLowerCase(Locale.ROOT);
        }
        return new StringBuilder(text.length())
            .append( text.substring(0, 1).toLowerCase(Locale.ROOT) )
            .append(text.substring(1))
            .toString();
    }
	
    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();
    }
    
    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();
    }
    
    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);
        }
        
    }
    
	public static String clip(String text, int size) {
        if(StringTraits.isEmpty(text)) {
            return "";
        }
        return text.substring(0, MathUtils.clip(size, 0, text.length()));
    }
    
    public static CharSequence limit(CharSequence text, int size) {
        return new CSSub(text, 0, MathUtils.clip(size, 0, text.length()));
    }
	
    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);
    }

    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);
    }
	
    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);
    }

    public static String commonPrefix(Collection<String> values) {
		if(values.isEmpty()) {
			return "";
		}
		return values.stream().reduce(values.iterator().next(), StringUtils::commonPrefix);
    }
	
	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);
    }
	
	public static String commonSuffix(Collection<String> values) {
		if(values.isEmpty()) {
			return "";
		}
		return values.stream().reduce(values.iterator().next(), StringUtils::commonSuffix);
    }
	
	public static String reverse(String text) {
        return StringTraits.isEmpty(text) ? "" : new StringBuilder(text).reverse().toString();
    }
	
    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);
    }
    
    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());
    }
	
	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);
    }
    
    /**
     * Cholernie przekombinowana metoda, która dodaje do javy pseudo-support dla
     * psuedo-raw-string-literals. Używa się jej prosto: <code>Strings.raw(/&#42raw text here;&#42;/)</code>
     * <p>
     * Komentarz wrzucony pomiędzy nawiasy jest przetwarzany i zwracany przez
     * funkcję. Żeby to działało, aplikacja musi mieć w runtime dostęp do plików
     * źródłowych za pomocą ClassLoadera.
     * </p>
     * @return 
     */
    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;
            }
            return source.substring(indexOf(source, "/*", offset) + 2, indexOf(source, "*/", offset));
        } catch(IOException ioe) {
            throw ExceptionUtils.rethrow(ioe);
        }
    }
	
	public static CharSequence wrap(char[] data) {
        return wrap(data, 0, data.length);
    }
    
    public static CharSequence wrap(char[] data, int begin, int end) {
        return new CharWrapper(data, begin, end);
    }
	
    public static CharSequence repeat(int n, String text) {
        if(StringTraits.isEmpty(text)) {
            return "";
        }
        return new NSSequence(n, text);
    }

    public static CharSequence repeat(int n, char chr) {
        return new NCSequence(n, chr);
    }
    
    public static int length(String value) {
		return null == value ? 0 : value.length();
	}
        
    public static int indexOf(CharSequence text, CharSequence pattern) {
        return indexOf(text, pattern, 0);
    }
    
    /**
     * We use Knuth–Morris–Pratt algorithm. Complexity: Θ(n)
     * @param text
     * @param pattern
     * @param offset
     * @return 
     */
	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;
	}
    
    public static int indexOf(CharSequence text, char ch) {
        return indexOf(text, ch, 0);
    }
    
    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;
    }
	
	public static boolean contains(CharSequence text, CharSequence pattern) {
		return indexOf(text, pattern) >= 0;
	}
    
    public static boolean contains(CharSequence text, char ch) {
        return indexOf(text, ch) >= 0;
    }
    
    public static String escapeRE(String text) {
        return REGEX_SPECIAL.matcher(text).replaceAll("\\\\$1");
    }
    
    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();
        }
    }
    
}
