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

import net.ranides.assira.collection.arrays.NativeArray;
import net.ranides.assira.generic.LazySupplier;
import net.ranides.assira.generic.LazySuppliers;

import java.io.Serializable;
import java.io.Writer;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Formatter;
import java.util.Iterator;
import java.util.Locale;
import java.util.function.Function;
import java.util.function.IntFunction;

/**
 * This is base abstract class with shared functionality offered by StrBuilder and StrAppender.
 * @param <P>
 */
@SuppressWarnings({
    "PMD.TooManyPublicMethods",
    "PMD.TooManyDifferentMethods"
})
public abstract class AbstractAppender<P extends AbstractAppender<P>> implements Appendable, Serializable {

    private final LazySupplier<Formatter> formatter = LazySuppliers.unique(() -> new Formatter(this));

    private final Deque<Options> options;

    protected AbstractAppender() {
        this.options = new ArrayDeque<>();
        this.options.push(new Options(null));
    }

    protected final P that() {
        return (P)this;
    }

    /**
     * Returns options for current block.
     *
     * StrAppender supports nested blocks marked by "open" and "close" methods.
     * Nested blocks inherit options from parent block, but can be modified without affecting other blocks.
     *
     * For example, you can open block, override separator, append some items and forget after close.
     * Your change won't brake later usages of separator in former blocks.
     *
     * @return options
     */
    public final Options options() {
        return options.peek();
    }

    protected final void resetOptions() {
        Options state = options.peek().copy();
        options.clear();
        options.push(state);
    }

    /**
     * Starts new block in list mode.
     * Current opened block is stacked and will be restored on close.
     *
     * Appends to the buffer "options.open" string
     *
     * @return this
     */
    public final P open() {
        options.push(options.peek().derive());
        return append(options.peek().open());
    }

    /**
     * Starts new block in list mode.
     * Current opened block is stacked and will be restored on close.
     *
     * Appends to the buffer "open" string
     *
     * @param open open
     * @return this
     */
    public final P open(String open) {
        options.push(options.peek().derive().open(open));
        return append(open);
    }

    /**
     * Starts new block in list mode.
     * Current opened block is stacked and will be restored on close.
     *
     * Appends to the buffer "open" string
     * Closing block will append "close" string
     *
     * @param open open
     * @param close close
     * @return this
     */
    public final P open(String open, String close) {
        options.push(options.peek().derive().open(open).close(close));
        return append(open);
    }

    /**
     * Starts new block in list mode.
     * Current opened block is stacked and will be restored on close.
     *
     * Appends to the buffer "open" string
     * Closing block will append "close" string
     * Inserted items will be separated by "separator"
     *
     * @param open open
     * @param close close
     * @param separator separator
     * @return this
     */
    public final P open(String open, String close, String separator) {
        options.push(options.peek().derive().open(open).close(close).separator(separator));
        return append(open);
    }

    /**
     * Closes current block.
     * Restores state of previously opened block, if any.
     *
     * Appends to the buffer "close" string
     *
     * @return this
     */
    public final P close() {
        if(options.size() <= 1) {
            throw new IllegalStateException("There is not block to close.");
        }
        return append(options.pop().close());
    }

    /**
     * If you want to insert some values into block, separated by "separator", you can use this method.
     * It should be called BEFORE each appended item.
     *
     * Appends to the buffer "separator" string if it is not first call inside current block.
     *
     * @return this
     */
    public final P item() {
        Options state = options.peek();
        if(0 != state.counter++) {
            append(state.separator());
        }
        return that();
    }

    /**
     * Appends new line separator as configured in options for current block.
     *
     * @return this
     */
    public final P endl() {
        return append(options.peek().endl());
    }

    /**
     * Appends blank value as configured in options for current block.
     *
     * @return this
     */
    public final P append() {
        String value = options.peek().blank();
        return null == value ? that() : append(value);
    }

    @Override
    public abstract P append(CharSequence value);

    @Override
    public abstract P append(CharSequence value, int begin, int end);

    /**
     * Appends chars into buffer
     *
     * @param value value
     * @return this
     */
    public abstract P append(char[] value);

    /**
     * Appends fragment of chars into buffer
     *
     * @param value value
     * @param begin begin
     * @param end end
     * @return this
     */
    public abstract P append(char[] value, int begin, int end);

