/*
 * 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.IOException;
import java.io.Reader;
import java.util.Locale;

import edu.umd.cs.findbugs.charsets.UTF8;

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

    private JAIFEvents callback;

    public JAIFParser(Reader reader, JAIFEvents callback) {
        this.scanner = new JAIFScanner(reader);
        this.callback = callback;
    }

    public void parse() throws IOException, JAIFSyntaxException {
        parseAnnotationFile();
    }

    int getLineNumber() {
        return scanner.getLineNumber();
    }

    private JAIFToken expect(String s) throws IOException, JAIFSyntaxException {
        JAIFToken t = scanner.nextToken();
        if (!t.lexeme.equals(s)) {
            throw new JAIFSyntaxException(this, "Unexpected token " + t + " (was expecting " + s + ")");
        }
        return t;
    }

    private JAIFToken expect(JAIFTokenKind kind) throws IOException, JAIFSyntaxException {
        JAIFToken t = scanner.nextToken();
        if (t.kind != kind) {
            throw new JAIFSyntaxException(this, "Unexpected token " + t + " (was expecting a `" + kind.toString() + "' token)");
        }
        return t;
    }

    private void expectEndOfLine() throws IOException, JAIFSyntaxException {
        // extract-annotations seems to sometimes produce multiple newlines
        // where
        // the grammar indicates that only one will appear.
        // So, we treat any sequence of one or more newlines as one newline.
        int nlCount = 0;
        JAIFToken t;

        while (true) {
            if (scanner.atEOF()) {
                t = null;
                break;
            }

            t = scanner.peekToken();
            if (t.kind != JAIFTokenKind.NEWLINE) {
                break;
            }

            ++nlCount;
            scanner.nextToken();
        }

        if (nlCount < 1) {
            String msg = (t == null) ? "Unexpected end of file" : "Unexpected token " + t + " (was expecting <newline>)";
            throw new JAIFSyntaxException(this, msg);
        }
    }

    private String readCompoundName() throws IOException, JAIFSyntaxException {
        StringBuilder buf = new StringBuilder();

        boolean firstToken = true;

        while (true) {
            JAIFToken t = scanner.nextToken();
            assert t.kind == JAIFTokenKind.IDENTIFIER_OR_KEYWORD;

            if (firstToken) {
                firstToken = false;
            } else if (t.lexeme.startsWith("@")) {
                throw new JAIFSyntaxException(this, "Illegal compound name (unexpected '@' character)");
            }

            buf.append(t.lexeme);

            t = scanner.peekToken();
            if (t.kind != JAIFTokenKind.DOT) {
                break;
            } else {
                buf.append(t.lexeme);
                scanner.nextToken();
            }
        }

        return buf.toString();
    }

    private String readType() throws IOException, JAIFSyntaxException {
        StringBuilder buf = new StringBuilder();

        JAIFToken t = expect(JAIFTokenKind.IDENTIFIER_OR_KEYWORD);

        if (t.lexeme.equals("enum")) {

        }

        return buf.toString();
    }

    private void parseAnnotationFile() throws IOException, JAIFSyntaxException {
        parsePackageDefinition();
        while (!scanner.atEOF()) {
            parsePackageDefinition();
        }
    }

    private void parsePackageDefinition() throws IOException, JAIFSyntaxException {
        expect("package");
        JAIFToken t = scanner.peekToken();

        String pkgName;

        if (t.kind != JAIFTokenKind.NEWLINE) {
            // Optional package name and package-level annotations

            // Hmmm....the spec says just a plain identifier here.
            // However, I'm pretty sure we want a compound name.
            pkgName = readCompoundName();

            expect(":");

            t = scanner.peekToken();
            while (t.isStartOfAnnotationName()) {
                parseAnnotation();
            }
        } else {
            pkgName = ""; // default package
        }

        expectEndOfLine();

        callback.startPackageDefinition(pkgName);

        while (!scanner.atEOF()) {
            t = scanner.peekToken();
            if (t.lexeme.equals("package")) {
                break;
            }

            parseAnnotationDefinitionOrClassDefinition();
        }

        callback.endPackageDefinition(pkgName);
    }

    private void parseAnnotation() throws IOException, JAIFSyntaxException {
        String annotationName = readCompoundName();
        assert annotationName.startsWith("@");

        callback.startAnnotation(annotationName);

        JAIFToken t = scanner.peekToken();
        if (t.kind == JAIFTokenKind.LPAREN) {
            parseAnnotationField();
            t = scanner.peekToken();
            while (t.kind != JAIFTokenKind.RPAREN) {
                expect(",");
                parseAnnotationField();
                t = scanner.peekToken();
            }
            assert t.kind == JAIFTokenKind.RPAREN;
            scanner.nextToken();
        }

        callback.endAnnotation(annotationName);
    }

    private void parseAnnotationField() throws IOException, JAIFSyntaxException {
        JAIFToken id = expect(JAIFTokenKind.IDENTIFIER_OR_KEYWORD);
        expect("=");
        Object constant = parseConstant();

        callback.annotationField(id.lexeme, constant);
    }

    private Object parseConstant() throws IOException, JAIFSyntaxException {
        JAIFToken t = scanner.peekToken();

        switch (t.kind) {
        case IDENTIFIER_OR_KEYWORD:
            // This is an enum constant specified by a compound name.
            // Represent it as a JAIFEnumConstant object.
            String name = readCompoundName();
            return new JAIFEnumConstant(name);
        case DECIMAL_LITERAL:
            t = scanner.nextToken();
            return Integer.parseInt(t.lexeme);
        case OCTAL_LITERAL:
            t = scanner.nextToken();
            return Integer.parseInt(t.lexeme, 8);
        case HEX_LITERAL:
            t = scanner.nextToken();
            return Integer.parseInt(t.lexeme, 16);
        case FLOATING_POINT_LITERAL:
            t = scanner.nextToken();
            boolean isFloat = t.lexeme.toLowerCase(Locale.ENGLISH).endsWith("f");
            if (isFloat) {
                return Float.parseFloat(t.lexeme);
            } else {
                return Double.parseDouble(t.lexeme);
            }
        case STRING_LITERAL:
            t = scanner.nextToken();
            return unparseStringLiteral(t.lexeme);
        default:
            throw new JAIFSyntaxException(this, "Illegal constant");
        }
    }

    private Object unparseStringLiteral(String lexeme) {
        StringBuilder buf = new StringBuilder();

        int where = 1; // skip initial double quote char

        while (true) {
            assert where < lexeme.length();
            char c = lexeme.charAt(where);

            if (c == '"') {
                break;
            }

            if (c != '\\') {
                buf.append(c);
                where++;
                continue;
            }

            where++;
            assert where < lexeme.length();

            c = lexeme.charAt(where);
            switch (c) {
            case 'b':
                buf.append('\b');
                where++;
                break;
            case 't':
                buf.append('\t');
                where++;
                break;
            case 'n':
                buf.append('\n');
                where++;
                break;
            case 'f':
                buf.append('\t');
                where++;
                break;
            case 'r':
                buf.append('\r');
                where++;
                break;
            case '"':
                buf.append('"');
                where++;
                break;
            case '\'':
                buf.append('\'');
                where++;
                break;
            case '\\':
                buf.append('\\');
                where++;
                break;
            default:
                char value = (char) 0;
                while (c >= '0' && c <= '7') {
                    value *= 8;
                    value += (c - '0');
                    where++;
                    assert where < lexeme.length();
                    c = lexeme.charAt(where);
                }
                buf.append(value);
            }
        }

        return buf.toString();
    }

    private void parseAnnotationDefinitionOrClassDefinition() throws IOException, JAIFSyntaxException {
        JAIFToken t = scanner.peekToken();

        if (t.lexeme.equals("annotation")) {
            parseAnnotationDefinition();
        } else if (t.lexeme.equals("class")) {
            parseClassDefinition();
        } else {
            throw new JAIFSyntaxException(this, "Unexpected token " + t + " (expected `annotation' or `class')");
        }
    }

    private void parseAnnotationDefinition() throws IOException, JAIFSyntaxException {
        expect("annotation");

        String retention = null;

        JAIFToken t = scanner.peekToken();
        if (t.lexeme.equals("visible") || t.lexeme.equals("invisible") || t.lexeme.equals("source")) {
            retention = t.lexeme;
            scanner.nextToken();
        }

        String annotationName = expect(JAIFTokenKind.IDENTIFIER_OR_KEYWORD).lexeme;

        expect(JAIFTokenKind.COLON);
        expectEndOfLine();

        callback.startAnnotationDefinition(annotationName, retention);

        t = scanner.peekToken();
        while (t.kind != JAIFTokenKind.NEWLINE) {
            parseAnnotationFieldDefinition();
        }
    }

    private void parseAnnotationFieldDefinition() throws IOException, JAIFSyntaxException {
        String type = readType();
        String fieldName = expect(JAIFTokenKind.IDENTIFIER_OR_KEYWORD).lexeme;

        callback.annotationFieldDefinition(type, fieldName);
    }

    private void parseClassDefinition() {

    }

    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println("Usage: " + JAIFParser.class.getName() + " <jaif file>");
            System.exit(1);
        }

        JAIFEvents callback = new JAIFEvents() {
            public void annotationField(String fieldName, Object constant) {
                System.out.println("    " + fieldName + "=" + constant);
            }

            public void endAnnotation(String annotationName) {
            }

            public void endPackageDefinition(String pkgName) {
            }

            public void startAnnotation(String annotationName) {
                System.out.println("  annotation " + annotationName);
            }

            public void startPackageDefinition(String pkgName) {
                System.out.println("package " + pkgName);
            }

            public void startAnnotationDefinition(String annotationName, String retention) {
                System.out.println("  annotation " + annotationName + " " + retention);
            }

            public void endAnnotationDefinition(String annotationName) {
            }

            public void annotationFieldDefinition(String type, String fieldName) {
                System.out.println("    " + type + " " + fieldName);
            }
        };

        JAIFParser parser = new JAIFParser(UTF8.fileReader(args[0]), callback);
        parser.parse();
    }
}
