/*
 * Copyright 2013-2014 Richard M. Hightower
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  		http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * __________                              _____          __   .__
 * \______   \ ____   ____   ____   /\    /     \ _____  |  | _|__| ____    ____
 *  |    |  _//  _ \ /  _ \ /    \  \/   /  \ /  \\__  \ |  |/ /  |/    \  / ___\
 *  |    |   (  <_> |  <_> )   |  \ /\  /    Y    \/ __ \|    <|  |   |  \/ /_/  >
 *  |______  /\____/ \____/|___|  / \/  \____|__  (____  /__|_ \__|___|  /\___  /
 *         \/                   \/              \/     \/     \/       \//_____/
 *      ____.                     ___________   _____    ______________.___.
 *     |    |____ ___  _______    \_   _____/  /  _  \  /   _____/\__  |   |
 *     |    \__  \\  \/ /\__  \    |    __)_  /  /_\  \ \_____  \  /   |   |
 * /\__|    |/ __ \\   /  / __ \_  |        \/    |    \/        \ \____   |
 * \________(____  /\_/  (____  / /_______  /\____|__  /_______  / / ______|
 *               \/           \/          \/         \/        \/  \/
 */

package org.boon.primitive;

import org.boon.Lists;
import org.boon.Maps;
import org.boon.Str;
import org.boon.cache.Cache;
import org.boon.cache.CacheType;
import org.boon.cache.SimpleCache;
import org.boon.core.Conversions;
import org.boon.core.Dates;
import org.boon.core.TypeType;
import org.boon.core.reflection.FastStringUtils;
import org.boon.core.reflection.Mapper;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.*;

import static org.boon.Exceptions.die;
import static org.boon.Lists.toListOrSingletonList;
import static org.boon.primitive.CharScanner.*;

/**
 * This class is like a StringBuilder but it also pretends to be a PrintWriter.
 * The class existed long before I added PrintWriter cababilities.
 * I did look at the PrintWriter code but any code in this class should be a derivative product.
 * If I copied and/or emulated any technique from PrintWriter, be assured that I give them full credit.
 *
 * I believe at all times the usage was small enough to be covered under "fair use".
 * But to remove any doubt, the comments in question have been deleted.
 *
 */
public class CharBuf extends PrintWriter implements CharSequence {
    protected int capacity = 16;
    protected int location = 0;

    protected char[] buffer;

    public CharBuf( char[] buffer ) {
        super(writer());
        this.buffer = buffer;
        this.capacity = buffer.length;
    }

    public CharBuf( byte[] bytes ) {

        super(writer());

        String str = new String(bytes, StandardCharsets.UTF_8);
        this.buffer = FastStringUtils.toCharArray(str);
        this.location = buffer.length;
        this.capacity = buffer.length;

    }

    public static CharBuf createExact( final int capacity ) {
        return new CharBuf( capacity ) {
            public CharBuf add( char[] chars ) {
                Chr._idx( buffer, location, chars );
                location += chars.length;
                return this;
            }
        };
    }

    public static CharBuf create( int capacity ) {
        return new CharBuf( capacity );
    }

    public static CharBuf createCharBuf( int capacity ) {
        return new CharBuf( capacity );
    }

    public static CharBuf createCharBuf(  ) {
        return new CharBuf( 100 );
    }

    public static CharBuf create( char[] buffer ) {
        return new CharBuf( buffer );
    }

    public static CharBuf createFromUTF8Bytes( byte[] buffer ) {
        return new CharBuf( buffer );
    }

    protected CharBuf( int capacity ) {

        super(writer());
        this.capacity = capacity;
        init();
    }

    protected CharBuf() {
        super(writer());
        init();
    }

    @Override
    public void write( char[] cbuf, int off, int len ) {

        if ( off == 0 && cbuf.length == len ) {
            this.add( cbuf );
        } else {
            char[] buffer = Arrays.copyOfRange( cbuf, off, off + len );
            this.add( buffer );
        }

    }

    @Override
    public void flush()  {
    }

    @Override
    public void close()  {
    }

    public void init() {
        buffer = new char[ capacity ];
    }

    public final CharBuf add( Object str ) {
        add( FastStringUtils.toCharArray( Str.str(str) ) );
        return this;
    }

    public final CharBuf add( String str ) {
        add( FastStringUtils.toCharArray( str ) );
        return this;
    }

    public final CharBuf add( CharSequence str ) {
        add( FastStringUtils.toCharArray( str ) );
        return this;
    }

    public final CharBuf addString( String str ) {
        add( FastStringUtils.toCharArray( str ) );
        return this;
    }

    public final CharBuf addObject ( Object object ) {
        String str = object.toString();
        addString ( str );
        return this;
    }

    public final CharBuf add( int i ) {

        add ( Integer.toString ( i ));
        return this;
    }

    private Cache <Integer, char[]> icache;

    public final CharBuf addInt( int i ) {
        switch (i) {
            case 0:
                addChar('0');
                return this;
            case 1:
                addChar('1');
                return this;
            case -1:
                addChar('-');
                addChar('1');
                return this;
        }

        addInt( Integer.valueOf( i ) );
        return this;
    }
    public final CharBuf addInt( Integer key ) {

        if (icache == null) {
            icache = new SimpleCache<> ( 1000, CacheType.LRU );
        }
        char [] chars = icache.get ( key );

        if ( chars ==  null ) {
            String str = Integer.toString ( key );
            chars = FastStringUtils.toCharArray ( str );
            icache.put ( key, chars );
        }

        addChars ( chars );
        return this;
    }

    final char [] trueChars = "true".toCharArray ();
    final char [] falseChars = "false".toCharArray ();

    public final CharBuf add( boolean b ) {
        addChars ( b ? trueChars : falseChars );
        return this;
    }

    public final CharBuf addBoolean( boolean b ) {

        add ( Boolean.toString ( b ));
        return this;
    }

    public final  CharBuf add( byte i ) {

        add ( Byte.toString ( i ));
        return this;
    }

    public final CharBuf addByte( byte i ) {

        addInt ( i );
        return this;
    }

    public final CharBuf add( short i ) {

        add ( Short.toString ( i ));
        return this;
    }

    public final CharBuf addShort( short i ) {

        addInt ( i );
        return this;
    }

    public final CharBuf add( long l ) {
        add ( Long.toString ( l ));
        return this;
    }

    public final  CharBuf add( double d ) {
        add ( Double.toString ( d ) );
        return this;
    }

    private Cache <Double, char[]> dcache;

    public final  CharBuf addDouble( double d ) {
        addDouble( Double.valueOf( d ) );
        return this;
    }

    public final  CharBuf addDouble( Double key ) {

        if (dcache == null) {
            dcache = new SimpleCache<> ( 100, CacheType.LRU );
        }
        char [] chars = dcache.get ( key );

        if ( chars ==  null ) {
            String str = Double.toString ( key );
            chars = FastStringUtils.toCharArray ( str );
            dcache.put ( key, chars );
        }

        add ( chars );
        return this;
    }

    public final  CharBuf add( float d ) {
        add ( Float.toString ( d ));
        return this;
    }

    private Cache <Float, char[]> fcache;

    public final  CharBuf addFloat( float d ) {
        addFloat( Float.valueOf( d ) );
        return this;
    }

    public final  CharBuf addFloat( Float key ) {

        if (fcache == null) {
            fcache = new SimpleCache<> ( 100, CacheType.LRU );
        }
        char [] chars = fcache.get ( key );

        if ( chars ==  null ) {
            String str = Float.toString ( key );
            chars = FastStringUtils.toCharArray ( str );
            fcache.put ( key, chars );
        }

        add ( chars );

        return this;
    }

    public final CharBuf addChar( byte i ) {
        add( ( char ) i );
        return this;
    }

    public final CharBuf addChar( int i ) {
        add( ( char ) i );
        return this;
    }

    public final CharBuf addChar( short i ) {
        add( ( char ) i );
        return this;
    }

    public final CharBuf addChar( final char ch ) {

        if ( 1 + location > capacity ) {
            buffer = Chr.grow( buffer );
            capacity = buffer.length;

        }

        buffer [location] = ch;
        location ++;

        return this;
    }

    public final CharBuf add( char ch ) {

        if ( 1 + location > capacity ) {
            buffer = Chr.grow( buffer );
            capacity = buffer.length;

        }

        buffer [location] = ch;
        location ++;

        return this;
    }

    public CharBuf addLine( String str ) {
        add( FastStringUtils.toCharArray(str));
        add( '\n' );
        return this;
    }

    public CharBuf addLine( Object str ) {
        add( FastStringUtils.toCharArray(
                Str.str(str)));
        add( '\n' );
        return this;
    }

    public CharBuf addLine(  ) {
        add( '\n' );
        return this;
    }

    public CharBuf addLine( char[] chars ) {
        add( chars );
        add( '\n' );
        return this;
    }

    public CharBuf addLine( CharSequence str ) {
        add( str.toString() );
        add( '\n' );
        return this;
    }

    public CharBuf add( char[] chars ) {
        if ( chars.length + location > capacity ) {
            buffer = Chr.grow( buffer, buffer.length * 2 + chars.length );
            capacity = buffer.length;
        }

        Chr._idx( buffer, location, chars );
        location += chars.length;
        return this;
    }

    public final CharBuf addChars( char[] chars ) {
        if ( chars.length + location > capacity ) {
            buffer = Chr.grow( buffer, buffer.length * 2 + chars.length );
            capacity = buffer.length;
        }

        System.arraycopy ( chars, 0, buffer, location, chars.length );
        location += chars.length;
        return this;
    }

    public final CharBuf addQuoted( char[] chars ) {

        int sizeNeeded = chars.length + 2 + location;
        if (  sizeNeeded > capacity ) {
            buffer = Chr.grow( buffer, sizeNeeded * 2  );
            capacity = buffer.length;
        }
        buffer [location] = '"';
        location++;

        System.arraycopy( chars, 0, buffer, location, chars.length );
        location += (chars.length);
        buffer [location] = '"';
        location++;

        return this;
    }

    public final CharBuf asJsonString(String jsonString, boolean asciiOnly) {
        char[] charArray = FastStringUtils.toCharArray ( jsonString );
        return addJsonEscapedString ( charArray, asciiOnly );

    }

