/*
 * @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.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
import java.util.function.Function;
import java.util.function.IntFunction;
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.generic.ValueUtils;
import net.ranides.assira.math.MathUtils;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
@SuppressWarnings({
    "PMD.TooManyPublicMethods",
    "PMD.TooManyDifferentMethods"
})
public final class StrBuilder implements CharSequence, Appendable, Serializable {
    
    private static final long serialVersionUID = 1L;
    
    private static final StrBuilderOptions OPTIONS = new StrBuilderOptions();
    
    protected static final char[] EMPTY = new char[0];
    
    protected static final int CAPACITY = 32;

    private char[] buffer;
    
    private int size;
    
    Deque<BuilderState> options = new ArrayDeque<>(); // StrBuilderOptions.DEFAULT;
    
    public StrBuilder() {
        this(CAPACITY);
    }

    public StrBuilder(int capacity) {
        buffer = new char[capacity];
        options.push(new BuilderState(OPTIONS));
    }
    
    public StrBuilderOptions options() {
        return options.peek();
    }
    
    @Override
    public int length() {
        return size;
    }

    public int capacity() {
        return buffer.length;
    }
    
    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;
    }

    public StrBuilder reserve(int capacity) {
        buffer = ArrayAllocator.grow(buffer, capacity, size);
        return this;
    }

    public StrBuilder trim() {
        if (buffer.length > length()) {
            char[] old = buffer;
            buffer = new char[length()];
            System.arraycopy(old, 0, buffer, 0, size);
        }
        return this;
    }
    
    public StrBuilder trim(int n) {
        if (buffer.length > n) {
            buffer = ArrayAllocator.trim(buffer, Math.max(n,size));
        }
        return this;
    }
    
    public StrBuilder clear() {
        size = 0;
        BuilderState state = options.peek().copy();
        options.clear();
        options.push(state);
        return this;
    }
    
    @Override
    public char charAt(int index) {
        StringUtils.checkIndex(size, index);
        return buffer[index];
    }
    
    public StrBuilder setCharAt(int index, char value) {
        StringUtils.checkIndex(size, index);
        buffer[index] = value;
        return this;
    }

    @Override
    public CharSequence subSequence(int begin, int end) {
        NativeArrayAllocator.ensureFromTo(size, begin, end);
        return StringUtils.wrap(buffer, begin, end);
    }
        
    public char[] getChars() {
        return getChars(0, size);
    }
    
    public char[] getChars(int begin, int end) {
        StringUtils.checkRange(size, begin, end);
        if (end == begin) {
            return EMPTY; //NOPMD
        }
        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) {
        StringUtils.checkRange(size, begin, end);
        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 StrBuilder reverse() {
        NativeArrayUtils.reverse(NativeArray.wrap(buffer), size);
        return this;
    }
    
    public StrBuilder open() {
        options.push(options.peek().derive());
        return append(options.peek().open());
    }
    
    public StrBuilder open(String open) {
        options.push(options.peek().derive().open(open));
        return append(open);
    }
    
    public StrBuilder open(String open, String close) {
        options.push(options.peek().derive().open(open).close(close));
        return append(open);
    }
    
    public StrBuilder open(String open, String close, String separator) {
        options.push(options.peek().derive().open(open).close(close).separator(separator));
        return append(open);
    }
    
    public StrBuilder close() {
        if(options.size() <= 1) {
            throw new IllegalStateException("There is not block to close.");
        }
        return append(options.pop().close());
    }
    
    public StrBuilder item() {
        BuilderState state = options.peek();
        if(0 != state.counter++) {
            append(state.separator());
        }
        return this;
    }
    
    public StrBuilder endl() {
        return append(options.peek().endl());
    }
    
    public StrBuilder append() {
        String value = options.peek().blank();
        return null == value ? this : append(value);
    }

    @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;
    }
    
    public StrBuilder append(String value) {
        return (value == null) ? append() : append(value, 0, value.length());
    }
    
    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;
    }
    
    public StrBuilder append(StringBuffer value) {
        return (value == null) ? append() : append(value, 0, value.length());
    }
    
    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;
    }
    
    public StrBuilder append(StringBuilder value) {
        return (value == null) ? append() : append(value, 0, value.length());
    }
    
    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;
    }
    
    public StrBuilder append(char[] value) {
        return (value == null) ? append() : append(value, 0, value.length);
    }
    
    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;
    }

    public StrBuilder append(Object value) {
        return (value == null) ? append() : append(value.toString());        
    }
    
    public StrBuilder append(boolean value) {
        return append(value ? "true" : "false");
    }
    
    public StrBuilder append(int value) {
        return append(String.valueOf(value));
    }

    public StrBuilder append(long value) {
        return append(String.valueOf(value));
    }

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

    public StrBuilder append(double value) {
        return append(String.valueOf(value));
    }
    
    public StrBuilder append(Iterable<?> values) {
		return append(values.iterator(), this::asString);
	}
    
    public <T> StrBuilder append(Iterable<? extends T> values, Function<? super T,String> function) {
		return append(values.iterator(), function);
	}
    
    public <T> StrBuilder append(Iterator<? extends T> values) {
        return append(values, this::asString);
    }
        
    public <T> StrBuilder append(Iterator<? extends T> values, Function<? super T,String> function) {
        if ( !values.hasNext() ) {
            return this;
        }
        String separator = options.peek().separator();
        for (;;) {
            T item = values.next();
            append(function.apply(item));
            if ( !values.hasNext() ) {
                return this;
            }
            append(separator);
        }
	}
    
    public <T> StrBuilder append(T[] values) {
        return append(values, this::asString);
    }
    
    private String asString(Object value) {
        return null == value ? null : value.toString();
    }
    
    public <T> StrBuilder append(T[] values, Function<? super T,String> function) {
        if ( 0==values.length ) {
            return this;
        }
        String separator = options.peek().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> StrBuilder append(NativeArray values) {
        return append(values.size(), i -> String.valueOf(values.get(i)));
    }
    
    public <T> StrBuilder append(int size, IntFunction<String> function) {
        if ( 0==size ) {
            return this;
        }
        String separator = options.peek().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 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);
    }
    
    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;
            }
            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 Writer {
        
        @Override
        public void close() {
            // do nothing
        }
        
        @Override
        public void flush() {
            // do nothing
        }
        
        @Override
        public void write(int c) {
            StrBuilder.this.append((char) c);
        }
        
        @Override
        public void write(char[] buffer) {
            StrBuilder.this.append(buffer);
        }
        
        @Override
        public void write(char[] buffer, int offset, int length) {
            StrBuilder.this.append(buffer, offset, length);
        }
        
        @Override
        public void write(String str) {
            StrBuilder.this.append(str);
        }
        
        @Override
        public void write(String str, int offset, int length) {
            StrBuilder.this.append(str, offset, length);
        }
        
    }
    
    protected final class BuilderState extends StrBuilderOptions {
        
        private static final long serialVersionUID = 1L;
        
        private final StrBuilderOptions parent;
        
        protected int counter;
        
        private String blank;

        private String endl;

        private String separator;

        private String open;

        private String close;
        
        public BuilderState(StrBuilderOptions parent) {
            this.parent = parent;
        }
        
        public BuilderState derive() {
            return new BuilderState(this);
        }
        
        public BuilderState copy() {
            BuilderState state = new BuilderState(OPTIONS);
            
            state.blank(this.blank());
            state.endl(this.endl());
            state.separator(this.separator());
            state.open(this.open());
            state.close(this.close());
            
            return state;
        }

        @Override
        public StrBuilder builder() {
            return StrBuilder.this;
        }
        
        @Override
        public String blank() {
            return ValueUtils.or(blank, parent.blank());
        }

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

        @Override
        public String endl() {
            return ValueUtils.or(endl, parent.endl());
        }

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

        @Override
        public String separator() {
            return ValueUtils.or(separator, parent.separator());
        }

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

        @Override
        public String open() {
            return ValueUtils.or(open, parent.open());
        }

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

        @Override
        public String close() {
            return ValueUtils.or(close, parent.close());
        }

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