/*
 * FindBugs - Find bugs in Java programs
 * Copyright (C) 2003-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;

import java.io.IOException;

import org.apache.bcel.Constants;

import edu.umd.cs.findbugs.ba.SignatureConverter;
import edu.umd.cs.findbugs.ba.XFactory;
import edu.umd.cs.findbugs.ba.XMethod;
import edu.umd.cs.findbugs.classfile.MethodDescriptor;
import edu.umd.cs.findbugs.internalAnnotations.DottedClassName;
import edu.umd.cs.findbugs.util.ClassName;
import edu.umd.cs.findbugs.visitclass.DismantleBytecode;
import edu.umd.cs.findbugs.visitclass.PreorderVisitor;
import edu.umd.cs.findbugs.xml.XMLAttributeList;
import edu.umd.cs.findbugs.xml.XMLOutput;

/**
 * A BugAnnotation specifying a particular method in a particular class.
 * A MethodAnnotation may (optionally) have a SourceLineAnnotation directly
 * embedded inside it to indicate the range of source lines where the
 * method is defined.
 *
 * @author David Hovemeyer
 * @see BugAnnotation
 */
public class MethodAnnotation extends PackageMemberAnnotation {
	private static final long serialVersionUID = 1L;

	private static final boolean UGLY_METHODS = SystemProperties.getBoolean("ma.ugly");

	private static final String DEFAULT_ROLE = "METHOD_DEFAULT";

	private String methodName;
	private String methodSig;
	private String fullMethod;
	private boolean isStatic;

	public static final String METHOD_DANGEROUS_TARGET_ACTUAL_GUARANTEED_NULL = "METHOD_DANGEROUS_TARGET_ACTUAL_GUARANTEED_NULL";

	public static final String METHOD_DANGEROUS_TARGET = "METHOD_DANGEROUS_TARGET";
	
	public static final String METHOD_RETURN_VALUE_OF = "METHOD_RETURN_VALUE_OF";

	public static final String METHOD_SAFE_TARGET = "METHOD_SAFE_TARGET";
	
	public static final String METHOD_EQUALS_USED = "METHOD_EQUALS_USED";
	public static final String METHOD_CALLED = "METHOD_CALLED";
	public static final String METHOD_OVERRIDDEN = "METHOD_OVERRIDDEN";


	/**
	 * Constructor.
	 *
	 * @param className  the name of the class containing the method
	 * @param methodName the name of the method
	 * @param methodSig  the Java type signature of the method
	 * @param isStatic   true if the method is static, false if not
	 */
	public MethodAnnotation(String className, String methodName, String methodSig, boolean isStatic) {
		super(className, DEFAULT_ROLE);
		this.methodName = methodName;
		if (methodSig.indexOf(".") >= 0) {
			assert false : "signatures should not be dotted: " + methodSig;
			methodSig = methodSig.replace('.','/');
		}
		this.methodSig = methodSig;
		this.isStatic = isStatic;
		fullMethod = null;
		sourceLines = null;
	}

	/**
	 * Factory method to create a MethodAnnotation from the method the
	 * given visitor is currently visiting.
	 *
	 * @param visitor the BetterVisitor currently visiting the method
	 */
	public static MethodAnnotation fromVisitedMethod(PreorderVisitor visitor) {
		String className = visitor.getDottedClassName();
		MethodAnnotation result = new MethodAnnotation(
				className,
				visitor.getMethodName(),
				visitor.getMethodSig(),
				visitor.getMethod().isStatic());

		// Try to find the source lines for the method
		SourceLineAnnotation srcLines = SourceLineAnnotation.fromVisitedMethod(visitor);
		result.setSourceLines(srcLines);

		return result;
	}

	/**
	 * Factory method to create a MethodAnnotation from a method
	 * called by the instruction the given visitor is currently visiting.
	 * 
	 * @param visitor the visitor
	 * @return the MethodAnnotation representing the called method
	 */
	public static MethodAnnotation fromCalledMethod(DismantleBytecode visitor) {
		String className = visitor.getDottedClassConstantOperand();
		String methodName = visitor.getNameConstantOperand();
		String methodSig = visitor.getSigConstantOperand();

		return fromCalledMethod(className, methodName, methodSig,
				visitor.getOpcode() == Constants.INVOKESTATIC);
	}