    public final CharBuf asJsonString(String jsonString) {

        return asJsonString(jsonString, false);
    }

    private static  boolean isJSONEscapeOrAsciiControlOrUnicode(int c) {

        return (c < 32 || c == 34 || c == 92 || c > 127 );

    }

    private static  boolean isJSONEscapeOrAsciiControl(int c) {

        return (c < 32 || c == 34 || c == 92 );

    }

    int jsonControlCount;

    private  boolean hasAnyAsciiControlOrUnicodeChars(final char[] charArray) {
        int index = 0;
        char c;
        while ( true ) {
            c = charArray[ index ];
            if ( isJSONEscapeOrAsciiControlOrUnicode(c)) {
                jsonControlCount++;

            }
            if ( ++index >= charArray.length) break;
        }

        return (jsonControlCount>0) ? true : false;

    }

    private final  boolean hasAnyAsciiControl(final char[] charArray) {
        int index = 0;
        char c;
        while ( true ) {
            c = charArray[ index ];
            if ( isJSONEscapeOrAsciiControl(c)) {
                jsonControlCount++;

            }
            if ( ++index >= charArray.length) break;
        }

        return jsonControlCount>0;

    }

    public final CharBuf addJsonEscapedString( final char[] charArray ) {

        return addJsonEscapedString(charArray, false);
    }

    public final CharBuf addJsonEscapedString( final char[] charArray, boolean asciiOnly ) {
        jsonControlCount = 0;

        if (!asciiOnly) {

            if (charArray.length > 0 && hasAnyAsciiControl(charArray)) {
                return doAddJsonEscapedString(charArray);
            } else {
                return this.addQuoted(charArray);
            }

        } else {
            if (charArray.length > 0 && hasAnyAsciiControlOrUnicodeChars(charArray)) {
                return doAddJsonEscapedStringEscapeUnicode(charArray);
            } else {
                return this.addQuoted(charArray);
            }

        }
    }

    final byte[] encoded = new byte[2];

    final byte[] charTo = new byte[2];

    private final CharBuf doAddJsonEscapedStringEscapeUnicode( char[] charArray ) {

        char [] _buffer = buffer;
        int _location =  this.location;

        final byte[] _encoded = encoded;

        final byte[] _charTo = charTo;
        /* We are making a bet that not all chars will be unicode. */
        int  ensureThisMuch = charArray.length +  ((jsonControlCount +2) * 5);

        int sizeNeeded =  (ensureThisMuch) + _location;
        if ( sizeNeeded  > capacity ) {

            int growBy =   ( _buffer.length * 2 ) <  sizeNeeded  ? sizeNeeded : (_buffer.length*2);
            _buffer = Chr.grow( _buffer, growBy);
            capacity = _buffer.length;
        }

        _buffer[_location] = '"';
        _location ++;

        int index = 0;
        while ( true ) {

            char c = charArray[ index ];

            if ( isJSONEscapeOrAsciiControlOrUnicode(c)) {

                switch ( c ) {
                    case '\"':
                        _buffer[_location] = '\\';
                        _location ++;
                        _buffer[_location] =  '"';
                        _location ++;
                        break;
                    case '\\':
                        _buffer[_location] = '\\';
                        _location ++;
                        _buffer[_location] =  '\\';
                        _location ++;
                        break;
                    case '\b':
                        _buffer[_location] = '\\';
                        _location ++;
                        _buffer[_location] =  'b';
                        _location ++;
                        break;
                    case '\f':
                        _buffer[_location] = '\\';
                        _location ++;
                        _buffer[_location] =  'f';
                        _location ++;
                        break;
                    case '\n':
                        _buffer[_location] = '\\';
                        _location ++;
                        _buffer[_location] =  'n';
                        _location ++;
                        break;
                    case '\r':
                        _buffer[_location] = '\\';
                        _location ++;
                        _buffer[_location] =  'r';
                        _location ++;
                        break;

                    case '\t':
                        _buffer[_location] = '\\';
                        _location ++;
                        _buffer[_location] =  't';
                        _location ++;
                        break;

                    default:

                        _buffer[_location] = '\\';
                        _location ++;
                        _buffer[_location] =  'u';
                        _location ++;
                        if (c <= 255) {
                            _buffer[_location] =  '0';
                            _location ++;
                            _buffer[_location] =  '0';
                            _location ++;
                            ByteScanner.encodeByteIntoTwoAsciiCharBytes( c, _encoded );
                            for (int b : _encoded) {
                                _buffer [_location] = (char)b;
                                _location ++;

                            }
                        } else {
                            Byt.charTo( _charTo, c );

                            for (int charByte : _charTo) {
                                ByteScanner.encodeByteIntoTwoAsciiCharBytes( charByte, _encoded );
                                for (int b : _encoded) {
                                    _buffer [_location] = (char)b;
                                    _location ++;
                                }
                            }

                        }

                }
            }else {

                _buffer[_location] = c;
                _location ++;

            }

            if ( ++index >= charArray.length) break;

        }
        _buffer[_location] = '"';
        _location ++;

        buffer = _buffer;
        location = _location;

        return this;
    }

    private final CharBuf doAddJsonEscapedString( char[] charArray ) {

        char [] _buffer = buffer;
        int _location =  this.location;

        final byte[] _encoded = encoded;

        final byte[] _charTo = charTo;
        /* We are making a bet that not all chars will be unicode. */
        int  ensureThisMuch = charArray.length +  ((jsonControlCount +2) * 5);

        int sizeNeeded =  (ensureThisMuch) + _location;
        if ( sizeNeeded  > capacity ) {

            int growBy =   ( _buffer.length * 2 ) <  sizeNeeded  ? sizeNeeded : (_buffer.length*2);
            _buffer = Chr.grow( _buffer, growBy);
            capacity = _buffer.length;
        }

        _buffer[_location] = '"';
        _location ++;

        int index = 0;
        while ( true ) {

                char c = charArray[ index ];

                if ( isJSONEscapeOrAsciiControl(c)) {

                    switch ( c ) {
                        case '\"':
                            _buffer[_location] = '\\';
                            _location ++;
                            _buffer[_location] =  '"';
                            _location ++;
                            break;
                        case '\\':
                            _buffer[_location] = '\\';
                            _location ++;
                            _buffer[_location] =  '\\';
                            _location ++;
                            break;
                        case '\b':
                            _buffer[_location] = '\\';
                            _location ++;
                            _buffer[_location] =  'b';
                            _location ++;
                            break;
                        case '\f':
                            _buffer[_location] = '\\';
                            _location ++;
                            _buffer[_location] =  'f';
                            _location ++;
                            break;
                        case '\n':
                            _buffer[_location] = '\\';
                            _location ++;
                            _buffer[_location] =  'n';
                            _location ++;
                            break;
                        case '\r':
                            _buffer[_location] = '\\';
                            _location ++;
                            _buffer[_location] =  'r';
                            _location ++;
                            break;

                        case '\t':
                            _buffer[_location] = '\\';
                            _location ++;
                            _buffer[_location] =  't';
                            _location ++;
                            break;

                        default:

                            _buffer[_location] = '\\';
                            _location ++;
                            _buffer[_location] =  'u';
                            _location ++;
                            if (c <= 255) {
                                _buffer[_location] =  '0';
                                _location ++;
                                _buffer[_location] =  '0';
                                _location ++;
                                ByteScanner.encodeByteIntoTwoAsciiCharBytes( c, _encoded );
                                for (int b : _encoded) {
                                    _buffer [_location] = (char)b;
                                    _location ++;

                                }
                            } else {
                                Byt.charTo( _charTo, c );

                                for (int charByte : _charTo) {
                                    ByteScanner.encodeByteIntoTwoAsciiCharBytes( charByte, _encoded );
                                    for (int b : _encoded) {
                                        _buffer [_location] = (char)b;
                                        _location ++;
                                    }
                                }

                            }

                    }
                }else {

                        _buffer[_location] = c;
                        _location ++;

                }

                if ( ++index >= charArray.length) break;

        }
        _buffer[_location] = '"';
        _location ++;

        buffer = _buffer;
        location = _location;

        return this;
    }

    public final CharBuf addJsonFieldName( String str ) {
        return addJsonFieldName ( FastStringUtils.toCharArray ( str ) );
    }

    public final CharBuf addJsonFieldName( char[] chars ) {
            int sizeNeeded = chars.length + 4 + location;
            if (  sizeNeeded > capacity ) {
                buffer = Chr.grow( buffer, sizeNeeded * 2  );
                capacity = buffer.length;
            }
            buffer [location] = '"';
            location++;

            System.arraycopy( chars, 0, buffer, location, chars.length );

            location += (chars.length);
            buffer [location] = '"';
            location++;
            buffer [location] = ':';
            location++;

            return this;
     }

    public final CharBuf addQuoted( String str ) {
        final char[] chars = FastStringUtils.toCharArray ( str );
        addQuoted ( chars );
        return this;
    }

    public CharBuf add( char[] chars, final int length ) {
        if ( length + location < capacity ) {
            Chr._idx( buffer, location, chars, length );
        } else {
            buffer = Chr.grow( buffer, buffer.length * 2 + length );
            Chr._idx( buffer, location, chars );
            capacity = buffer.length;
        }
        location += length;
        return this;
    }

    public CharBuf add( byte[] chars ) {
        if ( chars.length + location < capacity ) {
            Chr._idx( buffer, location, chars );
        } else {
            buffer = Chr.grow( buffer, buffer.length * 2 + chars.length );
            Chr._idx( buffer, location, chars );
            capacity = buffer.length;
        }
        location += chars.length;
        return this;
    }

    public CharBuf add( byte[] bytes, int start, int end ) {
        int charsLength =  end - start ;
        if ( charsLength + location > capacity ) {
            buffer = Chr.grow( buffer, buffer.length * 2 + charsLength );
        }
        Chr._idx( buffer, location, bytes, start, end );
        capacity = buffer.length;
        location += charsLength;
        return this;
    }

    public CharBuf add( String string, int start, int end ) {
        int charsLength =  end - start ;
        if ( charsLength + location > capacity ) {
            buffer = Chr.grow( buffer, buffer.length * 2 + charsLength );
        }

        return this.add(string.substring(start, end));
    }

    @Override
    public int length () {
        return len();
    }

    @Override
    public char charAt ( int index ) {
        return buffer [ index ];
    }