    @Override
    public abstract P append(char c);

    /**
     * Appends value into buffer.
     * Converts object using "toString" method.
     *
     * @param value value
     * @return this
     */
    public final P append(Object value) {
        return (value == null) ? append() : append(value.toString());        
    }

    /**
     * Appends value into buffer.
     * Converts boolean to "true" or "false" string
     *
     * @param value value
     * @return this
     */
    public final P append(boolean value) {
        return append(value ? "true" : "false");
    }

    /**
     * Appends value into buffer.
     * Converts value using "String.valueOf"
     *
     * @param value value
     * @return this
     */
    public final P append(int value) {
        return append(String.valueOf(value));
    }

    /**
     * Appends value into buffer.
     * Converts value using "String.valueOf"
     *
     * @param value value
     * @return this
     */
    public final P append(long value) {
        return append(String.valueOf(value));
    }

    /**
     * Appends value into buffer.
     * Converts value using "String.valueOf"
     *
     * @param value value
     * @return this
     */
    public final P append(float value) {
        return append(String.valueOf(value));
    }

    /**
     * Appends value into buffer.
     * Converts value using "String.valueOf"
     *
     * @param value value
     * @return this
     */
    public final P append(double value) {
        return append(String.valueOf(value));
    }

    /**
     * Appends values into buffer, separating them using "options.separator".
     * Converts values using "String.valueOf"
     *
     * @param values values
     * @return this
     */
    public final P append(Iterable<?> values) {
		return append(values.iterator(), this::asString);
	}

    /**
     * Appends values into buffer, separating them using "options.separator".
     * Converts values using "function"
     *
     * @param values values
     * @param function function
     * @param <T> T
     * @return this
     */
    public final <T> P append(Iterable<? extends T> values, Function<? super T,String> function) {
		return append(values.iterator(), function);
	}

    /**
     * Appends values into buffer, separating them using "options.separator".
     * Converts values using "String.valueOf"
     *
     * @param values values
     * @return this
     */
    public final P append(Iterator<?> values) {
        return append(values, this::asString);
    }

    /**
     * Appends values into buffer, separating them using "options.separator".
     * Converts values using "function"
     *
     * @param values values
     * @param function function
     * @param <T> T
     * @return this
     */
    public final <T> P append(Iterator<? extends T> values, Function<? super T,String> function) {
        if ( !values.hasNext() ) {
            return that();
        }
        String separator = options.peek().separator();
        for (;;) {
            T item = values.next();
            append(function.apply(item));
            if ( !values.hasNext() ) {
                return that();
            }
            append(separator);
        }
	}

    /**
     * Appends values into buffer, separating them using "options.separator".
     * Converts values using "String.valueOf"
     *
     * @param values values
     * @param <T> T
     * @return this
     */
    public final <T> P append(T[] values) {
        return append(values, this::asString);
    }
    
    private String asString(Object value) {
        return null == value ? null : value.toString();
    }

    /**
     * Appends values into buffer, separating them using "options.separator".
     * Converts values using "function"
     *
     * @param values values
     * @param function function
     * @param <T> T
     * @return this
     */
    public final <T> P append(T[] values, Function<? super T,String> function) {
        if ( 0==values.length ) {
            return that();
        }
        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 that();
	}

    /**
     * Appends values into buffer, separating them using "options.separator".
     * Converts values using "String.valueOf"
     *
     * @param values values
     * @return this
     */
    public final P append(NativeArray values) {
        return append(values.size(), i -> String.valueOf(values.get(i)));
    }

    /**
     * Appends "size" values generated by "function", separating them using "options.separator".
     *
     * @param size size
     * @param function function
     * @return this
     */
    public final P append(int size, IntFunction<String> function) {
        if ( 0==size ) {
            return that();
        }
        String separator = options.peek().separator();
        append(function.apply(0));
        for (int i=1; i<size; i++) {
            append(separator).append(function.apply(i));
        }
		return that();
	}

    /**
     * Appends string generated by {@link Formatter}.
     * It uses default locale.
     *
     * @param format format
     * @param arguments arguments
     * @return this
     */
	public final P printf(String format, Object... arguments) {
        return printf(Locale.getDefault(), format, arguments);
    }

