import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

/** Lexer (aka scanner) for the simple calculator language using regular expressions.
 */
public class RegexCalcLexer extends CalcLexer {
    /** Pairing of token type to regular expression. */
    private static Map<CalcToken.Type, Pattern> specs = Map.of(
        CalcToken.Type.OPA,  Pattern.compile("[+-]"),
        CalcToken.Type.OPM,  Pattern.compile("[*/]"),
        CalcToken.Type.NUM,  Pattern.compile("-?[0-9]+"),
        CalcToken.Type.LP,   Pattern.compile("\\("),
        CalcToken.Type.RP,   Pattern.compile("\\)"),
        CalcToken.Type.STOP, Pattern.compile(";")
    );

    private static Pattern whitespace = Pattern.compile("\\s*");

    /** The source of input lines. */
    private BufferedReader source;
    /** Stores progress in matching tokens from the most recent line of input. */
    private Matcher curLine = null;

    public RegexCalcLexer() throws IOException {
        this.source = new BufferedReader(new InputStreamReader(System.in));
    }

    /** Reads, moves past, and returns a single token from the input stream.
     * This works by first trying to get one more token out of the current line,
     * and reading in more lines as necessary to get a new token.
     * Note that it does NOT throw an error on EOF, but rather just return
     * the EOF token in that case.
     */
    @Override
    protected CalcToken readToken() throws IOException {
        while (true) {
            if (curLine == null) {
                // no current line, so read a new one
                String line = source.readLine();
                // check for end of input
                if (line == null)
                    return new CalcToken(CalcToken.Type.EOF, "");
                curLine = whitespace.matcher(line);
            }
            // skip past any whitespace before the next token
            if (curLine.usePattern(whitespace).lookingAt())
                curLine.region(curLine.end(), curLine.regionEnd());
            // stop this look when there is something left on the line besides whitespace
            if (curLine.regionStart() < curLine.regionEnd()) break;
            else curLine = null;
        }
        // use maximal munch to select the longest matching token
        CalcToken candidate = null;
        for (Map.Entry<CalcToken.Type, Pattern> spec : specs.entrySet()) {
            if (curLine.usePattern(spec.getValue()).lookingAt()) {
                // this token could be a match - check if it's the longest
                String text = curLine.group();
                if (candidate == null || text.length() > candidate.getText().length())
                    candidate = new CalcToken(spec.getKey(), text);
            }
        }
        if (candidate == null)
            throw new IOException("Unrecognized token starting on column %d"
                .formatted(curLine.regionStart()));
        // skip past the matched token on the current line
        curLine.region(curLine.regionStart() + candidate.getText().length(), curLine.regionEnd());
        return candidate;
    }
}