    @Override
    public CharSequence subSequence ( int start, int end ) {
        return new String (buffer, start, end - start);
    }

    public String toString() {
        return FastStringUtils.noCopyStringFromCharsNoCheck(buffer, location);
    }

    public String toDebugString() {
        return "CharBuf{" +
                "capacity=" + capacity +
                ", location=" + location +
                '}';
    }

    public String toStringAndRecycle() {

        String str = this.toString();
        location = 0;
        return str;
    }

    public int len() {
        return location;
    }

    public char[] toCharArray() {
        return this.buffer;
    }

    public void _len( int location ) {
        this.location = location;
    }

    public char[] readForRecycle() {
        this.location = 0;
        return this.buffer;
    }

    public void recycle() {
        this.location = 0;
    }

    public double doubleValue() {
        return CharScanner.parseDouble( this.buffer, 0, location );
    }

    public float floatValue() {
        return CharScanner.parseFloat( this.buffer, 0, location );
    }

    public int intValue() {
        return CharScanner.parseInt(buffer, 0, location);
    }

    public long longValue() {
        return CharScanner.parseLong(buffer, 0, location);
    }

    public byte byteValue() {
        return ( byte ) intValue();
    }

    public short shortValue() {
        return ( short ) intValue();
    }

    public Number toIntegerWrapper() {

         if ( isInteger( buffer, 0, location  ) ) {
            return intValue() ;
        } else {
            return longValue();
        }

    }

    public void addAsUTF( byte[] value ) {

        String str = new String(value, StandardCharsets.UTF_8);
        final char[] chars = FastStringUtils.toCharArray(str);
        this.add(chars);

    }

    final static char [] nullChars = "null".toCharArray ();
    public final void addNull () {
        this.add ( nullChars );
    }

    public char lastChar() {
        return buffer[location-1];
    }

    public void removeLastChar () {
        location--;
        if (location < 0) {
            location = 0;
        }
    }

    private Cache <BigDecimal, char[]> bigDCache;
    public CharBuf addBigDecimal( BigDecimal key ) {
        if (bigDCache == null) {
            bigDCache = new SimpleCache<> ( 100, CacheType.LRU );
        }
        char [] chars = bigDCache.get ( key );

        if ( chars ==  null ) {
            String str = key.toString();
            chars = FastStringUtils.toCharArray ( str );
            bigDCache.put ( key, chars );
        }

        add ( chars );

        return this;

    }

    private Cache <BigInteger, char[]> bigICache;

    public CharBuf addBigInteger( BigInteger key ) {
        if (bigICache == null) {
            bigICache = new SimpleCache<> ( 100, CacheType.LRU );
        }
        char [] chars = bigICache.get ( key );

        if ( chars ==  null ) {
            String str = key.toString();
            chars = FastStringUtils.toCharArray ( str );
            bigICache.put ( key, chars );
        }

        add ( chars );

        return this;

    }

    private Cache <Long, char[]> lcache;

    public final  CharBuf addLong( long l ) {

        addLong(Long.valueOf( l ));
        return this;
    }

    public final  CharBuf addLong( Long key ) {

        if (lcache == null) {
            lcache = new SimpleCache<> ( 100, CacheType.LRU );
        }
        char [] chars = lcache.get ( key );

        if ( chars ==  null ) {
            String str = Long.toString ( key );
            chars = FastStringUtils.toCharArray ( str );
            lcache.put ( key, chars );
        }

        add ( chars );

        return this;
    }

    private Cache <Currency, char[]> currencyCache;
    public CharBuf addCurrency( Currency key ) {
        if (currencyCache == null) {
            currencyCache = new SimpleCache<> ( 100, CacheType.LRU );
        }
        char [] chars = currencyCache.get ( key );

        if ( chars ==  null ) {
            String str = '"' + key.toString() + '"';
            chars = FastStringUtils.toCharArray ( str );
            currencyCache.put ( key, chars );
        }

        add ( chars );

        return this;

    }

    /**
     * Turn a single bytes into two hex character representation.
     *
     * @param decoded the byte to serializeObject.
     */
    public  CharSequence addHex( final int decoded  ) {

        int _location = location;
        char [] _buffer = buffer;
        int _capacity = capacity;

        if ( 2 + _location > _capacity ) {
            _buffer = Chr.grow( _buffer );
            _capacity = _buffer.length;
        }

        _buffer [_location] = (char) encodeNibbleToHexAsciiCharByte( ( decoded >> 4 ) & 0x0F );
        _location ++;

        _buffer [_location] = (char) encodeNibbleToHexAsciiCharByte( decoded & 0x0F );;
        _location ++;

        location = _location;
        buffer = _buffer;
        capacity = _capacity;
        return this;
    }

    /**
     * Turns a single nibble into an ascii HEX digit.
     *
     * @param nibble the nibble to serializeObject.
     * @return the encoded nibble (1/2 byte).
     */
    protected static int encodeNibbleToHexAsciiCharByte( final int nibble ) {

        switch ( nibble ) {
            case 0x00:
            case 0x01:
            case 0x02:
            case 0x03:
            case 0x04:
            case 0x05:
            case 0x06:
            case 0x07:
            case 0x08:
            case 0x09:
                return nibble + 0x30; // 0x30('0') - 0x39('9')
            case 0x0A:
            case 0x0B:
            case 0x0C:
            case 0x0D:
            case 0x0E:
            case 0x0F:
                return nibble + 0x57; // 0x41('a') - 0x46('f')
            default:
                die( "illegal nibble: " + nibble );
                return -1;
        }
    }

    public final CharBuf decodeJsonString ( char[] chars ) {
        return decodeJsonString ( chars, 0, chars.length );
    }

    static final char[] controlMap = new char[255];

    static {
        controlMap[(int)'n'] = '\n';
        controlMap[(int)'b'] = '\b';
        controlMap[(int)'/'] = '/';
        controlMap[(int)'f'] = '\f';
        controlMap[(int)'r'] = '\r';
        controlMap[(int)'t'] = '\t';
        controlMap[(int)'\\'] = '\\';

        controlMap[(int)'"'] = '"';
    }

    public final CharBuf decodeJsonString ( char[] chars, int start, int to ) {
        int len = to - start;

        char [] buffer = this.buffer;
        int location = this.location;

        if (len > capacity) {
            buffer =  Chr.grow ( buffer, (buffer.length * 2) + len );
            capacity = buffer.length;
            this.buffer = buffer;
        }

        char c;
        int index = start;

        while (true) {
            c = chars[ index ];
            if ( c == '\\'  && index < (to -1)) {
                    index++;
                    c = chars[ index ];
                    if (c!='u') {
                        buffer[location++] = controlMap[(int)c];
                    } else {

                            if ( index + 4 < to ) {
                                String hex = new String( chars, index + 1, 4 );
                                char unicode = ( char ) Integer.parseInt( hex, 16 );
                                buffer[location++]=unicode;
                                index += 4;
                            }
                    }

            } else {
                buffer[location++]=c;
            }
            if (index >= (to -1)) {
                break;
            }
            index++;
        }

        this.buffer = buffer;
        this.location = location;

        return this;

    }

    protected static final int DOUBLE_QUOTE = '"';

    protected static final int ESCAPE = '\\';

    protected static final int LETTER_N = 'n';

    protected static final int LETTER_U = 'u';

    protected static final int LETTER_T = 't';

    protected static final int LETTER_R = 'r';

    protected static final int LETTER_B = 'b';

    protected static final int LETTER_F = 'f';

    protected static final int FORWARD_SLASH = '/';

    public final CharBuf decodeJsonString ( byte[] bytes, int start, int to ) {

        String str = new String(bytes, start, to - start, StandardCharsets.UTF_8);
        final char[] chars = FastStringUtils.toCharArray(str);
        this.decodeJsonString(chars);
        return this;
    }

    public void ensure( int i ) {
        if ( i + location > capacity ) {
            buffer = Chr.grow( buffer, i * 2 );
            capacity = buffer.length;
        }
    }

    @Override
    public boolean equals(Object o) {
        if (o == null ) {
            return false;
        }

        if (o instanceof CharSequence) {
            return this.toString().equals(o.toString());
        } else {
            return false;
        }

    }

    @Override
    public int hashCode() {
        return this.toString().hashCode();
    }

    public CharBuf  multiply(char c, int len) {
        for (int index=0; index<len; index++) {
            this.add(c);
        }
        return this;
    }

    public CharBuf  multiply(CharSequence str, int len) {

        for (int index=0; index<len; index++) {
            this.add(str.toString());
        }
        return this;
    }

    public void puts(Object... messages) {

        for ( Object message : messages ) {

            if ( message == null ) {
                add( "<NULL>" );
            } else if (message instanceof char[]) {
                add((char[])message);
            } else if ( message.getClass().isArray() ) {
                add( toListOrSingletonList( message ).toString() );
            } else {
                add( message.toString() );
            }
            add( ' ' );
        }
        addLine();
    }

    /**
     * Prints an Object and then terminates the line.  This method calls
     * at first String.valueOf(x) to get the printed object's string value,
     * then behaves as
     * though it invokes <code>{@link #print(String)}</code> and then
     * <code>{@link #println()}</code>.
     *
     * @param message  The <code>Object</code> to be printed.
     */
    public void println(String message) {
        this.addLine(message);
    }

    /**
     * Prints a String and then terminates the line.  This method behaves as
     * though it invokes <code>{@link #print(String)}</code> and then
     * <code>{@link #println()}</code>.
     *
     * @param object the <code>String</code> value to be printed
     */
    public void println(Object object) {
        this.addLine(Str.toString(object));
    }

    public CharBuf indent(int i) {
        return multiply(' ', i);
    }

    public void jsonDate(long millis) {
        Dates.jsonDateChars(new Date(millis), this);
    }

    public PrintWriter append(CharSequence csq)  {
            return super.append(csq);
    }

    private static  Writer writer() {
        return new Writer() {
            @Override
            public void write(char[] cbuf, int off, int len) throws IOException {

            }

            @Override
            public void flush() throws IOException {

            }

            @Override
            public void close() throws IOException {

            }
        };
    }

        /* Methods that do not terminate lines */

    public void print(boolean b) {
        this.addBoolean(b);
    }

    public void print(char c) {
        this.add(c);
    }

    public void print(int i) {
        this.addInt(i);
    }

