/*
 * FindBugs - Find bugs in Java programs
 * Copyright (C) 2006 University of Maryland
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs.detect;

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

import org.apache.bcel.classfile.Attribute;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;

import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.BytecodeScanningDetector;
import edu.umd.cs.findbugs.ClassAnnotation;
import edu.umd.cs.findbugs.MethodAnnotation;
import edu.umd.cs.findbugs.ba.AnalysisContext;
import edu.umd.cs.findbugs.ba.XClass;
import edu.umd.cs.findbugs.ba.XFactory;
import edu.umd.cs.findbugs.ba.XMethod;
import edu.umd.cs.findbugs.bcel.BCELUtil;
import edu.umd.cs.findbugs.classfile.CheckedAnalysisException;
import edu.umd.cs.findbugs.classfile.ClassDescriptor;
import edu.umd.cs.findbugs.classfile.DescriptorFactory;
import edu.umd.cs.findbugs.classfile.Global;
import edu.umd.cs.findbugs.internalAnnotations.DottedClassName;
import edu.umd.cs.findbugs.util.ClassName;
import edu.umd.cs.findbugs.util.EditDistance;

public class UncallableMethodOfAnonymousClass extends BytecodeScanningDetector {

    BugReporter bugReporter;

    public UncallableMethodOfAnonymousClass(BugReporter bugReporter) {
        this.bugReporter = bugReporter;
    }

    XMethod potentialSuperCall;

    @Override
    public void visitJavaClass(JavaClass obj) {
        try {
            obj.getSuperClass();
        } catch (ClassNotFoundException e) {
            AnalysisContext.reportMissingClass(e);
            return;
        }

        String superclassName2 = getSuperclassName();
        boolean weird = superclassName2.equals("java.lang.Object") && obj.getInterfaceIndices().length == 0;
        boolean hasAnonymousName = ClassName.isAnonymous(obj.getClassName());
        boolean isAnonymousInnerClass = hasAnonymousName && !weird;
        if (isAnonymousInnerClass)
            super.visitJavaClass(obj);
    }

    boolean definedInThisClassOrSuper(JavaClass clazz, String method) throws ClassNotFoundException {
        if (clazz == null)
            return false;
        // System.out.println("Checking to see if " + method + " is defined in "
        // + clazz.getClassName());
        for (Method m : clazz.getMethods()) {
            String key = m.getName() + ":" + m.getSignature();
            if (!m.isStatic() && method.equals(key))
                return true;
        }

        return definedInSuperClassOrInterface(clazz, method);

    }

    @Override
    public void sawOpcode(int seen) {
        if (seen == INVOKESPECIAL) {
            XMethod m = getXMethodOperand();
            if (m == null)
                return;
            XClass c = getXClass();
            int nameDistance = EditDistance.editDistance(m.getName(), getMethodName());
            if (nameDistance < 4 && c.findMatchingMethod(m.getMethodDescriptor()) == null && !m.isFinal())
                potentialSuperCall = m;
        }
    }

    boolean definedInSuperClassOrInterface(JavaClass clazz, String method) throws ClassNotFoundException {
        if (clazz == null)
            return false;
        JavaClass superClass = clazz.getSuperClass();
        if (superClass == null)
            return false;
        try {
            XClass xClass = Global.getAnalysisCache().getClassAnalysis(XClass.class,
                    DescriptorFactory.createClassDescriptorFromDottedClassName(superClass.getClassName()));
            if (xClass.hasStubs())
                return true;
        } catch (CheckedAnalysisException e) {
            return true;
        }
        
        if (definedInThisClassOrSuper(superClass, method))
            return true;
        for (JavaClass i : clazz.getInterfaces())
            if (definedInThisClassOrSuper(i, method))
                return true;
        return false;
    }

    Set<String> definedInClass(JavaClass clazz) {
        HashSet<String> result = new HashSet<String>();
        for (Method m : clazz.getMethods()) {
            if (!skip(m))
                result.add(m.getName() + m.getSignature());
        }
        return result;
    }

    private boolean skip(Method obj) {
        if (BCELUtil.isSynthetic(obj))
            return true;
        if (obj.isPrivate())
            return true;
        if (obj.isAbstract())
            return true;

        String methodName = obj.getName();
        String sig = obj.getSignature();
        if (methodName.equals("<init>"))
            return true;
        if (methodName.equals("<clinit>"))
            return true;
        if (sig.equals("()Ljava/lang/Object;") && (methodName.equals("readResolve") || methodName.equals("writeReplace")))
            return true;
        if (methodName.startsWith("access$"))
            return true;
        if (methodName.length() < 2 || methodName.indexOf('$') >= 0)
            return true;
        XMethod m = getXMethod();
        for (ClassDescriptor c : m.getAnnotationDescriptors())
            if (c.getClassName().indexOf("inject") >= 0)
                return true;
        return false;
    }

    BugInstance pendingBug;

    @Override
    public void doVisitMethod(Method obj) {
        super.doVisitMethod(obj);
        if (pendingBug != null) {
            if (potentialSuperCall == null) {
                String role = ClassAnnotation.SUPERCLASS_ROLE;

                @DottedClassName String superclassName =  ClassName.toDottedClassName(getSuperclassName());
                if (superclassName.equals("java.lang.Object")) {

                    try {
                        JavaClass interfaces[] = getThisClass().getInterfaces();
                        if (interfaces.length == 1) {
                            superclassName = interfaces[0].getClassName();
                            role = ClassAnnotation.IMPLEMENTED_INTERFACE_ROLE;
                        }
                    } catch (ClassNotFoundException e) {
                        AnalysisContext.reportMissingClass(e);
                    }
                }
                pendingBug.addClass(superclassName).describe(role);
                try {
                    XClass from = Global.getAnalysisCache().getClassAnalysis(XClass.class,
                            DescriptorFactory.createClassDescriptorFromDottedClassName(superclassName));
                    XMethod  potentialMatch = null;
                    for(XMethod m : from.getXMethods())
                        if (!m.isStatic() && !m.isPrivate() && m.getName().toLowerCase().equals(obj.getName().toLowerCase())) {
                            if (potentialMatch == null)
                                potentialMatch = m;
                            else {
                                // multiple matches; ignore all
                                potentialMatch = null;
                                break;
                            }
                    }
                     if (potentialMatch != null)
                         pendingBug.addMethod(potentialMatch)
                         .describe(MethodAnnotation.METHOD_DID_YOU_MEAN_TO_OVERRIDE);

                } catch (CheckedAnalysisException e) {
                    AnalysisContext.logError("Error: ", e);
                }
            } else {
                pendingBug.setPriority(pendingBug.getPriority() - 1);
                pendingBug.addMethod(potentialSuperCall).describe(MethodAnnotation.METHOD_DID_YOU_MEAN_TO_OVERRIDE);
            }
            bugReporter.reportBug(pendingBug);
            pendingBug = null;
            potentialSuperCall = null;
        }

    }

    @Override
    public void visit(Code obj) {
        if (pendingBug != null)
            super.visit(obj);
    }

    @Override
    public void visit(Method obj) {
        try {

            if (skip(obj))
                return;

            JavaClass clazz = getThisClass();
            XMethod xmethod = XFactory.createXMethod(clazz, obj);
            XFactory factory = AnalysisContext.currentXFactory();
            String key = obj.getName() + ":" + obj.getSignature();
            if (!factory.isCalled(xmethod) && (obj.isStatic() || !definedInSuperClassOrInterface(clazz, key))) {
                int priority = NORMAL_PRIORITY;
                JavaClass superClass = clazz.getSuperClass();
                String superClassName = superClass.getClassName();
                if (superClassName.equals("java.lang.Object")) {
                    priority = NORMAL_PRIORITY;

                } else if (definedInClass(superClass).containsAll(definedInClass(clazz)))
                    priority = NORMAL_PRIORITY;
                else
                    priority = HIGH_PRIORITY;
                Code code = null;
                for (Attribute a : obj.getAttributes())
                    if (a instanceof Code) {
                        code = (Code) a;
                        break;
                    }
                if (code != null && code.getLength() == 1)
                    priority++;

                pendingBug = new BugInstance(this, "UMAC_UNCALLABLE_METHOD_OF_ANONYMOUS_CLASS", priority).addClassAndMethod(this);
                potentialSuperCall = null;
            }

        } catch (ClassNotFoundException e) {
            bugReporter.reportMissingClass(e);
        }

    }

}
