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

import java.math.BigInteger;

import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.java.ast.ASTBooleanLiteral;
import net.sourceforge.pmd.lang.java.ast.ASTCastExpression;
import net.sourceforge.pmd.lang.java.ast.ASTExpression;
import net.sourceforge.pmd.lang.java.ast.ASTFieldDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTLiteral;
import net.sourceforge.pmd.lang.java.ast.ASTNullLiteral;
import net.sourceforge.pmd.lang.java.ast.ASTPrimaryExpression;
import net.sourceforge.pmd.lang.java.ast.ASTReferenceType;
import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclarator;
import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId;
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;

/**
 * Detects redundant field initializers, i.e. the field initializer expressions
 * the JVM would assign by default.
 *
 * @author lucian.ciufudean@gmail.com
 * @since Apr 10, 2009
 */
public class RedundantFieldInitializerRule extends AbstractJavaRule {

    public RedundantFieldInitializerRule() {
        addRuleChainVisit(ASTFieldDeclaration.class);
    }

    public Object visit(ASTFieldDeclaration fieldDeclaration, Object data) {
        // Finals can only be initialized once.
        if (fieldDeclaration.isFinal()) {
            return data;
        }

        // Look for a match to the following XPath:
        // VariableDeclarator/VariableInitializer/Expression/PrimaryExpression/PrimaryPrefix/Literal
        for (ASTVariableDeclarator variableDeclarator : fieldDeclaration
                .findChildrenOfType(ASTVariableDeclarator.class)) {
            if (variableDeclarator.jjtGetNumChildren() > 1) {
                final Node variableInitializer = variableDeclarator.jjtGetChild(1);
                if (variableInitializer.jjtGetChild(0) instanceof ASTExpression) {
                    final Node expression = variableInitializer.jjtGetChild(0);
                    final Node primaryExpression;
                    if (expression.jjtGetNumChildren() == 1) {
                        if (expression.jjtGetChild(0) instanceof ASTPrimaryExpression) {
                            primaryExpression = expression.jjtGetChild(0);
                        } else if (expression.jjtGetChild(0) instanceof ASTCastExpression
                                && expression.jjtGetChild(0).jjtGetChild(1) instanceof ASTPrimaryExpression) {
                            primaryExpression = expression.jjtGetChild(0).jjtGetChild(1);
                        } else {
                            continue;
                        }
                    } else {
                        continue;
                    }
                    final Node primaryPrefix = primaryExpression.jjtGetChild(0);
                    if (primaryPrefix.jjtGetNumChildren() == 1 && primaryPrefix.jjtGetChild(0) instanceof ASTLiteral) {
                        final ASTLiteral literal = (ASTLiteral) primaryPrefix.jjtGetChild(0);
                        if (isRef(fieldDeclaration, variableDeclarator)) {
                            // Reference type
                            if (literal.jjtGetNumChildren() == 1 && literal.jjtGetChild(0) instanceof ASTNullLiteral) {
                                addViolation(data, variableDeclarator);
                            }
                        } else {
                            // Primitive type
                            if (literal.jjtGetNumChildren() == 1 && literal.jjtGetChild(0) instanceof ASTBooleanLiteral) {
                                // boolean type
                                ASTBooleanLiteral booleanLiteral = (ASTBooleanLiteral) literal.jjtGetChild(0);
                                if (!booleanLiteral.isTrue()) {
                                    addViolation(data, variableDeclarator);
                                }
                            } else if (literal.jjtGetNumChildren() == 0) {
                                // numeric type
                                // Note: Not catching NumberFormatException, as
                                // it shouldn't be happening on valid source
                                // code.
                                Number value = -1;
                                if (literal.isIntLiteral()) {
                                    value = parseInteger(literal.getImage());
                                } else if (literal.isLongLiteral()) {
                                    String s = literal.getImage();
                                    // remove the ending "l" or "L" for long
                                    // values
                                    s = s.substring(0, s.length() - 1);
                                    value = parseInteger(s);
                                } else if (literal.isFloatLiteral()) {
                                    String s = literal.getImage();
                                    // remove the ending "f" or "F" for float
                                    // values
                                    s = s.substring(0, s.length() - 1);
                                    value = Float.valueOf(s);
                                } else if (literal.isDoubleLiteral()) {
                                    value = Double.valueOf(literal.getImage());
                                } else if (literal.isCharLiteral()) {
                                    value = (int) literal.getImage().charAt(1);
                                }

                                if (value.doubleValue() == 0) {
                                    addViolation(data, variableDeclarator);
                                }
                            }
                        }
                    }
                }
            }
        }

        return data;
    }

    /**
     * Checks if a FieldDeclaration is a reference type (includes arrays). The
     * reference information is in the FieldDeclaration for this example:
     * 
     * <pre>
     * int[] ia1
     * </pre>
     * 
     * and in the VariableDeclarator for this example:
     * 
     * <pre>
     * int ia2[];
     * </pre>
     * 
     * .
     *
     * @param fieldDeclaration
     *            the field to check.
     * @param variableDeclarator
     *            the variable declarator to check.
     * @return <code>true</code> if the field is a reference. <code>false</code>
     *         otherwise.
     */
    private boolean isRef(ASTFieldDeclaration fieldDeclaration, ASTVariableDeclarator variableDeclarator) {
        Node type = fieldDeclaration.jjtGetChild(0).jjtGetChild(0);
        if (type instanceof ASTReferenceType) {
            // Reference type, array or otherwise
            return true;
        } else {
            // Primitive array?
            return ((ASTVariableDeclaratorId) variableDeclarator.jjtGetChild(0)).isArray();
        }
    }

    private void addViolation(Object data, ASTVariableDeclarator variableDeclarator) {
        super.addViolation(data, variableDeclarator, variableDeclarator.jjtGetChild(0).getImage());
    }

    private Number parseInteger(String s) {
        boolean negative = false;
        String number = s;
        if (number.charAt(0) == '-') {
            negative = true;
            number = number.substring(1);
        }
        BigInteger result;
        if (number.startsWith("0x") || number.startsWith("0X")) {
            result = new BigInteger(number.substring(2).replaceAll("_", ""), 16);
        } else if (number.startsWith("0b") || number.startsWith("0B")) {
            result = new BigInteger(number.substring(2).replaceAll("_", ""), 8);
        } else if (number.charAt(0) == '0' && number.length() > 1) {
            result = new BigInteger(number.substring(1).replaceAll("_", ""), 8);
        } else {
            result = new BigInteger(number.replaceAll("_", ""));
        }
        if (negative) {
            result = result.negate();
        }
        return result;
    }
}