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

import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.util.List;
import java.util.Map;
import net.ranides.assira.rules.utils.ASTUtils;

import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.java.ast.ASTConstructorDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
import net.sourceforge.pmd.lang.java.ast.JavaNode;
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
import net.sourceforge.pmd.lang.java.symboltable.JavaNameOccurrence;
import net.sourceforge.pmd.lang.java.symboltable.VariableNameDeclaration;
import net.sourceforge.pmd.lang.rule.properties.BooleanProperty;
import net.sourceforge.pmd.lang.symboltable.NameOccurrence;

@SuppressWarnings("OverridableMethodCallInConstructor")
public class UnusedParameterRule extends AbstractJavaRule {

    private static final BooleanProperty CHECKALL_DESCRIPTOR = new BooleanProperty(
        "checkAll", "Check all methods, including non-private ones", false, 1.0f
    );

    public UnusedParameterRule() {
        definePropertyDescriptor(CHECKALL_DESCRIPTOR);
    }

    @Override
    public Object visit(ASTConstructorDeclaration node, Object data) {
        check(node, data);
        return data;
    }

    @Override
    public Object visit(ASTMethodDeclaration node, Object data) {
        if (!node.isPrivate() && !getProperty(CHECKALL_DESCRIPTOR)) {
            return data;
        }
        if (!node.isNative() && !node.isAbstract() && !isSerializationMethod(node)) {
            check(node, data);
        }
        return data;
    }

    private boolean isSerializationMethod(ASTMethodDeclaration node) {
        return node.isPrivate() 
            && ASTUtils.isMethod(node, "readObject", ObjectInputStream.class)
            && ASTUtils.throwsOne(node, InvalidObjectException.class);
    }

    private void check(Node node, Object data) {
        Node parent = node.jjtGetParent().jjtGetParent().jjtGetParent();
        if (!ASTUtils.isTypeClass(parent)) {
            return;
        }

        Map<VariableNameDeclaration, List<NameOccurrence>> vars = 
                ((JavaNode)node).getScope().getDeclarations(VariableNameDeclaration.class);
        
        for (Map.Entry<VariableNameDeclaration, List<NameOccurrence>> entry : vars.entrySet()) {
            VariableNameDeclaration name = entry.getKey();
            if( name.getImage().startsWith("$") || name.getImage().startsWith("_")) {
                continue;
            }
            if (isActuallyUsed(name, entry.getValue())) {
                continue;
            }
            addViolation(data, name.getNode(), new Object[] {
                node instanceof ASTMethodDeclaration ? "method" : "constructor", 
                name.getImage() 
            });
        }
    }

    private boolean isActuallyUsed(VariableNameDeclaration name, List<NameOccurrence> usages) {
        for (NameOccurrence occ : usages) {
            JavaNameOccurrence jocc = (JavaNameOccurrence) occ;
            if (jocc.isOnLeftHandSide()) {
                if (name.isArray() && jocc.getLocation().jjtGetParent().jjtGetParent().jjtGetNumChildren() > 1) {
                    // array element access
                    return true;
                }
            } else {
                return true;
            }
        }
        return false;
    }
}