    /**
     * Appends string generated by {@link Formatter}.
     * It uses default locale.
     *
     * @param format format
     * @param arguments arguments
     * @return this
     */
	public final P printf(Locale locale, String format, Object... arguments) {
        formatter.compute().format(locale, format, arguments);
        return that();
    }

    @Override
    public abstract String toString();

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

    protected abstract class AbstractPrintWriter extends Writer {
        
        @Override
        public abstract void close();
        
        @Override
        public abstract void flush();
        
        @Override
        public final void write(int c) {
            AbstractAppender.this.append((char) c);
        }
        
        @Override
        public final void write(char[] buffer) {
            AbstractAppender.this.append(buffer);
        }
        
        @Override
        public final void write(char[] buffer, int offset, int length) {
            AbstractAppender.this.append(buffer, offset, length);
        }
        
        @Override
        public final void write(String str) {
            AbstractAppender.this.append(str);
        }
        
        @Override
        public final void write(String str, int offset, int length) {
            AbstractAppender.this.append(str, offset, length);
        }
        
    }

    /**
     * Class used by StrAppender to store information about internal state and configuration.
     *
     * @author Ranides Atterwim {@literal <ranides@gmail.com>}
     */
    public static class Options implements Serializable {

        private static final long serialVersionUID = 1L;

        private static final byte[] EMPTY = new byte[0];

        private final Options parent;

        private int counter;

        private String blank;

        private String endl;

        private String separator;

        private String open;

        private String close;

        protected Options(Options parent) {
            this.parent = parent;
        }

        public Options derive() {
            return new Options(this);
        }

        public Options copy() {
            Options state = new Options(null);

            state.blank(this.blank());
            state.endl(this.endl());
            state.separator(this.separator());
            state.open(this.open());
            state.close(this.close());

            return state;
        }

        /**
         * String value inserted into buffer if inserted value is "null"
         * It is used by {@link StrBuilder#append()} too.
         *
         * If returned String is still null , then buffer does not change its content.
         *
         * Default value is: null
         *
         * @return String
         */
        public String blank() {
            return blank != null ? blank : parent!=null ? parent.blank() : null;
        }

        /**
         * Changes String value inserted into buffer if inserted value is "null"
         *
         * @param value value
         * @return this
         */
        public Options blank(String value) {
            this.blank = value;
            return this;
        }

        /**
         * String value used by buffer as line separator.
         *
         * Default value is: {@link StringUtils#ENDL}
         *
         * @return String
         */
        public String endl() {
            return endl != null ? endl : parent!=null ? parent.endl() : StringUtils.ENDL;
        }

        /**
         * Changes String value used by buffer as line separator.
         *
         * @param value value
         * @return this
         */
        public Options endl(String value) {
            this.endl = value;
            return this;
        }

        /**
         * String value used as item separator.
         *
         * Default value is: ", "
         *
         * @return String
         */
        public String separator() {
            return separator != null ? separator : parent!=null ? parent.separator() : ", ";
        }

        /**
         * Changes String value used as item separator.
         *
         * @param value value
         * @return this
         */
        public Options separator(String value) {
            this.separator = value;
            return this;
        }

        /**
         * String value inserted before list of items.
         *
         * Default value is: null (which means that "blank" will be used)
         *
         * @return String
         */
        public String open() {
            return open != null ? open : parent!=null ? parent.open() : null;
        }

        /**
         * Changes String value inserted before list of items.
         *
         * @param value value
         * @return this
         */
        public Options open(String value) {
            this.open = value;
            return this;
        }

        /**
         * String value inserted after list of items.
         *
         * Default value is: null (which means that "blank" will be used)
         *
         * @return String
         */
        public String close() {
            return close != null ? close : parent!=null ? parent.close() : null;
        }

        /**
         * Changes String value inserted after list of items.
         *
         * @param value value
         * @return this
         */
        public Options close(String value) {
            this.close = value;
            return this;
        }

        @Override
        public String toString() {
            String b = hex(blank());
            String e = hex(endl());
            String s = hex(separator());
            String o = hex(open());
            String c = hex(close());
            return "{ blank='" + b + "', endl='" + e + "', separator='" + s + "', open='" + o + "', close='" + c + "' }";
        }

        private static String hex(String s) {
            return FormatHex.format(null == s ? EMPTY : s.getBytes(Charsets.UTF8));
        }

    }

}