    public void print(long l) {
        this.addLong(l);
    }

    public void print(float f) {
        this.add(f);
    }

    public void print(double d) {

        this.add(d);
    }

    public void print(char s[]) {
        this.addChars(s);
    }

    public void print(String s) {
        if (s == null) {
            s = "null";
        }
        this.addString(s);
    }

    public void print(Object obj) {
        write(String.valueOf(obj));
    }

    /* Methods that do terminate lines */

    public void println() {
        this.addLine();
    }

    public void println(boolean x) {
        this.addBoolean(x).addLine();
    }

    public void println(char x) {
        this.addChar(x).addLine();
    }

    public void println(int x) {
        this.addInt(x).addLine();

    }

    public void println(long x) {

        this.addLong(x).addLine();
    }

    public void println(float x) {
        add(x).addLine();
    }

    public void println(double x) {
        add(x).addLine();
    }

    public void println(char x[]) {
        add(x).addLine();

    }

    public PrintWriter printf(String format, Object ... args) {
        this.addLine(String.format(format, args));
        return this;
    }

    public PrintWriter printf(Locale l, String format, Object ... args) {

        this.addLine(String.format(l, format, args));
        return this;

    }

    public void display() {

        System.out.println(this.toString());
    }

    public  CharBuf prettyPrintMap(Map map) {
        return prettyPrintMap(map,  0);

    }

    public  CharBuf prettyPrintBean(Object object) {
        final Map<String, Object> map = Maps.toPrettyMap(object);
        return prettyPrintMap(map,  0);
    }

    public  CharBuf prettyPrintBeanWithTypes(Object object) {
        final Map<String, Object> map = Maps.toMap(object);
        return prettyPrintMap(map,  0);
    }

    public  CharBuf prettyPrintBean(Mapper mapper, Object object) {
        return prettyPrintMap(mapper.toMap(object),  0);
    }

    public  CharBuf prettyPrintMap(Map map, final int indent) {
        final Set set = map.entrySet();

        final Iterator iterator = set.iterator();

        indent(indent * 4).add("{\n");

        while (iterator.hasNext()) {

            Map.Entry entry =  (Map.Entry)iterator.next();

            indent((indent + 1) * 4);

            addJsonEscapedString(entry.getKey().toString().toCharArray());

            add(" : ");

            Object value = entry.getValue();

            prettyPrintObject(value, true, indent);

            add(",\n");

        }

        if (map.size()>0) {
            removeLastChar();
            removeLastChar();
            add("\n");
        }

        indent(indent * 4).add('}');

        return this;

    }

    public  CharBuf prettyPrintCollection(Collection values,  boolean type, int indent) {

        add('[');
        for (Object value : values) {

            prettyPrintObject(value, type, indent);

            add(',');
        }

        if (values.size()>0) {
            removeLastChar();
        }

        add(']');

        return this;
    }

    public CharBuf prettyPrintObject(Object value, boolean type, int indent) {

        TypeType instanceType = TypeType.getInstanceType(value);
        switch (instanceType) {

            case BYTE_WRAPPER:
            case SHORT_WRAPPER:
            case BOOLEAN_WRAPPER:
            case BIG_DECIMAL:
            case BIG_INT:
            case DOUBLE_WRAPPER:
            case INTEGER_WRAPPER:
            case LONG_WRAPPER:
            case NUMBER:
                add(value);
                break;

            case NULL:
                addNull();
                break;

            case MAP:
                prettyPrintMap((Map) value, indent + 2);
                break;

            case INSTANCE:
                if (type) {
                    prettyPrintMap(Maps.toMap(value), indent);
                } else {
                    prettyPrintMap(Maps.toPrettyMap(value), indent);
                }
                break;

            case COLLECTION:
            case SET:
            case LIST:
                prettyPrintCollection((Collection) value, type, indent);
                break;

            case ARRAY:
            case ARRAY_INT:
            case ARRAY_BYTE:
            case ARRAY_SHORT:
            case ARRAY_FLOAT:
            case ARRAY_DOUBLE:
            case ARRAY_LONG:
            case ARRAY_STRING:
            case ARRAY_OBJECT:
                prettyPrintCollection(Lists.list(Conversions.iterator(value)), type, indent);
                break;

            case ENUM:
                addJsonEscapedString(((Enum)value).name().toCharArray());
                break;

            default:
                addJsonEscapedString(value.toString().toCharArray());

        }

        return this;
    }

}


package org.boon.slumberdb.service.client;

import org.boon.HTTP;
import org.boon.Lists;
import org.boon.json.JsonSerializer;
import org.boon.json.JsonSerializerFactory;
import org.boon.slumberdb.service.protocol.Action;
import org.boon.slumberdb.service.protocol.ProtocolConstants;
import org.boon.slumberdb.service.protocol.requests.*;
import org.boon.slumberdb.service.results.BatchResult;
import org.boon.slumberdb.service.results.SingleResult;
import org.boon.slumberdb.service.results.StatsResults;
import org.boon.slumberdb.stores.DataOutputQueue;
import org.boon.slumberdb.stores.DataStoreSource;
import org.boon.slumberdb.stores.queue.DataOutputQueueTransferQueue;

import java.util.*;

import static org.boon.Boon.puts;

/**
 * Just for testing
 * Created by Richard on 7/2/14.
 */
public class DataStoreSimpleHttpClient implements DataStoreClient {

    private DataOutputQueue queue;
    private String url;
    private String clientId;
    private JsonSerializer serializer;

    private long messageId;

    private boolean verbose;

    private void send(DataStoreRequest request) {
        String message = request.formTextRequest();
        if (verbose) {
            puts("POST", url, ProtocolConstants.prettyPrintMessage(message));
            puts("POST", url);
            puts(ProtocolConstants.prettyPrintMessageWithLinesTabs(message));
        }
        String results = HTTP.post(url, message);
        handleMessageFromServer(results);
    }

    /**
     * RESPONSES FROM SERVER.
     *
     * @param textResponse
     */
    private void handleMessageFromServer(String textResponse) {

        if (verbose) {
            puts("RESPONSE", url, ProtocolConstants.prettyPrintMessage(textResponse));
            puts("RESPONSE", url);
            puts(ProtocolConstants.prettyPrintMessageWithLinesTabs(textResponse));
        }

        if (textResponse.startsWith(Action.GET.response().startsWith()) ||
                textResponse.startsWith(Action.SET_BROADCAST.response().startsWith())) {
            queue.put(SingleResult.fromTextMessage(textResponse));

        } else if (textResponse.startsWith(Action.BATCH_READ.response().startsWith())) {
            queue.put(BatchResult.fromTextMessage(textResponse));

        } else if (textResponse.startsWith(Action.GET_STATS.response().startsWith())) {
            queue.put(StatsResults.fromTextMessage(textResponse));

        } else {
            if (verbose) {
                puts(textResponse);
            }
        }

    }

    @Override
    public void get(String key) {
        GetRequest request = new GetRequest(Action.GET, messageId++, clientId, key);
        send(request);
    }

    @Override
    public void getFromMemory(String key) {
        GetRequest request = new GetRequest(Action.GET_MEM, messageId++, clientId, key);
        send(request);

    }

    @Override
    public void getFromLocalDB(String key) {
        GetRequest request = new GetRequest(Action.GET_LOCAL_DB, messageId++, clientId, key);
        send(request);

    }

    /**
     * public SetRequest(DataStoreSource source, Action action,
     * long id, String clientId, long version,
     * long  createTimeStamp, long updateTimeStamp,
     * String key, String payload) {
     *
     * @param source
     * @param key
     * @param value
     */
    @Override
    public void set(DataStoreSource source, String key, Object value) {
        SetRequest request = new SetRequest(source, Action.SET_SOURCE,
                messageId, clientId, 0, 0, 0, key, serializer.serialize(value).toString());
        send(request);

    }

    @Override
    public void get(DataStoreSource source, String key) {
        GetRequest request = new GetRequest(source, Action.GET_SOURCE, messageId++, clientId, key);
        send(request);
    }

    @Override
    public void set(String key, Object value) {
        SetRequest request = new SetRequest(messageId++, clientId, key, serializer.serialize(value).toString());
        send(request);
    }

    @Override
    public void setIfNotExists(String key, Object value) {
        SetRequest request = new SetRequest(Action.SET_IF_NOT_EXIST, messageId++, clientId, key, serializer.serialize(value).toString());
        send(request);

    }

    @Override
    public void broadcastSet(String key, Object value) {
        SetRequest request = new SetRequest(Action.SET_BROADCAST, messageId++, clientId, key, serializer.serialize(value).toString());
        send(request);

    }

    @Override
    public void setBatch(Map<String, Object> batch) {
        setBatch(null, batch);
    }

    @Override
    public void setBatch(DataStoreSource source, Map<String, Object> batch) {
        List<String> keys = Lists.list(batch.keySet());
        List<String> values = new ArrayList<>(keys.size());

        for (String key : keys) {
            values.add(serializer.serialize(batch.get(key)).toString());
        }

        BatchSetRequest request = new BatchSetRequest(source, messageId++, clientId, keys, values);
        send(request);
    }

    @Override
    public void setBatch(BatchSetRequest request) {
        send(request);
    }

    @Override
    public void remove(DataStoreSource source, String key) {

        RemoveRequest removeRequest = new RemoveRequest(source, Action.REMOVE_SOURCE, messageId++, clientId, key);
        send(removeRequest);

    }

    @Override
    public void remove(String key) {

        RemoveRequest removeRequest = new RemoveRequest(null, Action.REMOVE, messageId++, clientId, key);
        send(removeRequest);

    }

    @Override
    public void getStats() {

        StatsRequest request = new StatsRequest(null, Action.GET_STATS, messageId++, clientId);
        send(request);
    }

    @Override
    public void clearStats() {

        StatsRequest request = new StatsRequest(null, Action.CLEAR_STATS, messageId++, clientId);
        send(request);

    }

    @Override
    public void getStats(DataStoreSource source) {

        StatsRequest request = new StatsRequest(source, Action.GET_STATS, messageId++, clientId);
        send(request);

    }

    @Override
    public void clearStats(DataStoreSource source) {

        StatsRequest request = new StatsRequest(source, Action.CLEAR_STATS, messageId++, clientId);
        send(request);

    }

