/**
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
 */

package net.sourceforge.pmd.lang.java.rule.codestyle;

import java.util.List;
import java.util.Map;

import net.sourceforge.pmd.lang.java.ast.ASTAnnotation;
import net.sourceforge.pmd.lang.java.ast.ASTBlockStatement;
import net.sourceforge.pmd.lang.java.ast.ASTExpression;
import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTName;
import net.sourceforge.pmd.lang.java.ast.ASTPrimaryExpression;
import net.sourceforge.pmd.lang.java.ast.ASTPrimarySuffix;
import net.sourceforge.pmd.lang.java.ast.ASTReturnStatement;
import net.sourceforge.pmd.lang.java.ast.ASTVariableInitializer;
import net.sourceforge.pmd.lang.java.ast.AccessNode;
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
import net.sourceforge.pmd.lang.java.symboltable.VariableNameDeclaration;
import net.sourceforge.pmd.lang.symboltable.NameOccurrence;
import net.sourceforge.pmd.lang.symboltable.Scope;
import net.sourceforge.pmd.lang.symboltable.ScopedNode;
import net.sourceforge.pmd.properties.BooleanProperty;

public class UnnecessaryLocalBeforeReturnRule extends AbstractJavaRule {

    private static final BooleanProperty STATEMENT_ORDER_MATTERS = new BooleanProperty("statementOrderMatters",
            "If set to false this rule no longer requires the variable declaration and return statement to be "
            + "on consecutive lines. Any variable that is used solely in a return statement will be reported.",
            true, 1.0f);

    public UnnecessaryLocalBeforeReturnRule() {
        definePropertyDescriptor(STATEMENT_ORDER_MATTERS);
    }

    @Override
    public Object visit(ASTMethodDeclaration meth, Object data) {
        // skip void/abstract/native method
        if (meth.isVoid() || meth.isAbstract() || meth.isNative()) {
            return data;
        }
        return super.visit(meth, data);
    }

    @Override
    public Object visit(ASTReturnStatement rtn, Object data) {
        // skip returns of literals
        ASTName name = rtn.getFirstDescendantOfType(ASTName.class);
        if (name == null) {
            return data;
        }

        // skip 'complicated' expressions
        if (rtn.findDescendantsOfType(ASTExpression.class).size() > 1
                || rtn.findDescendantsOfType(ASTPrimaryExpression.class).size() > 1 || isMethodCall(rtn)) {
            return data;
        }

        Map<VariableNameDeclaration, List<NameOccurrence>> vars = name.getScope()
                .getDeclarations(VariableNameDeclaration.class);
        for (Map.Entry<VariableNameDeclaration, List<NameOccurrence>> entry : vars.entrySet()) {
            VariableNameDeclaration variableDeclaration = entry.getKey();
            List<NameOccurrence> usages = entry.getValue();

            if (usages.size() == 1) { // If there is more than 1 usage, then it's not only returned
                NameOccurrence occ = usages.get(0);

                if (occ.getLocation().equals(name) && isNotAnnotated(variableDeclaration)) {
                    String var = name.getImage();
                    if (var.indexOf('.') != -1) {
                        var = var.substring(0, var.indexOf('.'));
                    }

                    // Is the variable initialized with another member that is later used?
                    if (!isInitDataModifiedAfterInit(variableDeclaration, rtn)
                            && !statementsBeforeReturn(variableDeclaration, rtn)) {
                        addViolation(data, rtn, var);
                    }
                }
            }
        }
        return data;
    }

    private boolean statementsBeforeReturn(VariableNameDeclaration variableDeclaration, ASTReturnStatement returnStatement) {
        if (!getProperty(STATEMENT_ORDER_MATTERS)) {
            return false;
        }

        ASTBlockStatement declarationStatement = variableDeclaration.getAccessNodeParent().getFirstParentOfType(ASTBlockStatement.class);
        ASTBlockStatement returnBlockStatement = returnStatement.getFirstParentOfType(ASTBlockStatement.class);

        // double check: we should now be at the same level in the AST - both block statements are children of the same parent
        if (declarationStatement.jjtGetParent() == returnBlockStatement.jjtGetParent()) {
            return returnBlockStatement.jjtGetChildIndex() - declarationStatement.jjtGetChildIndex() > 1;
        }
        return false;
    }

    private boolean isInitDataModifiedAfterInit(final VariableNameDeclaration variableDeclaration,
            final ASTReturnStatement rtn) {
        final ASTVariableInitializer initializer = variableDeclaration.getAccessNodeParent()
                .getFirstDescendantOfType(ASTVariableInitializer.class);
        if (initializer != null) {
            final List<ASTName> referencedNames = initializer.findDescendantsOfType(ASTName.class);
            for (final ASTName refName : referencedNames) {
                // TODO : Shouldn't the scope allow us to search for a var name occurrences directly, moving up through parent scopes?
                Scope scope = refName.getScope();
                do {
                    final Map<VariableNameDeclaration, List<NameOccurrence>> declarations = scope
                            .getDeclarations(VariableNameDeclaration.class);
                    for (final Map.Entry<VariableNameDeclaration, List<NameOccurrence>> entry : declarations
                            .entrySet()) {
                        if (entry.getKey().getName().equals(refName.getImage())) {
                            // Variable found! Check usage locations
                            for (final NameOccurrence occ : entry.getValue()) {
                                final ScopedNode location = occ.getLocation();
                                // Is it used after initializing our "unnecessary" local but before the return statement?
                                // TODO : should node define isAfter / isBefore helper methods?
                                if ((location.getBeginLine() > initializer.getEndLine()
                                        || (location.getBeginLine() == initializer.getEndLine() && location.getBeginColumn() >= initializer.getEndColumn()))
                                        && (location.getEndLine() < rtn.getBeginLine()
                                                || (location.getEndLine() == rtn.getBeginLine()
                                                        && location.getEndColumn() <= rtn.getEndColumn()))) {
                                    return true;
                                }
                            }

                            return false;
                        }
                    }
                    scope = scope.getParent();
                } while (scope != null);
            }
        }

        return false;
    }

    private boolean isNotAnnotated(VariableNameDeclaration variableDeclaration) {
        AccessNode accessNodeParent = variableDeclaration.getAccessNodeParent();
        return !accessNodeParent.hasDescendantOfType(ASTAnnotation.class);
    }

    /**
     * Determine if the given return statement has any embedded method calls.
     *
     * @param rtn
     *            return statement to analyze
     * @return true if any method calls are made within the given return
     */
    private boolean isMethodCall(ASTReturnStatement rtn) {
        List<ASTPrimarySuffix> suffix = rtn.findDescendantsOfType(ASTPrimarySuffix.class);
        for (ASTPrimarySuffix element : suffix) {
            if (element.isArguments()) {
                return true;
            }
        }
        return false;
    }
}