	/**
	 * Factory method to create the MethodAnnotation from
	 * the classname, method name, signature, etc.
	 * The method tries to look up source line information for
	 * the method.
	 * 
	 * @param className  name of the class containing the method
	 * @param methodName name of the method
	 * @param methodSig  signature of the method
	 * @param accessFlags the access flags of the method
	 * @return the MethodAnnotation
	 */
	public static MethodAnnotation fromForeignMethod(
			String className, String methodName, String methodSig, int accessFlags) {

		// FIXME: would be nice to do this without using BCEL

		 className = ClassName.toDottedClassName(className);

		// Create MethodAnnotation.
		// It won't have source lines yet.
		MethodAnnotation methodAnnotation =
			new MethodAnnotation(className, methodName, methodSig, (accessFlags & Constants.ACC_STATIC) != 0);

		SourceLineAnnotation sourceLines = SourceLineAnnotation.getSourceAnnotationForMethod(
				className, methodName, methodSig);

		methodAnnotation.setSourceLines(sourceLines);

		return methodAnnotation;
	}
	/**
	 * Factory method to create the MethodAnnotation from
	 * the classname, method name, signature, etc.
	 * The method tries to look up source line information for
	 * the method.
	 * 
	 * @param className  name of the class containing the method
	 * @param methodName name of the method
	 * @param methodSig  signature of the method
	 * @param isStatic   true if the method is static, false otherwise
	 * @return the MethodAnnotation
	 */
	public static MethodAnnotation fromForeignMethod(
			String className, String methodName, String methodSig, boolean isStatic) {

		// FIXME: would be nice to do this without using BCEL

		 className = ClassName.toDottedClassName(className);

		// Create MethodAnnotation.
		// It won't have source lines yet.
		MethodAnnotation methodAnnotation =
			new MethodAnnotation(className, methodName, methodSig, isStatic);

		SourceLineAnnotation sourceLines = SourceLineAnnotation.getSourceAnnotationForMethod(
				className, methodName, methodSig);

		methodAnnotation.setSourceLines(sourceLines);

		return methodAnnotation;
	}

	/**
	 * Create a MethodAnnotation from a method that is not
	 * directly accessible.  We will use the repository to
	 * try to find its class in order to populate the information
	 * as fully as possible.
	 * 
	 * @param className  class containing called method
	 * @param methodName name of called method
	 * @param methodSig  signature of called method
	 * @param isStatic   true if called method is static
	 * @return the MethodAnnotation for the called method
	 */
	public static MethodAnnotation fromCalledMethod(
			String className, String methodName, String methodSig, boolean isStatic) {

		MethodAnnotation methodAnnotation =
			fromForeignMethod(className, methodName, methodSig, isStatic);
		methodAnnotation.setDescription("METHOD_CALLED");
		return methodAnnotation;

	}

	/**
	 * Create a MethodAnnotation from an XMethod.
	 * 
	 * @param xmethod the XMethod
	 * @return the MethodAnnotation
	 */
	public static MethodAnnotation fromXMethod(XMethod xmethod) {
		return fromForeignMethod(
				xmethod.getClassName(),
				xmethod.getName(),
				xmethod.getSignature(),
				xmethod.isStatic());
	}

	/**
	 * Create a MethodAnnotation from a MethodDescriptor.
	 * 
	 * @param methodDescriptor the MethodDescriptor
	 * @return the MethodAnnotation
	 */
	public static BugAnnotation fromMethodDescriptor(MethodDescriptor methodDescriptor) {
		return fromForeignMethod(
				methodDescriptor.getSlashedClassName(),
				methodDescriptor.getName(),
				methodDescriptor.getSignature(),
				methodDescriptor.isStatic());
	}

	/**
	 * Get the method name.
	 */
	public String getMethodName() {
		return methodName;
	}