    @Override
    public void setBatchIfNotExists(Map<String, Object> batch) {
        setBatchIfNotExists(null, batch);
    }

    @Override
    public void setBatchIfNotExists(DataStoreSource source, Map<String, Object> batch) {
        List<String> keys = Lists.list(batch.keySet());
        List<String> values = new ArrayList<>(keys.size());

        for (String key : keys) {
            values.add(serializer.serialize(batch.get(key)).toString());
        }

        BatchSetRequest request = new BatchSetRequest(source, Action.SET_BATCH_IF_NOT_EXISTS, messageId, clientId, keys, values);
        send(request);
    }

    @Override
    public void batchLoad(Collection<String> keys) {

        ReadBatchRequest request = new ReadBatchRequest(messageId++, clientId, keys);
        send(request);
    }

    @Override
    public DataOutputQueue queue() {
        return queue;
    }

    @Override
    public DataStoreAdminClient admin(String shardId, String password) {
        return null;
    }

    public void init(DataOutputQueue queue, String clientId, String url, boolean verbose) {
        this.queue = queue;
        this.url = url;

        if (clientId == null) {
            this.clientId = UUID.randomUUID().toString();
        } else {
            this.clientId = clientId;
        }

        if (queue == null) {
            this.queue = new DataOutputQueueTransferQueue(1000);
        }
        JsonSerializerFactory factory = new JsonSerializerFactory();
        serializer = factory.create();
        this.verbose = verbose;
    }

    @Override
    public void flush() {

    }

    @Override
    public boolean connected(String key) {
        return true;
    }

}


package org.boon.slumberdb.base;

import org.boon.core.Function;
import org.boon.slumberdb.entries.Entry;
import org.boon.slumberdb.KeyValueIterable;
import org.boon.slumberdb.KeyValueStore;
import org.boon.slumberdb.serialization.NoOpSerializer;

import java.util.*;

/**
 * Created by Richard on 4/9/14.
 */
public class BaseStringBinaryKeyValueStore<K, V> {
    /**
     * Store that does the actual writing to DB (likely).
     */
    protected final KeyValueStore<String, byte[]> store;

    protected Function<String, K> keyObjectConverter = new NoOpSerializer<>();
    protected Function<K, String> keyToByteArrayConverter = new NoOpSerializer<>();

    protected Function<byte[], V> valueObjectConverter;
    protected Function<V, byte[]> valueSerializer;

    public BaseStringBinaryKeyValueStore(final KeyValueStore<String, byte[]> store) {
        this.store = store;
    }

    /**
     * Remove a key from the store.
     *
     * @param key
     */
    public void remove(K key) {

        store.remove(toKeyString(key));
    }

    /**
     * Search for a key in the key / value store.
     *
     * @param startKey
     * @return
     */
    public KeyValueIterable<K, V> search(K startKey) {
        final KeyValueIterable<String, byte[]> search = store.search(toKeyString(startKey));
        final Iterator<Entry<String, byte[]>> iterator = search.iterator();
        return new KeyValueIterable<K, V>() {
            @Override
            public void close() {
                search.close();
            }

            @Override
            public Iterator<Entry<K, V>> iterator() {
                return new Iterator<Entry<K, V>>() {
                    @Override
                    public boolean hasNext() {
                        return iterator.hasNext();
                    }

                    @Override
                    public Entry<K, V> next() {
                        final Entry<String, byte[]> next = iterator.next();

                        return new Entry<>(toKeyObject(next.key()),
                                toValueObject(next.value()));
                    }

                    @Override
                    public void remove() {
                        iterator.remove();
                    }
                };
            }
        };
    }

    /**
     * Load all of the key / values from the store.
     *
     * @return
     */
    public KeyValueIterable<K, V> loadAll() {
        final KeyValueIterable<String, byte[]> search = store.loadAll();
        final Iterator<Entry<String, byte[]>> iterator = search.iterator();
        return new KeyValueIterable<K, V>() {
            @Override
            public void close() {
                search.close();
            }

            @Override
            public Iterator<Entry<K, V>> iterator() {
                return new Iterator<Entry<K, V>>() {
                    @Override
                    public boolean hasNext() {
                        return iterator.hasNext();
                    }

                    @Override
                    public Entry<K, V> next() {
                        final Entry<String, byte[]> next = iterator.next();

                        return new Entry<>(toKeyObject(next.key()),
                                toValueObject(next.value()));
                    }

                    @Override
                    public void remove() {
                        iterator.remove();
                    }
                };
            }
        };
    }

    public Collection<K> loadAllKeys() {
        final Collection<String> keys = store.loadAllKeys();

        final Set<K> set = new HashSet<>();

        for (String key : keys) {
            set.add(toKeyObject(key));
        }

        return set;
    }

    /**
     * Get a value from the key value store.
     *
     * @param key key
     * @return
     */
    public V load(K key) {
        final byte[] bytes = store.load(toKeyString(key));
        if (bytes != null) {
            return toValueObject(bytes);
        } else {
            return null;
        }
    }

    public Map<K, V> loadAllByKeys(Collection<K> keys) {

        Set<K> keySet = new TreeSet<>(keys);

        List<String> byteKeys = new ArrayList<>();

        for (K key : keySet) {
            String bKey = toKeyString(key);
            byteKeys.add(bKey);
        }
        final Map<String, byte[]> map = store.loadAllByKeys(byteKeys);
        final Map<K, V> results = new LinkedHashMap<>();
        for (Map.Entry<String, byte[]> entry : map.entrySet()) {
            results.put(toKeyObject(entry.getKey()), toValueObject(entry.getValue()));
        }

        return results;
    }

    /**
     * Close the store.
     */
    public void close() {
        store.close();

    }

    /**
     * Convert a binary array to a String.
     *
     * @param key key
     * @return
     */
    protected K toKeyObject(String key) {
        return keyObjectConverter.apply(key);
    }

    /**
     * Use serializer to read this byte array as an object.
     *
     * @param value value read
     * @return new object read from key/value store
     */
    protected V toValueObject(byte[] value) {
        return valueObjectConverter.apply(value);
    }

    /**
     * Converts an object to a byte array.
     *
     * @param v object to convert
     * @return byte array representation of object.
     */
    protected byte[] toValueBytes(V v) {
        return this.valueSerializer.apply(v);
    }

    /**
     * Convert a String key to bytes.
     *
     * @param key key to convert
     * @return value
     */
    protected String toKeyString(K key) {
        return this.keyToByteArrayConverter.apply(key);
    }

    /**
     * Put a value in the key/value store.
     *
     * @param key   key
     * @param value value
     */
    public void put(K key, V value) {
        store.put(toKeyString(key), toValueBytes(value));
    }

    /**
     * Put all of these values in the key value store.
     *
     * @param values values
     */
    public void putAll(Map<K, V> values) {
        Set<Map.Entry<K, V>> entries = values.entrySet();
        Map<String, byte[]> map = new HashMap<>(values.size());

        for (Map.Entry<K, V> entry : entries) {
            map.put(toKeyString(entry.getKey()), toValueBytes(entry.getValue()));
        }

        store.putAll(map);
    }

    /**
     * Remove all of these values from the key value store.
     *
     * @param keys
     */
    public void removeAll(Iterable<K> keys) {
        List<String> list = new ArrayList<>();

        for (K key : keys) {
            list.add(toKeyString(key));
        }

        store.removeAll(list);
    }

    public boolean isOpen() {
        return store.isOpen();
    }

    public boolean isClosed() {
        return store.isClosed();
    }

}


package org.boon.slumberdb.stores;

import org.boon.slumberdb.service.config.DataStoreConfig;
import org.boon.slumberdb.service.protocol.Action;
import org.boon.slumberdb.service.protocol.requests.*;
import org.boon.slumberdb.service.results.StatCount;
import org.boon.Logger;
import org.boon.Pair;
import org.boon.collections.IntList;
import org.boon.concurrent.Timer;
import org.boon.core.Sys;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;

import static org.boon.Boon.configurableLogger;

/**
 * Created by Richard on 6/27/14.
 */
public abstract class BaseDataStore {

    protected final DataStoreSource source;
    protected LinkedTransferQueue<DataStoreRequest> writeOperationsQueue = new LinkedTransferQueue<>();
    protected LinkedTransferQueue<DataStoreRequest> readOperationsQueue = new LinkedTransferQueue<>();
    protected AtomicBoolean stop = new AtomicBoolean();
    protected DataOutputQueue outputDataQueue;
    protected DataStore nextReaderDataStore;
    protected ScheduledExecutorService scheduledExecutorService;
    protected DataStoreConfig dataStoreConfig;
    protected List<DataStoreRequest> readOperationsBatch = new ArrayList<>();
    protected List<DataStoreRequest> writeOperationsBatch = new ArrayList<>();
    protected Future<?> future;
    protected Timer timer = Timer.timer();
    private Logger logger = configurableLogger(this.getClass());

    protected BaseDataStore(DataStoreSource source) {
        this.source = source;
    }

    public void clearStats() {

    }

    public void sendStats(long now) {

    }

    public void init(DataStoreConfig dataStoreConfig, DataOutputQueue queue,
                     DataStore nextReaderDataStore) {
        this.dataStoreConfig = dataStoreConfig;
        this.outputDataQueue = queue;
        this.nextReaderDataStore = nextReaderDataStore;
    }

    public void set(SetRequest setRequest) {
        this.writeOperationsQueue.offer(setRequest);

    }

    public void addAll(BatchSetRequest batchSetRequest) {
        this.writeOperationsQueue.offer(batchSetRequest);

    }

    public void get(GetRequest request) {
        this.readOperationsQueue.offer(request);
    }

