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

package net.ranides.assira.lexer;

import java.io.Serializable;
import java.util.Arrays;

/**
 * <p>
 * Strumień przetwarzający kolejno elementy zbioru przydatny przy pisaniu
 * lexerów <i>ad hoc</i>. Żadna funkcjonalność związana z obsługą gramatyki
 * nie istnieje. Przydatny do przetwarzania sekwencji sterujących, kodów
 * escape'ujących w stringach, wzorców podstawieniowych etc
 * </p><p>
 * Bardziej ściśle: do przetwarzania formatów, w których bezkontekstowe
 * symbole nie są zagnieżdżane rekurencyjnie.
 * </p>
 * @param <UserArgument>
 * @author ranides
 */
public class ByteStreamer<UserArgument> implements Serializable {

    private static final long serialVersionUID = 1L;

    private static final byte[] EMPTY = new byte[0];
    private final ByteConsumer<UserArgument> consumer;
    private byte[] tail;
    private byte[] array;
    private int recent;
    private int position;


    /**
     * Tworzy nowy strumień, który przekazuje wyodrębnione fragmenty do podanego
     * konsumenta.
     * @param consumer
     */
    public ByteStreamer(ByteConsumer<UserArgument> consumer) {
        this.tail      = null;
        this.array     = EMPTY;
        this.recent    = 0;
        this.position  = 0;
        this.consumer = consumer;
    }

    /**
     * Odczytuje element ze strumienia i przesuwa kursor do przodu.
     * @return
     */
    public byte get() {
        return array[position++];
    }

    /**
     * Odczytuje element ze strumienia nie przesuwając kursora
     * @return
     */
    public byte peek() {
        return array[position];
    }

    /**
     * Pomija element
     */
    public void skip() {
        position++;
    }

    /**
     * Sprawdza, czy przetworzono wszystkie dostępne dane.
     * @return
     */
    public boolean end() {
        return position >= array.length;
    }

    /**
     * Wysyła do odbiorcy fragment o wskazanej treści
     * @param extra
     * @param data
     */
    public void send(UserArgument extra, byte... data) {
        consumer.consume(extra, data);
    }

    /**
     * Odrzuca bieżący fragment i rozpoczyna przetwarzanie nowego od aktualnej
     * pozycji kursora.
     */
    public void flush() {
        recent = position;
    }

    /**
     * Wysyła bieżący fragment do odbiorcy, ignorując {@code left} pierwszych
     * znaków oraz {@code right} ostatnich. Następnie rozpoczyna przetwarzanie
     * nowego fragmentu rozpoczynającego się od aktualnej pozycji kursora.
     * @param extra
     * @param lclip
     * @param rclip
     */
    public void flush(UserArgument extra, int lclip, int rclip) {
        if(recent < position && position <= array.length) {
            send(extra, Arrays.copyOfRange(array, recent+lclip, position-rclip) );
        }
        flush();
    }

    /**
     * Wysyła bieżący fragment do odbiorcy. Następnie rozpoczyna przetwarzanie
     * nowego fragmentu od aktualnej pozycji kursora.
     * @param extra
     */
    public void flush(UserArgument extra) {
        flush(extra, 0, 0);
    }

    /**
     * Rozpoczyna przetwarzanie kolejnej porcji danych
     * @param input
     * @throws IllegalStateException jeśli strumień jest w trakcie przetwarzania
     * danych.
     */
    public void open(byte[] input) {
        if( null != tail ) {
            array = new byte[tail.length + input.length];
            System.arraycopy(tail, 0, array, 0, tail.length);
            System.arraycopy(input, 0, array, tail.length, input.length);
            tail = null;
        } else {
            array = Arrays.copyOf(input, input.length);
        }
        position = 0;
        recent = 0;
    }

    /**
     * Kończy przetwarzanie danych
     */
    public void close() {
        if(tail != null) {
            if( array!=EMPTY ) {
                throw new IllegalStateException("tail already in use");
            }
        } else {
            tail = Arrays.copyOfRange(array, recent, array.length);
            recent = 0;
            position = 0;
            array = EMPTY;
        }
    }

    /**
     * Sprawdza,czy przetwarzanie wykorzystało wszystkie przekazane dane
     * @return
     */
    public boolean pending() {
        return tail != null;
    }

}
