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

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

import java.util.ArrayList;
import java.util.List;

import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.java.ast.ASTBlockStatement;
import net.sourceforge.pmd.lang.java.ast.ASTForInit;
import net.sourceforge.pmd.lang.java.ast.ASTLambdaExpression;
import net.sourceforge.pmd.lang.java.ast.ASTLocalVariableDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTName;
import net.sourceforge.pmd.lang.java.ast.ASTReturnStatement;
import net.sourceforge.pmd.lang.java.ast.ASTThrowStatement;
import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclarator;
import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId;
import net.sourceforge.pmd.lang.java.ast.AbstractJavaNode;
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;

/**
 * Checks for variables in methods that are defined before they are really
 * needed. A reference is deemed to be premature if it is created ahead of a
 * block of code that doesn't use it that also has the ability to return or
 * throw an exception.
 * 
 * @author Brian Remedios
 */
public class PrematureDeclarationRule extends AbstractJavaRule {

    /**
     *
     * @param node
     *            ASTLocalVariableDeclaration
     * @param data
     *            Object
     * @return Object
     * @see net.sourceforge.pmd.lang.java.ast.JavaParserVisitor#visit(ASTLocalVariableDeclaration,
     *      Object)
     */
    public Object visit(ASTLocalVariableDeclaration node, Object data) {

        // is it part of a for-loop declaration?
        if (node.jjtGetParent() instanceof ASTForInit) {
            // yes, those don't count
            return visit((AbstractJavaNode) node, data);
        }

        String varName = varNameIn(node);

        AbstractJavaNode grandparent = (AbstractJavaNode) node.jjtGetParent().jjtGetParent();

        List<ASTBlockStatement> nextBlocks = blocksAfter(grandparent, node);

        for (ASTBlockStatement block : nextBlocks) {
            if (hasReferencesIn(block, varName) || isLambda(block)) {
                break;
            }

            if (hasExit(block)) {
                addViolation(data, node, varName);
                break;
            }
        }

        return visit((AbstractJavaNode) node, data);
    }

    private boolean isLambda(ASTBlockStatement block) {
        return block.getFirstParentOfType(ASTLambdaExpression.class) != null;
    }

    /**
     * Return whether a class of the specified type exists between the node
     * argument and the topParent argument.
     * 
     * @param node
     *            Node
     * @param intermediateParentClass
     *            Class
     * @param topParent
     *            Node
     * @return boolean
     */
    public static boolean hasAsParentBetween(Node node, Class<?> intermediateParentClass, Node topParent) {

        Node currentParent = node.jjtGetParent();

        while (!currentParent.equals(topParent)) {
            currentParent = currentParent.jjtGetParent();
            if (currentParent.getClass().equals(intermediateParentClass)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns whether the block contains a return call or throws an exception.
     * Exclude blocks that have these things as part of an inner class.
     * 
     * @param block
     *            ASTBlockStatement
     * @return boolean
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    private boolean hasExit(ASTBlockStatement block) {

        List exitBlocks = block.findDescendantsOfType(ASTReturnStatement.class);
        exitBlocks.addAll(block.findDescendantsOfType(ASTThrowStatement.class));

        if (exitBlocks.isEmpty()) {
            return false;
        }

        // now check to see if the ones we have are part of a method on a
        // declared inner class
        // or part of a lambda expression
        boolean result = false;
        for (int i = 0; i < exitBlocks.size(); i++) {
            Node exitNode = (Node) exitBlocks.get(i);
            if (!hasAsParentBetween(exitNode, ASTMethodDeclaration.class, block)
                    && !hasAsParentBetween(exitNode, ASTLambdaExpression.class, block)) {
                result = true;
                break;
            }
        }

        return result;
    }

    /**
     * Returns whether the variable is mentioned within the statement block or
     * not.
     * 
     * @param block
     *            ASTBlockStatement
     * @param varName
     *            String
     * @return boolean
     */
    private static boolean hasReferencesIn(ASTBlockStatement block, String varName) {

        List<ASTName> names = block.findDescendantsOfType(ASTName.class);

        for (ASTName name : names) {
            if (isReference(varName, name.getImage())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Return whether the shortName is part of the compound name by itself or as
     * a method call receiver.
     * 
     * @param shortName
     *            String
     * @param compoundName
     *            String
     * @return boolean
     */
    private static boolean isReference(String shortName, String compoundName) {

        int dotPos = compoundName.indexOf('.');

        return dotPos < 0 ? shortName.equals(compoundName) : shortName.endsWith(compoundName.substring(0, dotPos));
    }

    /**
     * Return the name of the variable we just assigned something to.
     * 
     * @param node
     *            ASTLocalVariableDeclaration
     * @return String
     */
    private static String varNameIn(ASTLocalVariableDeclaration node) {
        ASTVariableDeclarator declarator = node.getFirstChildOfType(ASTVariableDeclarator.class);
        return ((ASTVariableDeclaratorId) declarator.jjtGetChild(0)).getImage();
    }

    /**
     * Returns the index of the node block in relation to its siblings.
     * 
     * @param block
     *            SimpleJavaNode
     * @param node
     *            Node
     * @return int
     */
    private static int indexOf(AbstractJavaNode block, Node node) {

        int count = block.jjtGetNumChildren();

        for (int i = 0; i < count; i++) {
            if (node == block.jjtGetChild(i)) {
                return i;
            }
        }

        return -1;
    }

    /**
     * Returns all the blocks found right after the node supplied within the its
     * current scope.
     * 
     * @param block
     *            SimpleJavaNode
     * @param node
     *            SimpleNode
     * @return List
     */
    private static List<ASTBlockStatement> blocksAfter(AbstractJavaNode block, AbstractJavaNode node) {

        int count = block.jjtGetNumChildren();
        int start = indexOf(block, node.jjtGetParent()) + 1;

        List<ASTBlockStatement> nextBlocks = new ArrayList<>(count);

        for (int i = start; i < count; i++) {
            Node maybeBlock = block.jjtGetChild(i);
            if (maybeBlock instanceof ASTBlockStatement) {
                nextBlocks.add((ASTBlockStatement) maybeBlock);
            }
        }

        return nextBlocks;
    }
}