    /**
     * Called from worker thread.
     *
     * @throws InterruptedException
     */
    private void processReadQueue() throws InterruptedException {

        ReadStatus readStatus = new ReadStatus();

        while (true) {

            DataStoreRequest request = readOperationsQueue.poll(dataStoreConfig.pollTimeoutMS(), TimeUnit.MILLISECONDS);

            while (request != null) {

                readStatus.tracker.addCall(request, outputDataQueue);
                readOperationsBatch.add(request);
                if (readOperationsBatch.size() > dataStoreConfig.processQueueMaxBatchSize()) {
                    break;
                }
                request = readOperationsQueue.poll();

            }

            if (readOperationsBatch.size() > 0) {

                try {
                    recievedReadBatch(new ArrayList<>(readOperationsBatch));
                } finally {
                    readOperationsBatch.clear();
                }
            } else {
                flushReadsIfNeeded();
            }

            if (readStatus.readBatchSize.size() > 1_000) {

                StatCount count;
                final long now = Timer.timer().time();

                count = new StatCount(now, DataStoreSource.SERVER, Action.GET_STATS, "Thread TIME USER  BaseDataStore "
                        + Thread.currentThread().getName(), Sys.threadUserTime());
                this.outputDataQueue.put(count);

                count = new StatCount(now, DataStoreSource.SERVER, Action.GET_STATS, "Thread TIME CPU  BaseDataStore "
                        + Thread.currentThread().getName(), Sys.threadCPUTime());
                this.outputDataQueue.put(count);

                count = new StatCount(now, source, Action.GET, "BaseDataStore readStatus.readBatchSize.max", readStatus.readBatchSize.max());
                outputDataQueue.put(count);

                count = new StatCount(now, source, Action.GET, "BaseDataStore readStatus.readBatchSize.min", readStatus.readBatchSize.min());
                outputDataQueue.put(count);

                count = new StatCount(now, source, Action.GET, "BaseDataStore readStatus.readBatchSize.median", readStatus.readBatchSize.median());
                outputDataQueue.put(count);

                count = new StatCount(now, source, Action.GET, "BaseDataStore readStatus.readBatchSize.mean", readStatus.readBatchSize.mean());
                outputDataQueue.put(count);

                count = new StatCount(now, source, Action.GET, "BaseDataStore readStatus.readBatchSize.standardDeviation", readStatus.readBatchSize.standardDeviation());
                outputDataQueue.put(count);

                count = new StatCount(now, source, Action.GET, "BaseDataStore readStatus.readBatchSize.variance", readStatus.readBatchSize.variance());
                outputDataQueue.put(count);

                readStatus.readBatchSize.clear();

            }
        }

    }

    /**
     * Called from worker thread.
     * Processes the incoming queue for read and writes.
     */
    private void processWriteQueue() throws InterruptedException {

        WriteStatus status = new WriteStatus();

        while (true) {

            DataStoreRequest operation = writeOperationsQueue.poll(dataStoreConfig.pollTimeoutMS(), TimeUnit.MILLISECONDS);

            while (operation != null) {
                status.tracker.addCall(operation, outputDataQueue);
                writeOperationsBatch.add(operation);
                if (writeOperationsBatch.size() > dataStoreConfig.processQueueMaxBatchSize()) {
                    break;
                }
                operation = writeOperationsQueue.poll();

            }

            if (writeOperationsBatch.size() > 0) {

                try {
                    status.writeBatchSize.add(writeOperationsBatch.size());
                    recievedWriteBatch(new ArrayList<>(writeOperationsBatch));
                } finally {
                    writeOperationsBatch.clear();
                }
            } else {
                flushWritesIfNeeded();
            }

            if (status.writeBatchSize.size() > 1000) {

                status.sendBatchSize(source, outputDataQueue);
            }
        }
    }

    /**
     * Will flush to secondary storage if needed. This is used mostly by REMOTE_DB at the moment.
     */
    protected void flushReadsIfNeeded() throws InterruptedException {
    }

    /**
     * Will flush to secondary storage if needed. This is used mostly by REMOTE_DB at the moment.
     */
    protected void flushWritesIfNeeded() throws InterruptedException {
    }

    /**
     * Async send batches of data items to subclasses.
     */
    protected abstract void recievedReadBatch(List<DataStoreRequest> dataItems) throws InterruptedException;

    /**
     * Async send batches of data items to subclasses.
     */
    protected abstract void recievedWriteBatch(List<DataStoreRequest> dataItems) throws InterruptedException;

    public void stop() {

        stop.set(true);
        future.cancel(true);
        scheduledExecutorService.shutdownNow();

    }

    protected void nextDataStoreGet(GetRequest request) {
        try {
            nextReaderDataStore.get(request);
        } catch (Exception ex) {
            logger.error(ex, "Can't call next reader data store", nextReaderDataStore, request);
        }
    }

    protected void nextDataStoreReadBatch(ReadBatchRequest request) {

        try {

            nextReaderDataStore.batchRead(request);
        } catch (Exception ex) {
            logger.error(ex, "Can't call next reader data store for batch read", nextReaderDataStore);
        }

    }

    public void batchRead(ReadBatchRequest request) {

        readOperationsQueue.add(request);
    }

    public void remove(RemoveRequest request) {
        writeOperationsQueue.add(request);
    }

    /**
     * Start up the queue handlers.
     */
    public void start() {

        scheduledExecutorService = Executors.newScheduledThreadPool(2,
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable runnable) {
                        Thread thread = new Thread(runnable);
                        thread.setName(" DataQueue Process " + source);
                        return thread;
                    }
                }
        );

        future = scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {

                if (stop.get()) {
                    return;
                }
                try {
                    processWriteQueue();
                } catch (InterruptedException ex) {
                    //let it restart or stop

                } catch (Exception ex) {
                    logger.fatal(ex);
                }
            }
        }, 0, dataStoreConfig.threadErrorResumeTimeMS(), TimeUnit.MILLISECONDS);

        future = scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {

                if (stop.get()) {
                    return;
                }
                try {
                    processReadQueue();
                } catch (InterruptedException ex) {
                    //let it restart or stop

                } catch (Exception ex) {
                    logger.fatal(ex, "Problem with base data store running scheduled job");
                }
            }
        }, 0, dataStoreConfig.threadErrorResumeTimeMS(), TimeUnit.MILLISECONDS);

    }

    private static class ReadStatus {

        IntList readBatchSize = new IntList();
        CallStatusTracker tracker = new CallStatusTracker();
    }

    private static class CallStatus {

        Action action;
        DataStoreSource source;
        int count;
    }

    private static class CallStatusTracker {

        long numberOfCallsSinceLastSendAttempt = 0;
        long totalNumCalls = 0;
        long lastStatSend = 0;
        Map<Pair<Action, DataStoreSource>, CallStatus> callStatusMap = new HashMap<>();

        void addCall(DataStoreRequest request, DataOutputQueue queue) {

            Pair<Action, DataStoreSource> key = Pair.pair(request.action(), request.source());
            CallStatus callStatus = callStatusMap.get(
                    key);

            if (callStatus == null) {
                callStatus = new CallStatus();
                callStatus.action = request.action();
                callStatus.source = request.source();
                callStatus.count = request.count();
                callStatusMap.put(key, callStatus);
            } else {

                callStatus.action = request.action();
                callStatus.source = request.source();
                callStatus.count += request.count();
            }

            numberOfCallsSinceLastSendAttempt++;
            if (numberOfCallsSinceLastSendAttempt > 100) {
                totalNumCalls += numberOfCallsSinceLastSendAttempt;
                numberOfCallsSinceLastSendAttempt = 0;

                long now = Timer.timer().time();
                long duration = now - lastStatSend;

                /* Only send every 20 seconds. */
                if (duration > 20_000) {
                    for (CallStatus stat : callStatusMap.values()) {
                        StatCount count = new StatCount(now, stat.source,
                                stat.action,
                                "DATA SOURCE ACCESS ",
                                stat.count);
                        queue.put(count);
                    }

                    StatCount count = new StatCount(now, DataStoreSource.SERVER,
                            Action.NONE,
                            "DATA SOURCE ACCESS " + Thread.currentThread().getName(),
                            totalNumCalls);
                    queue.put(count);

                    try {

                        count = new StatCount(now, DataStoreSource.SERVER, Action.GET_STATS, "Thread CPU Time " + Thread.currentThread().getName(), Sys.threadCPUTime());
                        queue.put(count);

                    } catch (Throwable ex) {
                    }

                    lastStatSend = now;
                }
            }
        }

        public void clearStats() {

            numberOfCallsSinceLastSendAttempt = 0;
            totalNumCalls = 0;
            lastStatSend = 0;
            callStatusMap.clear();
        }
    }

    private static class WriteStatus {

        IntList writeBatchSize = new IntList();
        CallStatusTracker tracker = new CallStatusTracker();

        public void sendBatchSize(DataStoreSource source, DataOutputQueue queue) {

            int max = writeBatchSize.max();
            int mean = writeBatchSize.max();
            int min = writeBatchSize.max();
            int median = writeBatchSize.median();
            int standardDeviation = writeBatchSize.standardDeviation();
            int variance = writeBatchSize.variance();

            StatCount count;
            final long now = Timer.timer().time();

            count = new StatCount(now, DataStoreSource.SERVER, Action.GET_STATS, "Thread request handler User Time", Sys.threadUserTime());
            queue.put(count);

            count = new StatCount(now, source,
                    Action.SET, "DataStore WRITE BATCH SIZE max", max
            );
            queue.put(count);

            count = new StatCount(now, source,
                    Action.SET, "DataStore WRITE BATCH SIZE mean", mean
            );
            queue.put(count);

            count = new StatCount(now, source,
                    Action.SET, "DataStore WRITE BATCH SIZE min", min
            );
            queue.put(count);

            count = new StatCount(now, source,
                    Action.SET, "DataStore WRITE BATCH SIZE median", median
            );
            queue.put(count);

            count = new StatCount(now, source,
                    Action.SET, "DataStore WRITE BATCH SIZE standardDeviation", standardDeviation
            );
            queue.put(count);

            count = new StatCount(now, source,
                    Action.SET, "DataStore WRITE BATCH SIZE variance", variance
            );
            queue.put(count);

            writeBatchSize.clear();
        }
    }

}


/*
 * Copyright 2013-2014 Richard M. Hightower
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  		http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * __________                              _____          __   .__
 * \______   \ ____   ____   ____   /\    /     \ _____  |  | _|__| ____    ____
 *  |    |  _//  _ \ /  _ \ /    \  \/   /  \ /  \\__  \ |  |/ /  |/    \  / ___\
 *  |    |   (  <_> |  <_> )   |  \ /\  /    Y    \/ __ \|    <|  |   |  \/ /_/  >
 *  |______  /\____/ \____/|___|  / \/  \____|__  (____  /__|_ \__|___|  /\___  /
 *         \/                   \/              \/     \/     \/       \//_____/
 *      ____.                     ___________   _____    ______________.___.
 *     |    |____ ___  _______    \_   _____/  /  _  \  /   _____/\__  |   |
 *     |    \__  \\  \/ /\__  \    |    __)_  /  /_\  \ \_____  \  /   |   |
 * /\__|    |/ __ \\   /  / __ \_  |        \/    |    \/        \ \____   |
 * \________(____  /\_/  (____  / /_______  /\____|__  /_______  / / ______|
 *               \/           \/          \/         \/        \/  \/
 */

