/*
 * FindBugs - Find bugs in Java programs
 * Copyright (C) 2005 Dave Brosius <dbrosius@users.sourceforge.net>
 * Copyright (C) 2005 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.Constants;
import org.apache.bcel.Repository;
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 org.apache.bcel.classfile.Synthetic;
import org.apache.bcel.generic.Type;

import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.BytecodeScanningDetector;
import edu.umd.cs.findbugs.StatelessDetector;
import edu.umd.cs.findbugs.ba.ClassContext;

public class UselessSubclassMethod extends BytecodeScanningDetector implements StatelessDetector {

	enum State { SEEN_NOTHING, SEEN_PARM, SEEN_LAST_PARM, SEEN_INVOKE, SEEN_RETURN, SEEN_INVALID };
	

	private BugReporter bugReporter;
	private String superclassName;
	private State state;
	private int curParm;
	private int curParmOffset;
	private int invokePC;
	private Type[] argTypes;
	private Set<String> interfaceMethods = null;

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



	@Override
	public void visitClassContext(ClassContext classContext) {
		try {
			JavaClass cls = classContext.getJavaClass();
			superclassName = cls.getSuperclassName();
			JavaClass[] interfaces = null;
			if (cls.isClass() && ((cls.getAccessFlags() & Constants.ACC_ABSTRACT) != 0)) {
				interfaces = cls.getAllInterfaces();
				interfaceMethods = new HashSet<String>();
				for (JavaClass aInterface : interfaces) {
					Method[] infMethods = aInterface.getMethods();
					for (Method meth : infMethods) {
						interfaceMethods.add(meth.getName() + meth.getSignature());
					}
				}
			}
		} catch (ClassNotFoundException cnfe) {
			bugReporter.reportMissingClass(cnfe);
		}
		super.visitClassContext(classContext);
	}

	@Override
	public void visitAfter(JavaClass obj) {
		interfaceMethods = null;
		super.visitAfter(obj);
	}

	@Override
	public void visitMethod(Method obj) {
		if ((interfaceMethods != null) && ((obj.getAccessFlags() & Constants.ACC_ABSTRACT) != 0)) {
			String curDetail = obj.getName() + obj.getSignature();
			for (String infMethodDetail : interfaceMethods) {
				if (curDetail.equals(infMethodDetail))
					bugReporter.reportBug(new BugInstance(this, "USM_USELESS_ABSTRACT_METHOD", LOW_PRIORITY)
					.addClassAndMethod(getClassContext().getJavaClass(), obj));
			}
		}
		super.visitMethod(obj);
	}

	@Override
	public void visitCode(Code obj)
	{
		try {
			String methodName = getMethodName();

			if (!methodName.equals("<init>")
					&&  !methodName.equals("clone")
					&&  ((getMethod().getAccessFlags() & (Constants.ACC_STATIC|Constants.ACC_SYNTHETIC)) == 0)) {

				/* for some reason, access flags doesn't return Synthetic, so do this hocus pocus */
				Attribute[] atts = getMethod().getAttributes();
				for (Attribute att : atts) {
					if (att.getClass().equals(Synthetic.class))
						return;
				}

				byte[] codeBytes = obj.getCode();
				if ((codeBytes.length == 0) || (codeBytes[0] != ALOAD_0))
					return;

				state = State.SEEN_NOTHING;
				invokePC = 0;
				super.visitCode(obj);
				if ((state == State.SEEN_RETURN) && (invokePC != 0)) {
					//Do this check late, as it is potentially expensive
					Method superMethod = findSuperclassMethod(superclassName, getMethod());
					if ((superMethod == null) || accessModifiersAreDifferent(getMethod(), superMethod))
						return;

					bugReporter.reportBug( new BugInstance( this, "USM_USELESS_SUBCLASS_METHOD", LOW_PRIORITY )
					.addClassAndMethod(this)
					.addSourceLine(this, invokePC));
				}
			}
		}
		catch (ClassNotFoundException cnfe) {
			bugReporter.reportMissingClass(cnfe);
		}
	}

	@Override
	public void sawOpcode(int seen) {
		switch (state) {
		case SEEN_NOTHING:
			if (seen == ALOAD_0) {
				argTypes = Type.getArgumentTypes(this.getMethodSig()); 
				curParm = 0;
				curParmOffset = 1;
				if (argTypes.length > 0)
					state = State.SEEN_PARM;
				else
					state = State.SEEN_LAST_PARM;
			} else
				state = State.SEEN_INVALID;	
			break;

		case SEEN_PARM:
			if (curParm >= argTypes.length)
				state = State.SEEN_INVALID;
			else {
				String signature = argTypes[curParm++].getSignature();
				char typeChar0 = signature.charAt(0);
				if ((typeChar0 == 'L') || (typeChar0 == '[')) {
					checkParm(seen, ALOAD_0, ALOAD, 1);
				}
				else if (typeChar0 == 'D') {
					checkParm(seen, DLOAD_0, DLOAD, 2);
				}
				else if (typeChar0 == 'F') {
					checkParm(seen, FLOAD_0, FLOAD, 1);
				}
				else if (typeChar0 == 'I' || typeChar0 == 'Z' || typeChar0 == 'S' || typeChar0 == 'C' ) {
					checkParm(seen, ILOAD_0, ILOAD, 1);
				}
				else if (typeChar0 == 'J') {
					checkParm(seen, LLOAD_0, LLOAD, 2);
				} else 
					state = State.SEEN_INVALID;
				
				if ((state != State.SEEN_INVALID) && (curParm >= argTypes.length))
					state = State.SEEN_LAST_PARM;

			}
			break;

		case SEEN_LAST_PARM:
			if ((seen == INVOKENONVIRTUAL) && getMethodName().equals(getNameConstantOperand()) && getMethodSig().equals(getSigConstantOperand())) {
				invokePC = getPC();
				state = State.SEEN_INVOKE;
			}
			else
				state = State.SEEN_INVALID;
			break;

		case SEEN_INVOKE:
			Type returnType = getMethod().getReturnType();
			char retSigChar0 = returnType.getSignature().charAt(0);
			if ((retSigChar0 == 'V') && (seen == RETURN))
				state = State.SEEN_RETURN;
			else if (((retSigChar0 == 'L') || (retSigChar0 == '[')) && (seen == ARETURN))
				state = State.SEEN_RETURN;
			else if ((retSigChar0 == 'D') && (seen == DRETURN))
				state = State.SEEN_RETURN;
			else if ((retSigChar0 == 'F') && (seen == FRETURN))
				state = State.SEEN_RETURN;
			else if ((retSigChar0 == 'I' || retSigChar0 == 'S'  || retSigChar0 == 'C'  || retSigChar0 == 'B'  || retSigChar0 == 'Z' ) && (seen == IRETURN))
				state = State.SEEN_RETURN;
			else if ((retSigChar0 == 'J') && (seen == LRETURN))
				state = State.SEEN_RETURN;
			else
				state = State.SEEN_INVALID;
			break;

		case SEEN_RETURN:
			state = State.SEEN_INVALID;
			break;
		}
	}

	private void checkParm(int seen, int fastOpBase, int slowOp, int parmSize) {
		if ((curParmOffset >= 1) && (curParmOffset <= 3)) {
			if (seen == (fastOpBase + curParmOffset))
				curParmOffset += parmSize;
			else
				state = State.SEEN_INVALID;
		}
		else if (curParmOffset == 0)
			state = State.SEEN_INVALID;
		else if ((seen == slowOp) && (getRegisterOperand() == curParmOffset))
			curParmOffset += parmSize;
		else
			state = State.SEEN_INVALID;
	}

	private Method findSuperclassMethod(String superclassName, Method subclassMethod) 
	throws ClassNotFoundException {

		String methodName = subclassMethod.getName();
		Type[] subArgs = null;
		JavaClass superClass = Repository.lookupClass(superclassName);
		Method[] methods = superClass.getMethods();
		outer:
			for (Method m : methods) {
				if (m.getName().equals(methodName)) {
					if (subArgs == null)
						subArgs = Type.getArgumentTypes(subclassMethod.getSignature());
					Type[] superArgs = Type.getArgumentTypes(m.getSignature());
					if (subArgs.length == superArgs.length) {
						for (int j = 0; j < subArgs.length; j++) {
							if (!superArgs[j].equals(subArgs[j]))
								continue outer;
						}
						return m;
					}
				}
			}

		if(!superclassName.equals("Object")) {
			String superSuperClassName = superClass.getSuperclassName();
			if (superSuperClassName.equals(superclassName)) {
				throw new ClassNotFoundException(
						"superclass of " + superclassName + " is itself");
			}
			return findSuperclassMethod(superClass.getSuperclassName(), subclassMethod);
		}

		return null;
	}

	private boolean accessModifiersAreDifferent(Method m1, Method m2) {
		int access1 = m1.getAccessFlags() & (Constants.ACC_PRIVATE|Constants.ACC_PROTECTED|Constants.ACC_PUBLIC|Constants.ACC_FINAL);
		int access2 = m2.getAccessFlags() & (Constants.ACC_PRIVATE|Constants.ACC_PROTECTED|Constants.ACC_PUBLIC|Constants.ACC_FINAL);

		return access1 != access2;
	}
}
