package net.ranides.assira.lexer;

import net.ranides.assira.functional.special.CharPredicate;
import net.ranides.assira.text.StringUtils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * StringTokenizer class is essentially just a stream,
 * but with access methods designed for easier fragment extraction.
 *
 * You can use predicates and regular expressions to describe fragment boundaries.
 */
public class StringTokenizer {

    private final char[] content;

    private int position;

    /**
     * Creates new tokenizer for provided string
     *
     * @param content content
     */
    public StringTokenizer(String content) {
        this.content = content.toCharArray();
        this.position = 0;
    }

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

    /**
     * Returns fragment from current position matching predicate.
     * It does not change position.
     *
     * @param pred pred
     * @return String
     * @throws IllegalStateException if streamer alredy reached end.
     */
    public String peek(CharPredicate pred) {
        check();
        int begin = position;
        int current = position;
        while (content.length != current && pred.test(content[current])) {
            current++;
        }
        return new String(content, begin, current - begin);
    }

    /**
     * Returns fragment from current position with length "n".
     * It does not change position.
     *
     * @param n n
     * @return String
     * @throws IllegalStateException if streamer does not have enough available data.
     */
    public String peek(int n) {
        check(n);
        return new String(content, position, n);
    }

    /**
     * Returns fragment from current position matching regex.
     * It does not change position.
     *
     * @param regex regex
     * @return String
     * @throws IllegalStateException if streamer alredy reached end.
     */
    public String peek(Pattern regex) {
        check();
        Matcher matcher = matcher(regex);
        if(!matcher.find()) {
            return "";
        } else {
            return matcher.group();
        }
    }

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

    /**
     * Returns fragment from current position matching predicate.
     * It moves position forward.
     *
     * @param pred pred
     * @return String
     * @throws IllegalStateException if streamer alredy reached end.
     */
    public String get(CharPredicate pred) {
        int begin = position;
        skip(pred);
        return new String(content, begin, position - begin);
    }

    /**
     * Returns fragment from current position with length "n".
     * It moves position forward.
     *
     * @param n n
     * @return String
     * @throws IllegalStateException if streamer does not have enough available data.
     */
    public String get(int n) {
        check(n);
        position += n;
        return new String(content, position-n, n);
    }

    /**
     * Returns fragment from current position matching regex.
     * It moves position forward.
     *
     * @param regex regex
     * @return String
     * @throws IllegalStateException if streamer alredy reached end.
     */
    public String get(Pattern regex) {
        int begin = position;
        skip(regex);
        return new String(content, begin, position - begin);
    }

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

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

    /**
     * Moves position forward as long predicate matches content.
     *
     * @param pred pred
     * @return number of skipped chars
     */
    public int skip(CharPredicate pred) {
        int c = position;
        while (!end() && pred.test(peek())) {
            position++;
        }
        return position - c;
    }

    /**
     * Moves position forward as long regex matches content.
     *
     * @param regex regex
     * @return number of skipped chars
     */
    public int skip(Pattern regex) {
        Matcher matcher = matcher(regex);
        if(!matcher.find()) {
            return 0;
        } else {
            position += matcher.end();
            return matcher.end();
        }
    }

    /**
     * Checks if there is "value" fragment at current position.
     * If fragment is as expected, it moves forward and returns true.
     *
     * @param value value
     * @return boolean
     */
    public boolean expect(String value) {
        int n = value.length();
        if(position+ n > content.length) {
            return false;
        }
        if(value.equals(peek(n))) {
            skip(n);
            return true;
        }
        return false;
    }

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

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

    /**
     * Returns String which is not tokenized yet.
     * It does not change position.
     *
     * @return String
     */
    public String tail() {
        return new String(content, position, content.length - position);
    }

    private Matcher matcher(Pattern regex) {
        return regex.matcher(StringUtils.wrap(content, position, content.length));
    }

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

    private void check(int n) {
        if(position+n > content.length) {
            throw new IllegalStateException("Steamer: EOS (remained " + (content.length - position) + " instead of " + n +")");
        }
    }

}