	public String getJavaSourceMethodName() {
		if (methodName.equals("<clinit>")) return "<static initializer>";
		if (methodName.equals("<init>")) {
			String result = getClassName();
			int pos = Math.max(result.lastIndexOf('$'),result.lastIndexOf('.'));
			return className.substring(pos+1);
		}
		return methodName;
	}
	/**
	 * Get the method type signature.
	 */
	public String getMethodSignature() {
		return methodSig;
	}

	/**
	 * Return whether or not the method is static.
	 * 
	 * @return true if the method is static, false otherwise
	 */
	public boolean isStatic() {
		return isStatic;
	}

	/**
	 * Convert to an XMethod.
	 * 
	 * @return an XMethod specifying the same method as this MethodAnnotation
	 */
	public XMethod toXMethod() {
		return XFactory.createXMethod(className, methodName, methodSig, isStatic);
	}


	public void accept(BugAnnotationVisitor visitor) {
		visitor.visitMethodAnnotation(this);
	}

	@Override
	protected String formatPackageMember(String key, ClassAnnotation primaryClass) {
		if (key.equals(""))
			return UGLY_METHODS ? getUglyMethod() : getFullMethod(primaryClass);
		else if (key.equals("givenClass")) {
			if (methodName.equals("<init>")) {
				return "new " + shorten(primaryClass.getPackageName(), className) + getSignatureInClass(primaryClass);
			}
			if (className.equals(primaryClass.getClassName())) return getNameInClass(primaryClass);
			else return shorten(primaryClass.getPackageName(), className) + "." + getNameInClass(primaryClass);
		}
		else if (key.equals("name")) {
			 return methodName;
		}
		else if (key.equals("nameAndSignature")) {
			 return getNameInClass(primaryClass);
		} else if (key.equals("shortMethod") )
			return className + "." + methodName + "(...)";
		else if (key.equals("hash")){
				String tmp= getNameInClass(false, true, true);

				return className + "." + tmp;
		}
		else if (key.equals("returnType")) {
			int i = methodSig.indexOf(')');
			String returnType = methodSig.substring(i+1);
			String pkgName = primaryClass == null ? "" : primaryClass.getPackageName();
			SignatureConverter converter = new SignatureConverter(returnType);
			return shorten(pkgName, converter.parseNext());
		}	else
			throw new IllegalArgumentException("unknown key " + key);
	}

	/**
	 * Get the "full" method name.
	 * This is a format which looks sort of like a method signature
	 * that would appear in Java source code.
	 * @param primaryClass TODO
	 */
	public String getNameInClass(ClassAnnotation primaryClass) {
		return  getNameInClass(true, false, false, false);
	}
	public String getSignatureInClass(ClassAnnotation primaryClass) {
		return  getNameInClass(true, false, false, true);
	}
	
	public String getNameInClass(boolean shortenPackages, boolean useJVMMethodName, boolean hash) {
		return getNameInClass(shortenPackages, useJVMMethodName, hash, false);
	}
	/**
	 * Get the "full" method name.
	 * This is a format which looks sort of like a method signature
	 * that would appear in Java source code.
	 * 
	 * note: If shortenPackeges==true, this will return the same
	 * value as getNameInClass(), except that method caches the
	 * result and this one does not. Calling this one may be slow.
	 * 
	 * @param shortenPackages whether to shorten package names
	 * if they are in java or in the same package as this method.
	 * @param useJVMMethodName TODO
	 * @param hash TODO
	 */
	public String getNameInClass(boolean shortenPackages, boolean useJVMMethodName, boolean hash, boolean omitMethodName) {
		// Convert to "nice" representation
		StringBuilder result = new StringBuilder();
		if (!omitMethodName) {
			if (useJVMMethodName)
				result.append(getMethodName());
			else result.append(getJavaSourceMethodName());
		}
		result.append('(');

		// append args
		SignatureConverter converter = new SignatureConverter(methodSig);

		if (converter.getFirst() != '(')
			throw new IllegalStateException("bad method signature " + methodSig);
		converter.skip();

		boolean needsComma = false;
		while (converter.getFirst() != ')') {
			if (needsComma)
				if (hash) result.append(",");
				else result.append(", ");
			if (shortenPackages)
				result.append(removePackageName(converter.parseNext()));
			else
				result.append(converter.parseNext());
			needsComma = true;
		}
		converter.skip();

		result.append(')');
		return result.toString();
	}


