/*
 * Copyright (C) 2010 Teleal GmbH, Switzerland
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.teleal.lemma.reader.javacode;

import japa.parser.JavaParser;
import japa.parser.ParseException;
import japa.parser.ast.CompilationUnit;
import japa.parser.ast.body.MethodDeclaration;
import japa.parser.ast.body.Parameter;
import japa.parser.ast.body.ClassOrInterfaceDeclaration;
import japa.parser.ast.body.BodyDeclaration;
import japa.parser.ast.visitor.VoidVisitorAdapter;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Iterator;
import java.util.logging.Logger;

import org.teleal.lemma.reader.content.LineRange;

/**
 * Finds the "line range" of declarations in a Java source file.
 * <p>
 * We need this to demarcate raw source code for citation, where a method/nested class begins and where it ends.
 * The challenge is matching the Javadoc metadata of a method to this parser's metadata of a method.
 * Javadoc has knowledge of the type system (e.g. qualified vs. unqualified parameters of a method
 * are detectable) while this parser only has the declared string as-is from the source file and there is
 * no way we can qualify/unqualify a method parameter type. So calculating a method signature here
 * might have a different result than calculating a method signature in the Javadoc reader.
 * </p>
 * <p>
 * In other words, don't be surprised when the source lines of a method can't be found if you use
 * qualified type names on method parameters in your source.
 * </p>
 * <p/>
 * TODO: Revisit this and see if it helps: http://code.google.com/p/javaparser/issues/detail?id=9
 *
 * @author Christian Bauer
 */
public class LineRangeParser {

    private Logger log = Logger.getLogger(LineRangeParser.class.getName());

    protected final Map<String, LineRange> methodsLineRange = new HashMap();
    protected final Map<String, LineRange> typesLineRange = new HashMap();

    public LineRangeParser(final File file) throws IOException, ParseException {
        log.fine("Parsing Java source of file: " + file);

        CompilationUnit cu = JavaParser.parse(file);

        new VoidVisitorAdapter() {

            @Override
            public void visit(ClassOrInterfaceDeclaration typeDeclaration, Object o) {

                String signature = typeDeclaration.getName();
                LineRange range = new LineRange(typeDeclaration.getBeginLine(), typeDeclaration.getEndLine());

                log.fine("Parsed source of type '" + signature + "', lines: " + range);
                LineRangeParser.this.typesLineRange.put(signature, range);

                for (BodyDeclaration bodyDeclaration : typeDeclaration.getMembers()) {
                    bodyDeclaration.accept(this, null);
                }
            }

            @Override
            public void visit(MethodDeclaration methodDeclaration, Object arg) {

                String signature = getSignature(methodDeclaration);
                LineRange range = new LineRange(methodDeclaration.getBeginLine(), methodDeclaration.getEndLine());

                log.fine("Parsed source of method '" + signature + "', lines: " + range);

                LineRangeParser.this.methodsLineRange.put(signature, range);
            }
        }.visit(cu, null);
    }

    public Map<String, LineRange> getMethodsLineRange() {
        return methodsLineRange;
    }

    public Map<String, LineRange> getTypesLineRange() {
        return typesLineRange;
    }

    protected String getSignature(MethodDeclaration methodDeclaration) {
        final StringBuilder signature = new StringBuilder();
        signature.append(methodDeclaration.getName());
        signature.append("(");
        if (methodDeclaration.getParameters() != null) {
            for (Iterator<Parameter> i = methodDeclaration.getParameters().iterator(); i.hasNext();) {
                Parameter p = i.next();
                signature.append(p.getType().toString());
                if (i.hasNext()) {
                    signature.append(",");
                }
            }
        }
        signature.append(")");
        return signature.toString();
    }
}
