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

import java.io.Closeable;
import java.util.Arrays;
import java.util.function.BiConsumer;

/**
 * This is Streamer class designed with "char" arrays.
 *
 * Working with streamer required few, repeatable steps:
 * REPEAT:
 *   - put some data into streamer
 *   - REPEAT
 *       - move forward using get/forward
 *       - mark position
 *       - move forward using get/forward
 *       - send data between mark and current position to "consumer"
 *
 * The whole idea is, that in one place you implement chopping logic,
 * and you send extracted fragments into consumer, which handles parsing logic
 *
 * Every time when you send data to consumer, you can send additionaly user defined data
 * which could be used to interpret extracted fragment. User defined data could be as simple
 * as "enum" marking data type or as complex as whole parsing context with already detected
 * symbols, scopes etc
 *
 * Streamers are helpfull in writing ad hoc top down parsers.
 *
 * Because it does not have any support for formal grammar processing, it is very lightweight,
 * in the same time quite primitive. Could be used for parsing rather simple formats, for example:
 * ECMA escape sequences, String escape codes, replacement patterns, etc
 *
 * @param <T>
 * @author ranides
 */
public class CharStreamer<T> implements Closeable {

    private static final char[] EMPTY = new char[0];
    private final BiConsumer<T, char[]> consumer;
    private char[] array;
    private int length;
    private int recent;
    private int position;

    /**
     * Creates new empty stream which sends extracted fragments to provided consumer.
     * After construction you must insert data using {@link #put}.
     *
     * @param consumer consumer
     */
    public CharStreamer(BiConsumer<T, char[]> consumer) {
        this.array     = EMPTY;
        this.length    = 0;
        this.recent    = 0;
        this.position  = 0;
        this.consumer  = consumer;
    }

    /**
     * Closes streamer.
     *
     * You should not close stream before reaching end.
     * You should call "discard" before close, if you really want to ignore pending fragment.
     *
     * It throws IllegalStateException if streams still contains some avaiable or pending data.
     */
    @Override
    public void close() {
        if( pending() > 0 || available() > 0 ) {
            throw new IllegalStateException("Streamer not processed all content");
        }
    }

    /**
     * Inserts data into streamer.
     * Please note, that streamer removes already processed data to save heap space.
     *
     * @param input input
     * @return this
     */
    public CharStreamer<T> put(char[] input) {
        int pending = length - recent;

        length = pending + input.length;

        char[] appended;
        if(length <= array.length) {
            appended = array;
        } else {
            appended = new char[length];
        }

        System.arraycopy(array, recent, appended, 0, pending);
        array = appended;
        System.arraycopy(input, 0, array, pending, input.length);

        position = position - recent;
        recent = 0;

        return this;
    }

    /**
     * Returns char at current position.
     * It does not change position.
     *
     * @return char
     * @throws IllegalStateException if streamer alredy reached end.
     */
    public char peek() {
        check();
        return array[position];
    }

    /**
     * Returns char at current position.
     * It moves position forward.
     *
     * @return char
     * @throws IllegalStateException if streamer alredy reached end.
     */
    public char get() {
        check();
        return array[position++];
    }

    /**
     * Moves position forward.
     *
     * @throws IllegalStateException if streamer alredy reached end.
     */
    public void forward() {
        check();
        position++;
    }

    /**
     * Moves position forward.
     *
     * @param offset offset
     * @throws IllegalStateException if streamer alredy reached end.
     */
    public void forward(int offset) {
        check(offset);
        position += offset;
    }

    /**
     * Marks current position.
     * Mark is used when sending data to consumer.
     */
    public void mark() {
        recent = position;
    }

    /**
     * Discards pending data, that means: data between mark and current position.
     */
    public void discard() {
        mark();
    }

    /**
     * Checks if position in streamer reached end of data
     *
     * @return boolean
     */
    public boolean end() {
        return position >= length;
    }

    /**
     * Returns number of chars available to read before reaching the end.
     *
     * @return int
     */
    public int available() {
        return Math.max(0, length - position);
    }

    /**
     * Returns number of chars between mark and current position.
     * This pending fragment could be sent to consumer.
     *
     * @return int
     */
    public int pending() {
        return Math.max(0, position - recent);
    }

    /**
     * Sends to consumer provided chars, completely ignoring pending data.
     *
     * Please note: it does not change streamer state: both position and mark are not affected.
     *
     * @param extra extra
     * @param data data
     */
    public void send(T extra, char[] data) {
        consumer.accept(extra, data);
    }

    /**
     * Sends to consumer pending data, but without first "left" bytes and last "right" chars.
     * Mark is updated to current position.
     *
     * @param extra extra
     * @param left left
     * @param right right
     */
    public void send(T extra, int left, int right) {
        if(recent < position && position <= length) {
            send(extra, Arrays.copyOfRange(array, recent+left, position-right) );
        }
        mark();
    }

    /**
     * Sends to consumer pending data.
     * Mark is updated to current position.
     *
     * @param extra extra
     */
    public void send(T extra) {
        send(extra, 0, 0);
    }

    private void check() {
        if(position >= length) {
            throw new IllegalStateException("Steamer: EOS");
        }
    }

    private void check(int offset) {
        if(position+offset > length) {
            position = length;
            throw new IllegalStateException("EOS");
        }
    }

}