package org.boon;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import org.boon.core.reflection.FastStringUtils;
import org.boon.primitive.CharBuf;
import org.boon.primitive.CharScanner;
import org.boon.primitive.Chr;

import static org.boon.Exceptions.die;

public class Str {

    /**
     * Empty string
     */
    public final static String EMPTY_STRING = "";

    /**
     * gets length
     * @param str string
     * @return length
     */
    @Universal
    public static int lengthOf( String str ) {
        return len(str);
    }

    /**
     * Gets slice of a string.
     * @param str string
     * @param start start index of slice
     * @return new string
     */
    @Universal
    public static String sliceOf( String str, int start ) {
        return slc(str, start);
    }

    /**
     * Get slice of string
     * @param str string
     * @param start start index
     * @param end end index
     * @return new string
     */
    @Universal
    public static String sliceOf( String str, int start, int end ) {
        return slc(str, start, end);
    }

    /**
     * Gets end slice of a string.
     * @param str string
     * @param end end index of slice
     * @return new string
     */
    @Universal
    public static String endSliceOf( String str, int end ) {
        return slcEnd(str, end);
    }

    /**
     * Gets character at index
     * @param str string
     * @param index index
     * @return char at
     */
    @Universal
    public static char atIndex( String str, int index ) {
        return idx(str, index);
    }

    /**
     * Puts character at index
     * @param str string
     * @param index index
     * @param c char to put in
     * @return new string
     */
    @Universal
    public static String atIndex( String str, int index, char c ) {
            return idx (str, index, c);
    }

    /**
     * gets length
     * @param str string
     * @return length
     */
    @Universal
    public static int len( String str ) {
        return str.length();
    }

    /**
     * Gets slice of a string.
     * @param str string
     * @param start start index of slice
     * @return new string
     */
    @Universal
    public static String slc( String str, int start ) {

        return FastStringUtils.noCopyStringFromChars( Chr.slc( FastStringUtils.toCharArray(str), start ) );
    }

    /**
     * Get slice of string
     * @param str string
     * @param start start index
     * @param end end index
     * @return new string
     */
    @Universal
    public static String slc( String str, int start, int end ) {
        return FastStringUtils.noCopyStringFromChars(Chr.slc(FastStringUtils.toCharArray(str), start, end));
    }

    /**
     * Gets end slice of a string.
     * @param str string
     * @param end end index of slice
     * @return new string
     */
    @Universal
    public static String slcEnd( String str, int end ) {
        return FastStringUtils.noCopyStringFromChars( Chr.slcEnd( FastStringUtils.toCharArray(str), end ) );
    }

    /**
     * Gets character at index
     * @param str string
     * @param index index
     * @return char at
     */
    @Universal
    public static char idx( String str, int index ) {
        int i = calculateIndex( str.length(), index );

        char c = str.charAt( i );
        return c;
    }

    /**
     * Puts character at index
     * @param str string
     * @param index index
     * @param c char to put in
     * @return new string
     */
    @Universal
    public static String idx( String str, int index, char c ) {

        char[] chars = str.toCharArray();
        Chr.idx( chars, index, c );
        return new String( chars );
    }

    /**
     * See if chars is in another string
     * @param chars chars
     * @param str string
     * @return true or false
     */
    @Universal
    public static boolean in( char[] chars, String str ) {
        return Chr.in ( chars, FastStringUtils.toCharArray(str) );
    }

    /**
     * See if a char is in another string
     * @param c char
     * @param str string
     * @return true or false
     */
    @Universal
    public static boolean in( char c, String str ) {
        return Chr.in ( c, FastStringUtils.toCharArray(str) );
    }

    /**
     * See if a char is in a string
     * @param c char
     * @param offset offset
     * @param str string
     * @return true or false
     */
    @Universal
    public static boolean in( char c, int offset, String str ) {
        return Chr.in ( c, offset, FastStringUtils.toCharArray(str) );
    }

    /**
     * See if a char is in a string but in a certain bounds of string
     * @param c char
     * @param offset offset
     * @param end end of span to search
     * @param str string
     * @return true or false
     */
    @Universal
    public static boolean in( char c, int offset, int end, String str ) {
        return Chr.in ( c, offset, end, FastStringUtils.toCharArray(str) );
    }

    /**
     * Add a char to a string
     * @param str string
     * @param c char
     * @return new string
     */
    @Universal
    public static String add( String str, char c ) {
        return FastStringUtils.noCopyStringFromChars( Chr.add( FastStringUtils.toCharArray(str), c ) );
    }

    /**
     * Add one string to another
     * @param str string 1
     * @param str2 string 2
     * @return new string
     */
    @Universal
    public static String add( String str, String str2 ) {
        return FastStringUtils.noCopyStringFromChars(
                Chr.add(
                 FastStringUtils.toCharArray(str),
                 FastStringUtils.toCharArray(str2) )
        );
    }

    /**
     * Add many strings together to another
     * @param strings strings
     * @return new string
     */
    @Universal
    public static String add( String... strings ) {
        int length = 0;
        for ( String str : strings ) {
            if ( str == null ) {
                continue;
            }
            length += str.length();
        }
        CharBuf builder = CharBuf.createExact( length );
        for ( String str : strings ) {
            if ( str == null ) {
                continue;
            }
            builder.add( str );
        }
        return builder.toString();
    }

    /**
     * Add many objects converted to strings together.
     * Null are ignored so be careful.
     * @param objects objects to convert to strings
     * @return new string
     */
    public static String addObjects( Object... objects ) {
        int length = 0;
        for ( Object obj : objects ) {
            if ( obj == null ) {
                continue;
            }
            length += obj.toString().length();
        }
        CharBuf builder = CharBuf.createExact( length );
        for ( Object str : objects ) {
            if ( str == null ) {
                continue;
            }
            builder.add( str.toString() );
        }
        return builder.toString();
    }

    /**
     * Gets rid of null characters lurking in the string
     * @param str string
     * @return new string
     */
    public static String compact( String str ) {
        return FastStringUtils.noCopyStringFromChars( Chr.compact( FastStringUtils.toCharArray(str) ) );
    }

    /**
     * calculates an index for slicing.
     * @param length length of index
     * @param originalIndex original index might be negative
     * @return new index
     */
    private static int calculateIndex( final int length, int originalIndex ) {

        int index = originalIndex;

        /* Adjust for reading from the right as in
        -1 reads the 4th element if the length is 5
         */
        if ( index < 0 ) {
            index = length + index;
        }

        /* Bounds check
            if it is still less than 0, then they
            have an negative index that is greater than length
         */
         /* Bounds check
            if it is still less than 0, then they
            have an negative index that is greater than length
         */
        if ( index < 0 ) {
            index = 0;
        }
        if ( index >= length ) {
            index = length - 1;
        }
        return index;
    }

    /**
     * Split a string
     * @param str string to split
     * @return string array
     */
    public static String[] split( String str ) {
        char[][] split = Chr.split( FastStringUtils.toCharArray(str) );
        return fromCharArrayOfArrayToStringArray( split );
    }

    /**
     * Split a string by lines
     * @param str string to split
     * @return string array
     */
    public static String[] splitLines( String str ) {
        char[][] split = Chr.splitLines( FastStringUtils.toCharArray(str) );
        return fromCharArrayOfArrayToStringArray( split );
    }

    /**
     * Split a string by commas
     * @param str string to split
     * @return string array
     */
    public static String[] splitComma( String str ) {
        char[][] split = Chr.splitComma( FastStringUtils.toCharArray(str) );
        return fromCharArrayOfArrayToStringArray( split );
    }

    /**
     * Split a string by space
     * @param str string to split
     * @return string array
     */
    public static String[] splitBySpace( String str ) {
        char[][] split = CharScanner.splitBySpace( FastStringUtils.toCharArray(str) );
        return fromCharArrayOfArrayToStringArray( split );
    }

    /**
     * Split a string by pipe
     * @param str string to split
     * @return string array
     */
    public static String[] splitByPipe( String str ) {
        char[][] split = CharScanner.splitByPipe( FastStringUtils.toCharArray(str) );
        return fromCharArrayOfArrayToStringArray( split );
    }

    /**
     * Convert arrays of chars to arrays of strings
     * @param split array of chars
     * @return string array
     */
    public static String[] fromCharArrayOfArrayToStringArray( char[][] split ) {
        String[] results = new String[ split.length ];

        char[] array;

        for ( int index = 0; index < split.length; index++ ) {
            array = split[ index ];

            results[ index ] = array.length == 0 ?
                    EMPTY_STRING : FastStringUtils.noCopyStringFromChars( array );
        }
        return results;
    }

    /**
     * Convert to upper case
     * @param str string to convert
     * @return new string
     */
    public static String upper( String str ) {
        return str.toUpperCase();
    }

    /**
     * Convert to lower case
     * @param str string to convert
     * @return new string
     */
    public static String lower( String str ) {
        return str.toLowerCase();
    }

    /**
     * Convert to camel case upper (starts with upper case)
     * @param in string to convert
     * @return new string
     */
    public static String camelCaseUpper( String in ) {
        return camelCase( in, true );
    }

    /**
     * Convert to camel case lower (starts with lower case)
     * @param in string to convert
     * @return new string
     */
    public static String camelCaseLower( String in ) {
        return camelCase( in, false );
    }

    /**
     * Convert to camel case
     * @param in string to convert
     * @return new string
     */
    public static String camelCase( String in ) {
        return camelCase( in, false );
    }

    /**
     * Convert to camel case and pass upper or lower
     * @param inStr string to convert
     * @param upper upper flag
     * @return new string
     */
    public static String camelCase( String inStr, boolean upper ) {
        char[] in = FastStringUtils.toCharArray(inStr);
        char[] out = Chr.camelCase( in, upper );
        return FastStringUtils.noCopyStringFromChars( out );
    }