	/**
	 * Get the "full" method name.
	 * This is a format which looks sort of like a method signature
	 * that would appear in Java source code.
	 * @param primaryClass TODO
	 */
	public String getFullMethod(ClassAnnotation primaryClass) {
		if (fullMethod == null) {
			if (methodName.equals("<init>"))
				fullMethod = "new " + stripJavaLang(className) + getSignatureInClass(primaryClass);
			else fullMethod = stripJavaLang(className) + "." + getNameInClass(primaryClass);
		}

		return fullMethod;
	}
	
	public String stripJavaLang(@DottedClassName String className) {
		if (className.startsWith("java.lang.")) return className.substring(10);
		return className;
	}

	private String getUglyMethod() {
		return className + "." + methodName + " : " + methodSig.replace('/', '.');
	}

	@Override
	public int hashCode() {
		return className.hashCode() + methodName.hashCode() + methodSig.hashCode();
	}

	@Override
	public boolean equals(Object o) {
		if (!(o instanceof MethodAnnotation))
			return false;
		MethodAnnotation other = (MethodAnnotation) o;
		return className.equals(other.className)
				&& methodName.equals(other.methodName)
				&& methodSig.equals(other.methodSig);
	}

	public int compareTo(BugAnnotation o) {
		if (!(o instanceof MethodAnnotation)) // BugAnnotations must be Comparable with any type of BugAnnotation
			return this.getClass().getName().compareTo(o.getClass().getName());
		MethodAnnotation other = (MethodAnnotation) o;
		int cmp;
		cmp = className.compareTo(other.className);
		if (cmp != 0)
			return cmp;
		cmp = methodName.compareTo(other.methodName);
		if (cmp != 0)
			return cmp;
		return methodSig.compareTo(other.methodSig);
	}

	/* ----------------------------------------------------------------------
	 * XML Conversion support
	 * ---------------------------------------------------------------------- */

	private static final String ELEMENT_NAME = "Method";



	public void writeXML(XMLOutput xmlOutput) throws IOException {
	}

	public void writeXML(XMLOutput xmlOutput, boolean addMessages, boolean isPrimary) throws IOException {
		XMLAttributeList attributeList = new XMLAttributeList()
			.addAttribute("classname", getClassName())
			.addAttribute("name", getMethodName())
			.addAttribute("signature", getMethodSignature())
			.addAttribute("isStatic", String.valueOf(isStatic()));
		if (isPrimary) attributeList.addAttribute("primary", "true");

		String role = getDescription();
		if (!role.equals(DEFAULT_ROLE))
			attributeList.addAttribute("role", role);

		if (sourceLines == null && !addMessages) {
			xmlOutput.openCloseTag(ELEMENT_NAME, attributeList);
		} else {
			xmlOutput.openTag(ELEMENT_NAME, attributeList);
			if (sourceLines != null) {
				sourceLines.writeXML(xmlOutput);
			}
			if (addMessages) {
				xmlOutput.openTag(MESSAGE_TAG);
				xmlOutput.writeText(this.toString());
				xmlOutput.closeTag(MESSAGE_TAG);
			}
			xmlOutput.closeTag(ELEMENT_NAME);
		}
	}
	
	@Override
	public boolean isSignificant() {
		String role = getDescription();
		if (METHOD_DANGEROUS_TARGET.equals(role) 
				|| METHOD_DANGEROUS_TARGET_ACTUAL_GUARANTEED_NULL.equals(role) 
				|| METHOD_SAFE_TARGET.equals(role)
				|| METHOD_EQUALS_USED.equals(role)) return false;
		return true;
	}
}

// vim:ts=4
