/**
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
 */
package net.sourceforge.pmd.lang.java.rule.comments;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import net.sourceforge.pmd.lang.java.ast.ASTAnnotation;
import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceBodyDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
import net.sourceforge.pmd.lang.java.ast.ASTFieldDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclarator;
import net.sourceforge.pmd.lang.java.ast.ASTName;
import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId;
import net.sourceforge.pmd.lang.java.ast.AbstractJavaAccessNode;
import net.sourceforge.pmd.lang.java.ast.Comment;
import net.sourceforge.pmd.lang.rule.properties.StringProperty;

/**
 * Check for Methods, Fields and Nested Classes that have a default access modifier
 *
 * @author Damián Techeira
 */
public class CommentDefaultAccessModifierRule extends AbstractCommentRule {

	private static final StringProperty REGEX_DESCRIPTOR = new StringProperty("regex", "Regular expression", "", 1.0f);
	private static final String MESSAGE = "To avoid mistakes add a comment " +
			"at the beginning of the %s %s if you want a default access modifier";
	private final Set<Integer> interestingLineNumberComments = new HashSet<Integer>();

	public CommentDefaultAccessModifierRule() {
		definePropertyDescriptor(REGEX_DESCRIPTOR);
	}

	public CommentDefaultAccessModifierRule(final String regex) {
		this();
		setRegex(regex);
	}

	public void setRegex(final String regex) {
		setProperty(CommentDefaultAccessModifierRule.REGEX_DESCRIPTOR, regex);
	}

	@Override
	public Object visit(final ASTCompilationUnit node, final Object data) {
		interestingLineNumberComments.clear();
		final List<Comment> comments = node.getComments();
		for (final Comment comment : comments) {
			if (comment.getImage().matches(getProperty(REGEX_DESCRIPTOR).trim())) {
				interestingLineNumberComments.add(comment.getBeginLine());
			}
		}
		return super.visit(node, data);
	}

	@Override
	public Object visit(final ASTMethodDeclaration decl, final Object data) {
		if (shouldReport(decl)) {
			addViolationWithMessage(data, decl, String.format(MESSAGE,
					decl.getFirstChildOfType(ASTMethodDeclarator.class).getImage(), "method"));
		}
		return super.visit(decl, data);
	}

	@Override
	public Object visit(final ASTFieldDeclaration decl, final Object data) {
		if (shouldReport(decl)) {
			addViolationWithMessage(data, decl, String.format(MESSAGE,
					decl.getFirstDescendantOfType(ASTVariableDeclaratorId.class).getImage(), "field"));
		}
		return super.visit(decl, data);
	}

	@Override
	public Object visit(final ASTClassOrInterfaceDeclaration decl, final Object data) {
		// check for nested classes
		if (decl.isNested() && shouldReport(decl)) {
			addViolationWithMessage(data, decl, String.format(MESSAGE, decl.getImage(), "nested class"));
		}
		return super.visit(decl, data);
	}

	private boolean shouldReport(final AbstractJavaAccessNode decl) {
		List<ASTClassOrInterfaceDeclaration> parentClassOrInterface =
		        decl.getParentsOfType(ASTClassOrInterfaceDeclaration.class);
		// ignore if is a Interface
        return (!parentClassOrInterface.isEmpty() && !parentClassOrInterface.get(0).isInterface())
		// check if the field/method/nested class has a default access modifier
		&& decl.isPackagePrivate()
		// if is a default access modifier check if there is a comment in this line
		&& !interestingLineNumberComments.contains(decl.getBeginLine())
		// that it is not annotated with @VisibleForTesting
		&& hasNoVisibleForTestingAnnotation(decl);
	}

    private boolean hasNoVisibleForTestingAnnotation(AbstractJavaAccessNode decl) {
        boolean result = true;
        ASTClassOrInterfaceBodyDeclaration parent = decl.getFirstParentOfType(ASTClassOrInterfaceBodyDeclaration.class);
        if (parent != null) {
            List<ASTAnnotation> annotations = parent.findChildrenOfType(ASTAnnotation.class);
            for (ASTAnnotation annotation : annotations) {
                List<ASTName> names = annotation.findDescendantsOfType(ASTName.class);
                for (ASTName name : names) {
                    if (name.hasImageEqualTo("VisibleForTesting")) {
                        result = false;
                        break;
                    }
                }
                if (result == false) {
                    break;
                }
            }
        }
        return result;
    }
}