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

import java.io.Reader;
import java.io.Serializable;
import java.io.Writer;

import net.ranides.assira.collection.arrays.ArrayAllocator;
import net.ranides.assira.collection.arrays.ArrayUtils;
import net.ranides.assira.collection.arrays.NativeArray;
import net.ranides.assira.collection.arrays.NativeArrayAllocator;
import net.ranides.assira.collection.arrays.NativeArrayUtils;
import net.ranides.assira.math.MathUtils;

/**
 * This class is alternative to java StringBuilder:
 *  - it offers a bit more methods for appending
 *  - it supports "printf" append
 *  - it supports appending list of items (using some join separator)
 *  - it can be converted to Reader/Writer views
 *
 *  Configuration options (for example list separators) could be configured independently in every opened scope
 *
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@SuppressWarnings({
    "PMD.TooManyPublicMethods",
    "PMD.TooManyDifferentMethods"
})
public class StrBuilder extends AbstractAppender<StrBuilder> implements CharSequence, Appendable, Serializable {
    
    private static final long serialVersionUID = 1L;

    protected static final char[] EMPTY = new char[0];
    
    protected static final int CAPACITY = 32;

    protected char[] buffer;

    protected int size;

    /**
     * Creates new builder with default capacity
     */
    public StrBuilder() {
        this(CAPACITY);
    }

    /**
     * Creates new builder with specified capacity
     *
     * @param capacity capacity
     */
    public StrBuilder(int capacity) {
        buffer = new char[capacity];
    }

    /**
     * Returns current size of buffer
     *
     * @return int
     */
    @Override
    public int length() {
        return size;
    }

    /**
     * Returns current size of internal buffer.
     * Capacity is automatically increased on demand.
     *
     * @return int
     */
    public int capacity() {
        return buffer.length;
    }

    /**
     * Changes size of content stored the buffer.
     * If content is longer than "length", some data will be discarded.
     * If content is shorter than "length", it will append "c" characters.
     *
     * @param length length
     * @param c c
     * @return this
     */
    public StrBuilder resize(int length, char c) {
        if (length < 0) {
            throw new IllegalArgumentException("negative size: " + length);
        }
        if (length > size) {
            reserve(length);
            ArrayUtils.fill(buffer, size, length, c);
        }
        size = length;
        return this;
    }

    /**
     * Allocates space for internal buffer.
     * If buffer is larger than specified "capacity", it does nothing.
     *
     * @param capacity capacity
     * @return this
     */
    public StrBuilder reserve(int capacity) {
        buffer = ArrayAllocator.grow(buffer, capacity, size);
        return this;
    }

    /**
     * Reallocates internal buffer to current "size"
     *
     * @return this
     */
    public StrBuilder trim() {
        return trim(size);
    }

    /**
     * Reallocates internal buffer to specified size "n".
     *
     * Please note that it preserves current content, so trimmed buffer can be larger than "n"
     *
     * @param n n
     * @return this
     */
    public StrBuilder trim(int n) {
        if (buffer.length > n && buffer.length!=Math.max(n,size)) {
            buffer = ArrayAllocator.trim(buffer, Math.max(n,size));
        }
        return this;
    }

    /**
     * Clears data appended into this buffer
     *
     * @return this
     */
    public StrBuilder clear() {
        size = 0;
        resetOptions();
        return this;
    }
    
    @Override
    public char charAt(int index) {
        StringUtils.checkIndex(size, index);
        return buffer[index];
    }

    /**
     * Changes character inside buffer at specified position.
     *
     * @param index index
     * @param value value
     * @return this
     */
    public StrBuilder setCharAt(int index, char value) {
        StringUtils.checkIndex(size, index);
        buffer[index] = value;
        return this;
    }

    /**
     * Returns subsequence which is view of this buffer.
     * Every change inside buffer will be visible in returned CharSequence
     *
     * @param begin begin
     * @param end end
     * @return CharSequence
     */
    @Override
    public CharSequence subSequence(int begin, int end) {
        NativeArrayAllocator.ensureFromTo(size, begin, end);
        return StringUtils.wrap(buffer, begin, end);
    }

    /**
     * Returns new array with current data copied into it
     *
     * @return copy
     */
    public char[] getChars() {
        return getChars(0, size);
    }

    /**
     * Returns new array with fragment of current data copied into it
     *
     * @param begin begin
     * @param end end
     * @return copy
     */
    public char[] getChars(int begin, int end) {
        StringUtils.checkRange(size, begin, end);
        if (end == begin) {
            return EMPTY; //NOPMD
        }
        return ArrayUtils.slice(buffer, begin, end);
    }

    /**
     * Copies current data into provided array.
     *
     * @param target target
     * @return target
     */
    public char[] getChars(char[] target) {
        return getChars(0, size, target, 0);
    }

    /**
     * Copies fragment of current data into provided array
     * Copy operation will start writing to array from "offset"
     *
     * @param begin begin
     * @param end end
     * @param target target
     * @param offset offset
     * @return target
     */
    public char[] getChars(int begin, int end, char[] target, int offset) {
        StringUtils.checkRange(size, begin, end);
        System.arraycopy(buffer, begin, target, offset, end - begin);
        return target;
    }

    /**
     * Returns string with "length" leading characters.
     *
     * @param length length
     * @return String
     */
    public String getHeadFragment(int length) {
        return new String(buffer, 0, MathUtils.clip(length, 0, size));
    }

    /**
     * Returns string with "length" trailing characters.
     *
     * @param length length
     * @return String
     */
    public String getTailFragment(int length) {
        int n = MathUtils.clip(length, 0, size);
        return new String(buffer, size - n, n);
    }

    /**
     * Reverses characters inside buffer.
     *
     * @return this
     */
    public StrBuilder reverse() {
        NativeArrayUtils.reverse(NativeArray.wrap(buffer), size);
        return this;
    }

    @Override
    public StrBuilder append(CharSequence value) {
        return (value == null) ? append() : append(value, 0, value.length());
    }

    @Override
    public StrBuilder append(CharSequence value, int begin, int end) {
        if (value == null) {
            return append();
        } 
        reserve(length() + (end - begin));
        for(int i=begin; i<end; i++) {
            buffer[size++] = value.charAt(i);
        }
        return this;
    }

    /**
     * Appends string into buffer
     *
     * @param value value
     * @return this
     */
    public StrBuilder append(String value) {
        return (value == null) ? append() : append(value, 0, value.length());
    }

    /**
     * Appends fragment of string into buffer
     *
     * @param value value
     * @param begin begin
     * @param end end
     * @return this
     */
    public StrBuilder append(String value, int begin, int end) {
        if (value == null) {
            return append();
        }
        NativeArrayAllocator.ensureFromTo(value.length(), begin, end);
        int n = end - begin;
        if (n > 0) {
            reserve(size + n);
            value.getChars(begin, end, buffer, size);
            size += n;
        }
        return this;
    }

    /**
     * Appends StringBuffer into buffer
     *
     * @param value value
     * @return this
     */
    public StrBuilder append(StringBuffer value) {
        return (value == null) ? append() : append(value, 0, value.length());
    }

    /**
     * Appends fragment of StringBuffer into buffer
     *
     * @param value value
     * @param begin begin
     * @param end end
     * @return this
     */
    public StrBuilder append(StringBuffer value, int begin, int end) {
        if (value == null) {
            return append();
        }
        NativeArrayAllocator.ensureFromTo(value.length(), begin, end);
        int n = end - begin;
        if (n > 0) {
            reserve(size + n);
            value.getChars(begin, end, buffer, size);
            size += n;
        }
        return this;
    }

    /**
     * Appends StringBuilder into buffer
     *
     * @param value value
     * @return this
     */
    public StrBuilder append(StringBuilder value) {
        return (value == null) ? append() : append(value, 0, value.length());
    }

    /**
     * Appends fragment of StringBuilder into buffer
     *
     * @param value value
     * @param begin begin
     * @param end end
     * @return this
     */
    public StrBuilder append(StringBuilder value, int begin, int end) {
        if (value == null) {
            return append();
        }
        NativeArrayAllocator.ensureFromTo(value.length(), begin, end);
        int n = end - begin;
        if (n > 0) {
            reserve(size + n);
            value.getChars(begin, end, buffer, size);
            size += n;
        }
        return this;
    }

    /**
     * Appends chars into buffer
     *
     * @param value value
     * @return this
     */
    public StrBuilder append(char[] value) {
        return (value == null) ? append() : append(value, 0, value.length);
    }

    /**
     * Appends fragment of chars into buffer
     *
     * @param value value
     * @param begin begin
     * @param end end
     * @return this
     */
    public StrBuilder append(char[] value, int begin, int end) {
        if (value == null) {
            return append();
        }
        NativeArrayAllocator.ensureFromTo(value.length, begin, end);
        int n = end - begin;
        if (n > 0) {
            reserve(size + end);
            System.arraycopy(value, begin, buffer, size, n);
            size += n;
        }
        return this;
    }

    @Override
    public StrBuilder append(char c) {
        reserve(size + 1);
        buffer[size++] = c;
        return this;
    }

    /**
     * Replaces characters from specified range by new value.
     * Shifts all data, if necessary, i.e. value had different length than range.
     *
     * @param begin begin
     * @param end end
     * @param value value
     * @return this
     */
    public StrBuilder replace(int begin, int end, CharSequence value) {
        if(value == null) {
            value = options().blank();
        }
        if(value == null) {
            value = "";
        }
        int d = value.length() - (end - begin);
        if(d>0) {
            insertBytes(begin, d);
        } else {
            removeBytes(begin, -d);
        }
        copy(begin, value, 0, value.length());
        return this;
    }

    /**
     * Replaces characters from specified range by new value
     * Shifts all data, if necessary, i.e. value had different length than range.
     *
     * @param begin begin
     * @param end end
     * @param value value
     * @return this
     */
    public StrBuilder replace(int begin, int end, char[] value) {
        if(value == null) {
            return replace(begin, end, options().blank());
        }
        int d = value.length - (end - begin);
        if(d>0) {
            insertBytes(begin, d);
        } else {
            removeBytes(begin, -d);
        }
        copy(begin, value, 0, value.length);
        return this;
    }

    /**
     * Inserts value at specified index.
     * Shifts content already stored in buffer if necessary.
     *
     * @param offset offset
     * @param value value
     * @return this
     */
    public StrBuilder insert(int offset, CharSequence value) {
        insertBytes(offset, value.length());
        copy(offset, value, 0, value.length());
        return this;
    }

    /**
     * Inserts value at specified index.
     * Shifts content already stored in buffer if necessary.
     *
     * @param offset offset
     * @param value value
     * @return this
     */
    public StrBuilder insert(int offset, char[] value) {
        insertBytes(offset, value.length);
        copy(offset, value, 0, value.length);
        return this;
    }

    protected StrBuilder copy(int offset, CharSequence value, int begin, int end) {
        for(int i=begin; i<end; i++, offset++) {
            buffer[offset] = value.charAt(i);
        }
        return this;
    }

    protected StrBuilder copy(int offset, char[] value, int begin, int end) {
        System.arraycopy(value, begin, buffer, offset, end-begin);
        return this;
    }

    protected StrBuilder removeBytes(int offset, int count) {
        moveBytes(offset+count, offset, size - (offset+count));
        size -= count;
        return this;
    }

    protected StrBuilder insertBytes(int offset, int count) {
        reserve(size + count);
        moveBytes(offset, offset + count, size - offset);
        size += count;
        return this;
    }

    protected StrBuilder moveBytes(int src, int target, int count) {
        System.arraycopy(buffer, src, buffer, target, count);
        return this;
    }

    @Override
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }
        if (object instanceof StrBuilder) {
            return equalsSB((StrBuilder)object);
        }
        if (object instanceof CharSequence) {
            return equalsCS((CharSequence)object);
        }
        return false;
    }

    private boolean equalsSB(StrBuilder other) {
        if (this.size != other.size) {
            return false;
        }
        for(int i=0; i<size; i++) {
            if(this.buffer[i] != other.buffer[i]) {
                return false;
            }
        }
        return true;
    }

    private boolean equalsCS(CharSequence other) {
        if (this.size != other.length()) {
            return false;
        }
        for(int i=0; i<size; i++) {
            if(this.buffer[i] != other.charAt(i)) {
                return false;
            }
        }
        return true;
    }

    @Override
    public int hashCode() {
        return NativeArray.wrap(buffer).hashCode(0, size);
    }
    
    @Override
    public String toString() {
        return new String(buffer, 0, size);
    }

    /**
     * Creates new StringBuilder with buffer content.
     * StringBuilder capacity is exactly current size.
     *
     * @return StringBuilder
     */
    public StringBuilder toBuilder() {
        return new StringBuilder(size).append(buffer, 0, size);
    }

    /**
     * Returns new instance of Writer view.
     * Writer can be used to change buffer and vice versa.
     *
     * @return Writer
     */
    public Writer asWriter() {
        return new SWriter();
    }

    /**
     * Returns new instance of Reader view.
     * Changes inside buffer will be visible in Reader.
     *
     * @return Reader
     */
    public Reader asReader() {
        return new SReader();
    }
    
    private final class SReader extends Reader {
        
        private int pos;
        private int mark;

        @Override
        public void close() {
            // do nothing
        }
        
        @Override
        public int read() {
            if (!ready()) {
                return -1;
            }
            return buffer[pos++];
        }
        
        @Override
        public int read(char[] target, int offset, int length) {
            if (pos >= length()) {
                return -1;
            }
            if (pos + length > length()) {
                length = length() - pos;
            }
            StrBuilder.this.getChars(pos, pos + length, target, offset);
            pos += length;
            return length;
        }
        
        @Override
        public long skip(long n) {
            if (pos + n > size) {
                n = size - pos;
            }
            if (n < 0) {
                return 0;
            }
            pos += n;
            return n;
        }
        
        @Override
        public boolean ready() {
            return pos < size;
        }
        
        @Override
        public boolean markSupported() {
            return true;
        }
        
        @Override
        public void mark(int readAheadLimit) {
            mark = pos;
        }
        
        @Override
        public void reset() {
            pos = mark;
        }

    }

    private final class SWriter extends AbstractPrintWriter {
        
        @Override
        public void close() {
            // do nothing
        }
        
        @Override
        public void flush() {
            // do nothing
        }

    }

}
