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

package net.ranides.assira.text;

import java.io.Reader;
import java.io.Serializable;
import java.io.Writer;
import java.util.Iterator;
import java.util.function.Function;
import java.util.function.IntFunction;
import net.ranides.assira.collection.arrays.ArrayUtils;
import net.ranides.assira.collection.arrays.NativeArray;
import net.ranides.assira.collection.arrays.NativeArrayUtils;
import net.ranides.assira.math.MathUtils;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
@SuppressWarnings({
    "PMD.TooManyPublicMethods",
    "PMD.TooManyDifferentMethods"
})
public class StrBuffer implements Appendable, CharSequence, Serializable {

    private static final long serialVersionUID = 1L;

    protected static final char[] EMPTY = new char[0];

    protected char[] buffer;

    protected int size;

    private final BuilderState options = new BuilderState();

    public StrBuffer(int capacity) {
        this.buffer = new char[capacity];
    }

    public StrBuilderOptions options() {
        return options;
    }
    
    @Override
    public int length() {
        return size;
    }
    
    public int capacity() {
        return buffer.length;
    }
    
    public StrBuffer resize(int length, char c) {
        if (length < 0) {
            throw new IllegalArgumentException("negative size: " + length);
        }
        if (length > capacity()) {
            throw new IllegalArgumentException(length + " is greater than " + capacity());
        }
        if(length > size) {
            ArrayUtils.fill(buffer, size, length, c);
        }
        
        size = length;
        return this;
    }
    
    public StrBuffer clear() {
        size = 0;
        return this;
    }
    
    @Override
    public char charAt(int index) {
        return buffer[index];
    }
    
    public StrBuffer setCharAt(int index, char value) {
        buffer[index] = value;
        return this;
    }

    @Override
    public CharSequence subSequence(int begin, int end) {
        return StringUtils.wrap(buffer, begin, end);
    }
    
    public char[] getChars() {
        return getChars(0, size);
    }
    
    public char[] getChars(int begin, int end) {
        if (end == begin) {
            return StrBuffer.EMPTY;
        }
        return ArrayUtils.slice(buffer, begin, end);
    }
    
    public char[] getChars(char[] target) {
        return getChars(0, size, target, 0);
    }
    
    public char[] getChars(int begin, int end, char target[], int offset) {
        System.arraycopy(buffer, begin, target, offset, end - begin);
        return target;
    }
    
    public String getRightFragment(int length) {
        int n = MathUtils.clip(length, 0, size);
        return new String(buffer, size - n, n);
    }

    public String getLeftFragment(int length) {
        return new String(buffer, 0, MathUtils.clip(length, 0, size));
    }

    public StrBuffer reverse() {
        NativeArrayUtils.reverse(NativeArray.wrap(buffer), size);
        return this;
    }
    
    public StrBuffer open() {
        return StrBuffer.this.append(options.reset().open());
    }

    public StrBuffer open(String open) {
        options.open(open);
        return open();
    }

    public StrBuffer open(String open, String close) {
        options.open(open).close(close);
        return open();
    }

    public StrBuffer open(String open, String close, String separator) {
        options.open(open).close(close).separator(separator);
        return open();
    }
    
    public StrBuffer close() {
        return StrBuffer.this.append(options.close());
    }

    public StrBuffer item() {
        if(0 != options.counter++) {
            StrBuffer.this.append(options.separator());
        }
        return this;
    }
    
    public StrBuffer endl() {
        return StrBuffer.this.append(options.endl());
    }
    
    public StrBuffer append() {
        return this;
    }
    
    @Override
    public StrBuffer append(CharSequence value) {
        return append(value, 0, value.length());
    }

    @Override
    public StrBuffer append(CharSequence value, int begin, int end) {
        for(int i=begin; i<end; i++) {
            buffer[size++] = value.charAt(i);
        }
        return this;
    }
    
    public StrBuffer append(String value) {
        return append(value, 0, value.length());
    }
    
    public StrBuffer append(String value, int begin, int end) {
        int n = end - begin;
        value.getChars(begin, end, buffer, size);
        size += n;
        return this;
    }
    
    public StrBuffer append(StringBuffer value) {
        return append(value, 0, value.length());
    }
    
    public StrBuffer append(StringBuffer value, int begin, int end) {
        int n = end - begin;
        value.getChars(begin, end, buffer, size);
        size += n;
        return this;
    }
    
    public StrBuffer append(StringBuilder value) {
        return append(value, 0, value.length());
    }
    
