InputReader.java

package org.sterling.source.scanner;

import static java.lang.Character.isWhitespace;
import static java.util.regex.Pattern.compile;
import static org.sterling.source.Location.at;
import static org.sterling.source.LocationRange.between;
import static org.sterling.source.syntax.Token.token;
import static org.sterling.util.BufferUtil.buffer;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.sterling.source.Location;
import org.sterling.source.exception.InputException;
import org.sterling.source.exception.NoAcceptedInputException;
import org.sterling.source.exception.NoMoreInputException;
import org.sterling.source.syntax.NodeKind;
import org.sterling.source.syntax.Token;

public class InputReader implements AutoCloseable {

    private final String source;
    private final BufferedReader reader;
    private final Deque<InputReference> readBuffer;
    private final Deque<StoredReference> storeBuffer;
    private final StateManager<InputReference> stateManager;
    private final Pattern whitespace;
    private String input;
    private int line;
    private int column;

    public InputReader(String source, InputStream inputStream) {
        this.source = source;
        this.reader = buffer(inputStream);
        this.readBuffer = new ArrayDeque<>();
        this.storeBuffer = new ArrayDeque<>();
        this.stateManager = new StateManager<>(readBuffer);
        this.whitespace = compile("^\\s+");
        this.line = -1;
        this.column = 0;
    }

    public Token accept(NodeKind kind) {
        Deque<StoredReference> acceptedReferences = acceptReferences();
        Location start = acceptedReferences.peek().getLocation();
        Location end = acceptedReferences.peekLast().getLocation();
        StringBuilder builder = new StringBuilder();
        while (!acceptedReferences.isEmpty()) {
            builder.append(acceptedReferences.pop().getValue());
        }
        return token(kind, builder.toString(), between(start, at(
            end.getSource(),
            end.getLine(),
            end.getColumn() + 1
        )));
    }

    public void begin() {
        stateManager.begin();
    }

    @Override
    public void close() {
        try {
            stateManager.close();
            reader.close();
        } catch (IOException exception) {
            // intentionally empty
        }
    }

    public void end() {
        stateManager.end();
    }

    public boolean expect(char value) {
        return peek() == value;
    }

    public boolean expect(Pattern... patterns) {
        if (hasMore()) {
            for (Pattern pattern : patterns) {
                if (match(pattern).find()) {
                    return true;
                }
            }
        }
        return false;
    }

    public Location getLocation() {
        hasMore();
        InputReference reference = peekReference();
        if (reference == null) {
            return at(source, line, column);
        } else {
            return reference.getLocation();
        }
    }

    public char peek() {
        if (hasMore()) {
            return peekReference().getValue();
        } else {
            return '\0';
        }
    }

    public void reject() {
        while (!storeBuffer.isEmpty()) {
            backward();
        }
    }

    public void rollback() {
        stateManager.restore();
    }

    public void skip() {
        forward(true);
    }

    public void skip(Pattern pattern) {
        Matcher matcher = match(pattern);
        if (matcher.find()) {
            int length = matcher.group(0).length();
            for (int i = 0; i < length; i++) {
                skip();
            }
        }
    }

    public void skipWhitespace() {
        while (isWhitespace(peek())) {
            skip(whitespace);
        }
    }

    public void store() {
        forward();
    }

    public void store(int thisMany) {
        for (int i = 0; i < thisMany; i++) {
            store();
        }
    }

    public boolean store(Pattern... patterns) {
        for (Pattern pattern : patterns) {
            Matcher matcher = match(pattern);
            if (matcher.find()) {
                store(matcher.group(0).length());
                return true;
            }
        }
        return false;
    }

    private Deque<StoredReference> acceptReferences() {
        Deque<StoredReference> acceptedInput = new ArrayDeque<>();
        for (StoredReference reference : storeBuffer) {
            if (reference.isKeep()) {
                acceptedInput.push(reference);
            }
        }
        if (acceptedInput.isEmpty()) {
            throw new NoAcceptedInputException();
        }
        storeBuffer.clear();
        return acceptedInput;
    }

    private void backward() {
        readBuffer.push(storeBuffer.pop().getReference());
    }

    private void forward() {
        forward(false);
    }

    private void forward(boolean skip) {
        if (hasMore()) {
            InputReference next = readBuffer.pop();
            stateManager.push(next);
            storeBuffer.push(new StoredReference(next, skip));
        } else {
            throw new NoMoreInputException("No more input [" + getLocation() + "]");
        }
    }

    private String getCurrentLine() {
        StringBuilder builder = new StringBuilder();
        int offset = getLocation().getColumn();
        for (InputReference reference : readBuffer) {
            builder.append(reference.getValue());
            offset = reference.getLocation().getColumn();
        }
        if (input != null) {
            builder.append(input.substring(offset + 1));
        }
        return builder.toString();
    }

    private String getLine() {
        if (hasMore()) {
            if (getLocation().getLine() == line) {
                return getCurrentLine();
            } else {
                return getPreviousLine();
            }
        } else {
            return "\0";
        }
    }

    private String getPreviousLine() {
        StringBuilder builder = new StringBuilder();
        for (InputReference reference : readBuffer) {
            if (reference.getLocation().getLine() != line) {
                builder.append(reference.getValue());
            }
        }
        return builder.toString();
    }

    private boolean hasLine() {
        if (input == null || column >= input.length()) {
            try {
                input = reader.readLine();
            } catch (IOException exception) {
                throw new InputException(exception);
            }
            if (input != null) {
                input = input + "\n";
                column = 0;
                line++;
            }
        }
        return input != null;
    }

    private boolean hasMore() {
        if (readBuffer.isEmpty()) {
            if (hasLine()) {
                readBuffer.push(new InputReference(at(source, line, column), input.charAt(column++)));
            } else {
                return false;
            }
        }
        return true;
    }

    private Matcher match(Pattern pattern) {
        return pattern.matcher(getLine());
    }

    private InputReference peekReference() {
        return readBuffer.peek();
    }

    private static final class InputReference {

        private final Location location;
        private final char value;

        public InputReference(Location location, char value) {
            this.location = location;
            this.value = value;
        }

        public Location getLocation() {
            return location;
        }

        public char getValue() {
            return value;
        }
    }

    private static final class StoredReference {

        private final InputReference reference;
        private final boolean discard;

        public StoredReference(InputReference reference, boolean discard) {
            this.reference = reference;
            this.discard = discard;
        }

        public Location getLocation() {
            return reference.getLocation();
        }

        public InputReference getReference() {
            return reference;
        }

        public char getValue() {
            return reference.getValue();
        }

        public boolean isKeep() {
            return !discard;
        }
    }
}