    /**
     * Checks to see if a string is inside of another
     * @param start start
     * @param inStr input string
     * @param end index at end
     * @return
     */
    public static boolean insideOf(String start, String inStr, String end) {
        return Chr.insideOf(FastStringUtils.toCharArray(start), FastStringUtils.toCharArray(inStr), FastStringUtils.toCharArray(end));
    }

    /**
     * Convert to under bar case
     * @param inStr input string
     * @return new string
     */
    public static String underBarCase( String inStr ) {
        char[] in = FastStringUtils.toCharArray(inStr);
        char[] out = Chr.underBarCase( in );
        return FastStringUtils.noCopyStringFromChars( out );
    }

    /**
     * See if they are equal or die
     * @param a a
     * @param b b
     */
    public static void equalsOrDie(CharSequence a, CharSequence b) {
        char[] ac = FastStringUtils.toCharArray(a);
        char[] bc = FastStringUtils.toCharArray(b);
        Chr.equalsOrDie(ac, bc);
    }

    /**
     * See if they are equal or die
     * @param a a
     * @param b b
     */
    public static void equalsOrDie(String a, String b) {
        if (a == null && b == null) {
            return;
        }
        if (a == null || b == null) {
            die("Values not equal value a=", a, "value b=", b);
        }

        char[] ac = FastStringUtils.toCharArray(a);
        char[] bc = FastStringUtils.toCharArray(b);
        Chr.equalsOrDie(ac, bc);
    }

    /**
     *
     * @param inStr
     * @param size
     * @param fill
     * @return
     */
    public static String lpad( String inStr, int size, char fill ) {
        return new String( Chr.lpad( inStr.toCharArray(), size, fill ) );
    }

    /**
     *
     * @param inStr
     * @param size
     * @return
     */
    public static String lpad( String inStr, int size ) {
        return new String( Chr.lpad( inStr.toCharArray(), size, ' ' ) );
    }

    /**
     *
     * @param inStr
     * @param size
     * @return
     */
    public static String lpad( Object inStr, int size ) {
        return new String( Chr.lpad(inStr == null ? "".toCharArray() : inStr.toString().toCharArray(), size, ' ') );
    }

    /**
     *
     * @param inStr
     * @return
     */
    public static String lpad( Object inStr) {
        return new String( Chr.lpad( inStr == null ? "".toCharArray() : inStr.toString().toCharArray(), 20, ' ' ) );
    }

    /**
     *
     * @param num
     * @param size
     * @return
     */
    public static String zfill( int num, int size ) {
        return new String( Chr.lpad( Integer.toString( num ).toCharArray(),
                size, '0' ) );
    }

    /**
     *
     * @param inStr
     * @param size
     * @param fill
     * @return
     */
    public static String rpad( String inStr, int size, char fill ) {
        return new String( Chr.rpad( inStr.toCharArray(), size, fill ) );
    }

    /**
     *
     * @param inStr
     * @param size
     * @return
     */
    public static String rpad( String inStr, int size) {
        return new String( Chr.rpad( inStr.toCharArray(), size, ' ' ) );
    }

    /**
     *
     * @param obj
     * @param size
     * @return
     */
    public static String rpad( Object obj, int size) {
        if (obj != null) {
            return new String( Chr.rpad( obj.toString().toCharArray(), size, ' ' ) );
        } else {
            return new String( Chr.rpad( "<NULL>".toCharArray(), size, ' ' ) );
        }
    }

    /**
     *
     * @param obj
     * @return
     */
    public static String rpad( Object obj) {
        if (obj != null) {
            return new String( Chr.rpad( obj.toString().toCharArray(), 20, ' ' ) );
        } else {
            return new String( Chr.rpad( "<NULL>".toCharArray(), 20, ' ' ) );
        }
    }

    /**
     *
     * @param obj
     * @param size
     * @param fill
     * @return
     */
    public static String rpad( Object obj, int size, char fill ) {
        if (obj != null) {
            return new String( Chr.rpad( obj.toString().toCharArray(), size, fill ) );
        } else {
            return new String( Chr.rpad( "<NULL>".toCharArray(), size, fill ) );
        }
    }

    /**
     *
     * @param input
     * @param split
     * @return
     */
    public static String[] split( final String input,
                                  final char split ) {
        return StringScanner.split( input, split );

    }

    /**
     *
     * @param value
     * @param str
     * @return
     */
    @Universal
    public static boolean in( String value, String str ) {
        return str.contains( value );
    }

    /**
     *
     * @param lines
     * @return
     */
    public static String lines( String... lines ) {
        return join( '\n', lines );
    }

    /**
     *
     * @param lines
     * @return
     */
    public static String linesConvertQuotes( String... lines ) {

        for (int index=0; index < lines.length; index++) {
            lines[index]=lines[index].replace( '\'', '"' );
        }
        return join( '\n', lines );
    }

    /**
     *
     * @param delim
     * @param args
     * @return
     */
    public static String join( char delim, String... args ) {
        CharBuf builder = CharBuf.create( 10 * args.length );

        int index = 0;
        for ( String arg : args ) {
            builder.add( arg );
            if ( !( index == args.length - 1 ) ) {
                builder.add( delim );
            }
            index++;
        }
        return builder.toString();
    }

    /**
     *
     * @param delim
     * @param args
     * @return
     */
    public static String joinObjects( char delim, Object... args ) {
        CharBuf builder = CharBuf.create( 10 * args.length );

        int index = 0;
        for ( Object arg : args ) {
            builder.add( arg == null ? "null" : arg.toString() );
            if ( !( index == args.length - 1 ) ) {
                builder.add( delim );
            }
            index++;
        }
        return builder.toString();
    }

    /**
     *
     * @param args
     * @return
     */
    public static String join( String... args ) {
        CharBuf builder = CharBuf.create( 10 * args.length );

        for ( String arg : args ) {
            builder.add( arg );
        }
        return builder.toString();
    }

    /**
     *
     * @param delim
     * @param args
     * @return
     */
    public static String joinCollection( char delim, List<?> args ) {
        CharBuf builder = CharBuf.create( 10 * args.size() );

        int index = 0;
        for ( Object arg : args ) {
            if ( arg == null ) {
                continue;
            }
            builder.add( arg.toString() );
            if ( !( index == args.size() - 1 ) ) {
                builder.add( delim );
            }
            index++;
        }
        return builder.toString();

    }

    /**
     *
     * @param delim
     * @param args
     * @return
     */
    public static String joinCollection( String delim, List<?> args ) {
        CharBuf builder = CharBuf.create( 10 * args.size() );

        int index = 0;
        for ( Object arg : args ) {
            if ( arg == null ) {
                continue;
            }
            builder.add( arg.toString() );
            if ( !( index == args.size() - 1 ) ) {
                builder.add( delim );
            }
            index++;
        }
        return builder.toString();

    }

    /**
     *
     * @param str
     * @return
     */
    @Universal
    public static boolean isEmpty( String str ) {
        if ( str == null ) {
            return true;
        } else {
            return str.isEmpty();
        }

    }

    @Universal
    public static boolean isEmpty( Object str ) {
        if ( str == null ) {
            return true;
        } else {
            return str.toString().isEmpty();
        }

    }

    /**
     *
     * @param string
     * @return
     */
    public static String uncapitalize( String string ) {
        StringBuilder rv = new StringBuilder();
        if ( string.length() > 0 ) {
            rv.append( Character.toLowerCase( string.charAt( 0 ) ) );
            if ( string.length() > 1 ) {
                rv.append( string.substring( 1 ) );
            }
        }
        return rv.toString();
    }

    /**
     *
     * @param object
     * @param defaultString
     * @return
     */
    public static String toString(Object object, String defaultString) {
        if (object == null) {
            return defaultString;
        } else {
            return object.toString();
        }
    }

    /**
     *
     * @param object
     * @return
     */
    public static String toString(Object object) {
        if (object == null) {
            return "";
        } else {
            return object.toString();
        }
    }

    /**
     *
     * @param str
     * @return
     */
    public static String str(Object str) {
        return str == null ? "<NULL>" : str.toString();
    }

    /**
     *
     * @param name
     * @param startsWithList
     * @return
     */
    public static boolean startsWithItemInCollection(String name, Collection<String> startsWithList) {
        for (String startsWith : startsWithList) {
            if (name.startsWith(startsWith)) {
                return true;
            }
        }
        return false;
    }

    public static String readable(String s) {
        return s.replace("\\n", "\n");
    }

    /**
     * Quote a string
     * @param s input string
     * @return new String
     */
    public static String quote(String s) {
        return add("\"", s, "\"");
    }

    /**
     * single Quote a string
     * @param s input string
     * @return new String
     */
    public static String singleQuote(String s) {
        return add("\'", s, "\'");
    }

    /**
     * double Quote a string
     * @param s input string
     * @return new String
     */
    public static String doubleQuote(String s) {
        return add("\"", s, "\"");
    }

    /** Create a string from bytes.
     * @param bytes bytes in
     * @return string out
     */
    public static String str(byte[] bytes) {
        return new String(bytes, StandardCharsets.UTF_8);

    }

    /**
     * Do a nice pretty print of a number.
     * Add commas and such.
     * @param count number
     * @return string format of number
     */
    public static String num(Number count) {

        if (count == null) {
            return "";
        }
        if (count instanceof  Double || count instanceof BigDecimal) {
            String s = count.toString();
            if (idx(s, 1) == '.' && s.length() > 7) {
                s = slc(s, 0, 5);
                return s;
            } else {
                return s;
            }

        } else if (count instanceof Integer || count instanceof Long || count instanceof Short || count instanceof BigInteger){
            String s = count.toString();
            s = new StringBuilder(s).reverse().toString();

            CharBuf buf = CharBuf.create(s.length());

            int index = 0;
            for (char c : s.toCharArray()) {

                index++;

                buf.add(c);

                if (index % 3 == 0) {
                    buf.add(',');
                }

            }

            if (buf.lastChar() == ',') {
                buf.removeLastChar();
            }

            s = buf.toString();

            s = new StringBuilder(s).reverse().toString();

            return s;

        }

        return count.toString();

    }

    /**
     * Null safe empty check.
     *
     * @param str - the String to check
     * @return whether the String is null || empty
     */
    public static boolean empty(String str) { return str == null || str.isEmpty(); }

}