    public StrBuffer append(StringBuilder value, int begin, int end) {
        int n = end - begin;
        value.getChars(begin, end, buffer, size);
        size += n;
        return this;
    }
    
    public StrBuffer append(char[] value) {
        return append(value, 0, value.length);
    }
    
    public StrBuffer append(char[] value, int begin, int end) {
        int n = end - begin;
        System.arraycopy(value, begin, buffer, size, n);
        size += n;
        return this;
    }

    @Override
    public StrBuffer append(char c) {
        buffer[size++] = c;
        return this;
    }
    
    public StrBuffer append(Object value) {
        return append(value.toString());        
    }
    
    public StrBuffer append(boolean value) {
        return append(value ? "true" : "false");
    }
    
    public StrBuffer append(int value) {
        return append(String.valueOf(value));
    }
    
    public StrBuffer append(long value) {
        return append(String.valueOf(value));
    }

    public StrBuffer append(float value) {
        return append(String.valueOf(value));
    }

    public StrBuffer append(double value) {
        return append(String.valueOf(value));
    }
    
    public StrBuffer append(Iterable<?> values) {
        return append(values.iterator(), Object::toString);
    }
    
    public <T> StrBuffer append(Iterable<? extends T> values, Function<? super T,String> function) {
        return append(values.iterator(), function);
    }
    
    public <T> StrBuffer append(Iterator<? extends T> values) {
        return append(values, Object::toString);
    }
    
    public <T> StrBuffer append(Iterator<? extends T> values, Function<? super T,String> function) {
        if ( !values.hasNext() ) {
            return this;
        }
        String separator = options().separator();
        for (;;) {
            T item = values.next();
            append(function.apply(item));
            if ( !values.hasNext() ) {
                return this;
            }
            append(separator);
        }
    }
    
    public <T> StrBuffer append(T[] values) {
        return append(values, Object::toString);
    }
    
    public <T> StrBuffer append(T[] values, Function<? super T,String> function) {
        if ( 0==values.length ) {
            return this;
        }
        String separator = options().separator();
        append(function.apply(values[0]));
        for (int i=1; i<values.length; i++) {
            append(separator).append(function.apply(values[i]));
        }
        return this;
    }
    
    public <T> StrBuffer append(NativeArray values) {
        return append(values.size(), i -> String.valueOf(values.get(i)));
    }
    
    public <T> StrBuffer append(int size, IntFunction<String> function) {
        if ( 0==size ) {
            return this;
        }
        String separator = options().separator();
        append(function.apply(0));
        for (int i=1; i<size; i++) {
            append(separator).append(function.apply(i));
        }
        return this;
    }

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

    private boolean equalsSB(StrBuffer 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);
    }

    public StringBuilder toBuilder() {
        return new StringBuilder(size).append(buffer, 0, size);
    }

    public Writer asWriter() {
        return new SWriter();
    }

    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;
            }
            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 Writer {

        @Override
        public void close() {
            // do nothing
        }

        @Override
        public void flush() {
            // do nothing
        }

        @Override
        public void write(int c) {
            StrBuffer.this.append((char) c);
        }

        @Override
        public void write(char[] buffer) {
            StrBuffer.this.append(buffer);
        }

        @Override
        public void write(char[] buffer, int offset, int length) {
            StrBuffer.this.append(buffer, offset, length);
        }

        @Override
        public void write(String str) {
            StrBuffer.this.append(str);
        }

        @Override
        public void write(String str, int offset, int length) {
            StrBuffer.this.append(str, offset, length);
        }

    }

    private static final class BuilderState extends StrBuilderOptions {

        private static final long serialVersionUID = 1L;

        protected int counter;

        private String endl = StringUtils.ENDL;

        private String separator = ",";

        private String open = "";

        private String close = "";

        @Override
        public StrBuilder builder() {
            throw new UnsupportedOperationException();
        }

        @Override
        public String endl() {
            return endl;
        }

        @Override
        public BuilderState endl(String value) {
            this.endl = value;
            return this;
        }

        @Override
        public String separator() {
            return separator;
        }

        @Override
        public BuilderState separator(String value) {
            this.separator = value;
            return this;
        }

        @Override
        public String open() {
            return open;
        }

        @Override
        public BuilderState open(String value) {
            this.open = value;
            return this;
        }

        @Override
        public String close() {
            return close;
        }

        @Override
        public BuilderState close(String value) {
            this.close = value;
            return this;
        }

        protected BuilderState reset() {
            this.counter = 0;
            return this;
        }

    }

}

