/*
 * FindBugs - Find Bugs in Java programs
 * Copyright (C) 2003-2007 University of Maryland
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs.jaif;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Lexical scanner for external annotation files.
 *
 * @author David Hovemeyer
 * @see <a
 *      href="http://groups.csail.mit.edu/pag/jsr308/annotation-file-utilities/">Annotation
 *      File Utilities/</a>
 */
public class JAIFScanner {

    static class TokenPattern {
        private final Pattern pattern;

        private final JAIFTokenKind kind;

        public TokenPattern(String regex, JAIFTokenKind kind) {
            this.pattern = Pattern.compile("^" + regex);
            this.kind = kind;
        }

        public JAIFTokenKind getKind(String lexeme) {
            return kind;
        }

        public Pattern getPattern() {
            return pattern;
        }
    }

    // See http://java.sun.com/docs/books/jls/third_edition/html/lexical.html
    // Hexidecimal floating-point literals are not implemented.
    // Unicode escapes are not implemented (but could be implemented in the
    // fillLineBuf() method).

    private static final String ID_START = "[@A-Za-z_\\$]";

    private static final String ID_REST = "[A-Za-z0-9_\\$]";

    private static final String DIGIT = "[0-9]";

    private static final String DIGITS = DIGIT + "+";

    private static final String DIGITS_OPT = DIGIT + "*";

    private static final String SIGN_OPT = "[+-]?";

    private static final String DOT = "\\.";

    private static final String EXP_PART = "([Ee]" + SIGN_OPT + DIGITS + ")";

    private static final String EXP_PART_OPT = EXP_PART + "?";

    private static final String FLOAT_TYPE_SUFFIX = "[FfDd]";

    private static final String FLOAT_TYPE_SUFFIX_OPT = FLOAT_TYPE_SUFFIX + "?";

    private static final String OCTAL_DIGITS = "[0-7]+";

    private static final String HEX_SIGNIFIER = "0[Xx]";

    private static final String HEX_DIGITS = "[0-9A-Fa-f]+";

    private static final String INT_TYPE_SUFFIX_OPT = "[Ll]?";

    private static final String INPUT_CHAR = "[^\\\\\\\"]";// anything other
    // than backslash or
    // double-quote
    // character

    private static final String OCT_ESCAPE = "([0-7]|[0-3]?[0-7][0-7])";

    private static final String ESCAPE_SEQ = "(\\\\[btnfr\"'\\\\]|\\\\" + OCT_ESCAPE + ")";

    private static final String STRING_CHARS_OPT = "(" + INPUT_CHAR + "|" + ESCAPE_SEQ + ")*";

    private static final TokenPattern[] TOKEN_PATTERNS = {
        // Misc. syntax
        new TokenPattern(":", JAIFTokenKind.COLON),
        new TokenPattern("\\(", JAIFTokenKind.LPAREN),
        new TokenPattern("\\)", JAIFTokenKind.RPAREN),
        new TokenPattern(",", JAIFTokenKind.COMMA),
        new TokenPattern("=", JAIFTokenKind.EQUALS),

        // Identifiers and keywords
        new TokenPattern(ID_START + "(" + ID_REST + ")*", JAIFTokenKind.IDENTIFIER_OR_KEYWORD),

        // FP literals
        new TokenPattern(DIGITS + DOT + DIGITS_OPT + EXP_PART_OPT + FLOAT_TYPE_SUFFIX_OPT,
                JAIFTokenKind.FLOATING_POINT_LITERAL),
                new TokenPattern(DOT + DIGITS + EXP_PART_OPT + FLOAT_TYPE_SUFFIX_OPT, JAIFTokenKind.FLOATING_POINT_LITERAL),
                new TokenPattern(DIGITS + EXP_PART + FLOAT_TYPE_SUFFIX_OPT, JAIFTokenKind.FLOATING_POINT_LITERAL),
                new TokenPattern(DIGITS + EXP_PART_OPT + FLOAT_TYPE_SUFFIX, JAIFTokenKind.FLOATING_POINT_LITERAL),

                // This must come after the FP literal patterns
                new TokenPattern(DOT, JAIFTokenKind.DOT),

                // Integer literals
                new TokenPattern("0" + OCTAL_DIGITS + INT_TYPE_SUFFIX_OPT, JAIFTokenKind.OCTAL_LITERAL),
                new TokenPattern(HEX_SIGNIFIER + HEX_DIGITS + INT_TYPE_SUFFIX_OPT, JAIFTokenKind.HEX_LITERAL),
                new TokenPattern(DIGITS + INT_TYPE_SUFFIX_OPT, JAIFTokenKind.DECIMAL_LITERAL),

                // String literals
                new TokenPattern("\"" + STRING_CHARS_OPT + "\"", JAIFTokenKind.STRING_LITERAL), };

    private final BufferedReader reader;

    private JAIFToken next;

    private String lineBuf;

    private int lineNum;

    /**
     * @param reader
     */
    public JAIFScanner(Reader reader) {
        this.reader = new BufferedReader(reader);
        this.lineNum = 0;
    }

    public int getLineNumber() {
        return lineNum;
    }

    public JAIFToken nextToken() throws IOException, JAIFSyntaxException {
        if (next == null) {
            fetchToken();
        }
        JAIFToken result = next;
        next = null;
        return result;
    }

    public JAIFToken peekToken() throws IOException, JAIFSyntaxException {
        if (next == null) {
            fetchToken();
        }
        return next;
    }

    public boolean atEOF() throws IOException {
        fillLineBuf();
        return lineBuf == null;
    }

    private void fillLineBuf() throws IOException {
        if (lineBuf == null) {
            lineBuf = reader.readLine();
            if (lineBuf != null) {
                ++lineNum;
            }
        }
    }

    private boolean isHorizWhitespace(char c) {
        return c == ' ' || c == '\t';
    }

    private void fetchToken() throws IOException, JAIFSyntaxException {
        assert next == null;

        fillLineBuf();
        if (lineBuf == null) {
            throw new JAIFSyntaxException(this, "Unexpected end of file");
        }

        // Strip leading whitespace, if any
        int wsCount = 0;
        while (wsCount < lineBuf.length() && isHorizWhitespace(lineBuf.charAt(wsCount))) {
            wsCount++;
        }
        if (wsCount > 0) {
            lineBuf = lineBuf.substring(wsCount);
        }
        // System.out.println("Consumed " + wsCount +
        // " characters of horizontal whitespace");

        if ("".equals(lineBuf)) {
            // Reached end of line.
            next = new JAIFToken(JAIFTokenKind.NEWLINE, "\n", lineNum);
            lineBuf = null;
            return;
        }

        // Try matching line buffer against all known patterns
        // until we fine one that matches.
        for (TokenPattern tokenPattern : TOKEN_PATTERNS) {
            Matcher m = tokenPattern.getPattern().matcher(lineBuf);
            if (m.find()) {
                String lexeme = m.group();
                lineBuf = lineBuf.substring(lexeme.length());
                next = new JAIFToken(tokenPattern.getKind(lexeme), lexeme, lineNum);
                return;
            }
        }

        throw new JAIFSyntaxException(this, "Unrecognized token (trying to match text `" + lineBuf + "')");
    }
}
