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

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

/**
 * <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 CharStreamer<UserArgument> {

    private static final char[] EMPTY = new char[0];
    private final BiConsumer<UserArgument, char[]> consumer;
    private char[] tail;
    private char[] array;
    private int recent;
    private int position;
    
    
    /**
     * Tworzy nowy strumień, który przekazuje wyodrębnione fragmenty do podanego
     * konsumenta.
     * @param consumer
     */
    public CharStreamer(BiConsumer<UserArgument, char[]> 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 char get() {
        return array[position++];
    }
    
    /**
     * Odczytuje element ze strumienia nie przesuwając kursora
     * @return 
     */
    public char peek() {
        return array[position];
    }
    
    /**
     * Pomija element
     */
    public void skip() {
        position++;
    }
    
    public void skip(int offset) {
        position += offset;
    }
    
    /**
     * 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, char... data) {
        consumer.accept(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 left
     * @param right 
     */
    public void flush(UserArgument extra, int left, int right) {
        if(recent < position && position <= array.length) {
            send(extra, Arrays.copyOfRange(array, recent+left, position-right) );
        } 
        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(char[] input) {
        if(array != EMPTY) {
            throw new IllegalStateException("already opened");
        }
        if( null != tail ) {
            array = new char[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;
    }

}
