/*
 * FindBugs - Find Bugs in Java programs
 * 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.model;

import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import org.apache.bcel.Repository;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.FieldOrMethod;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;

import edu.umd.cs.findbugs.ba.Hierarchy;
import edu.umd.cs.findbugs.ba.JavaClassAndMethod;
import edu.umd.cs.findbugs.ba.SignatureParser;
import edu.umd.cs.findbugs.xml.XMLAttributeList;
import edu.umd.cs.findbugs.xml.XMLOutput;
import edu.umd.cs.findbugs.xml.XMLWriteable;

/**
 * Features of a class which may be used to identify it if it is renamed
 * or modified.
 * 
 * @author David Hovemeyer
 */
public class ClassFeatureSet implements XMLWriteable {
	public static final String CLASS_NAME_KEY = "Class:";
	public static final String METHOD_NAME_KEY = "Method:";
	public static final String CODE_LENGTH_KEY = "CodeLength:";
	public static final String FIELD_NAME_KEY = "Field:";

	private String className;
	private boolean isInterface;
	private Set<String> featureSet;

	/**
	 * Constructor.
	 * Creates an empty feature set.
	 */
	public ClassFeatureSet() {
		this.featureSet = new HashSet<String>();
	}

	/**
	 * Minimum code length required to add a CodeLength feature.
	 */
	public static final int MIN_CODE_LENGTH = 10;

	/**
	 * Initialize from given JavaClass.
	 * 
	 * @param javaClass the JavaClass
	 * @return this object
	 */
	public ClassFeatureSet initialize(JavaClass javaClass) {
		this.className = javaClass.getClassName();
		this.isInterface = javaClass.isInterface();

		addFeature(CLASS_NAME_KEY + transformClassName(javaClass.getClassName()));

		for (Method method : javaClass.getMethods()) {
			if (!isSynthetic(method)) {
				String transformedMethodSignature = transformMethodSignature(method.getSignature());

				if (method.isStatic() || !overridesSuperclassMethod(javaClass, method)) {
					addFeature(METHOD_NAME_KEY + method.getName() + ":" + transformedMethodSignature);
				}

				Code code = method.getCode();
				if (code != null && code.getCode() != null && code.getCode().length >= MIN_CODE_LENGTH) {
					addFeature(CODE_LENGTH_KEY + method.getName() + ":" + transformedMethodSignature +
							":" + code.getCode().length);
				}
			}
		}

		for (Field field : javaClass.getFields()) {
			if (!isSynthetic(field)) {
				addFeature(
						FIELD_NAME_KEY + field.getName() + ":" +
						transformSignature(field.getSignature()));
			}
		}

		return this;
	}

	/**
	 * Determine if given method overrides a superclass or superinterface method.
	 * 
	 * @param javaClass class defining the method
	 * @param method    the method
	 * @return true if the method overrides a superclass/superinterface method, false if not
	 * @throws ClassNotFoundException 
	 */
	private boolean overridesSuperclassMethod(JavaClass javaClass, Method method) {
		if (method.isStatic())
			return false;

		try {
			JavaClass[] superclassList = javaClass.getSuperClasses();
			if (superclassList != null) {
				JavaClassAndMethod match =
					Hierarchy.findMethod(superclassList, method.getName(), method.getSignature(), Hierarchy.INSTANCE_METHOD);
				if (match != null)
					return true;
			}

			JavaClass[] interfaceList = javaClass.getAllInterfaces();
			if (interfaceList != null) {
				JavaClassAndMethod match =
					Hierarchy.findMethod(interfaceList, method.getName(), method.getSignature(), Hierarchy.INSTANCE_METHOD);
				if (match != null)
					return true;
			}

			return false;
		} catch (ClassNotFoundException e) {
			return true;
		}
	}

	/**
	 * Figure out if a class member (field or method) is synthetic.
	 * 
	 * @param member a field or method
	 * @return true if the member is synthetic
	 */
	private boolean isSynthetic(FieldOrMethod member) {
		if (member.isSynthetic()) // this never works, but worth a try
			return true;

		String name = member.getName();

		if (name.startsWith("class$"))
			return true;

		if (name.startsWith("access$"))
			return true;

		return false;
	}

	/**
	 * @return Returns the className.
	 */
	public String getClassName() {
		return className;
	}

	/**
	 * @param className The className to set.
	 */
	public void setClassName(String className) {
		this.className = className;
	}

	/**
	 * @return Returns the isInterface.
	 */
	public boolean isInterface() {
		return isInterface;
	}

	/**
	 * @param isInterface The isInterface to set.
	 */
	public void setInterface(boolean isInterface) {
		this.isInterface = isInterface;
	}

	public int getNumFeatures() {
		return featureSet.size();
	}

	public void addFeature(String feature) {
		featureSet.add(feature);
	}

	public Iterator<String> featureIterator() {
		return featureSet.iterator();
	}

	public boolean hasFeature(String feature) {
		return featureSet.contains(feature);
	}

	/**
	 * Transform a class name by stripping its package name.
	 * 
	 * @param className a class name
	 * @return the transformed class name
	 */
	public static String transformClassName(String className) {
		int lastDot = className.lastIndexOf('.');
		if (lastDot >= 0) {
			String pkg = className.substring(0, lastDot);
			if (!isUnlikelyToBeRenamed(pkg)) {
				className = className.substring(lastDot + 1);
			}
		}
		return className;
	}

	/**
	 * Return true if classes in the given package is unlikely to be renamed:
	 * e.g., because they are part of a public API.
	 * 
	 * @param pkg the package name
	 * @return true if classes in the package is unlikely to be renamed 
	 */
	public static boolean isUnlikelyToBeRenamed(String pkg) {
		return pkg.startsWith("java.");
	}

	/**
	 * Transform a method signature to allow it to be compared even if
	 * any of its parameter types are moved to another package.
	 * 
	 * @param signature a method signature
	 * @return the transformed signature
	 */
	public static String transformMethodSignature(String signature) {
		StringBuilder buf = new StringBuilder();

		buf.append('(');

		SignatureParser parser = new SignatureParser(signature);
		for (Iterator<String> i = parser.parameterSignatureIterator(); i.hasNext(); ) {
			String param = i.next();
				param = transformSignature(param);
			buf.append(param);
		}

		buf.append(')');

		return buf.toString();
	}

	/**
	 * Transform a field or method parameter signature to allow it to be
	 * compared even if it is moved to another package.
	 * 
	 * @param signature the signature
	 * @return the transformed signature
	 */
	public static String transformSignature(String signature) {
		StringBuilder buf = new StringBuilder();

		int lastBracket = signature.lastIndexOf('[');
		if (lastBracket > 0) {
			buf.append(signature.substring(0, lastBracket+1));
			signature = signature.substring(lastBracket+1);
		}

		if (signature.startsWith("L")) {
			signature = signature.substring(1, signature.length() - 1).replace('/', '.');
			signature = transformClassName(signature);
			signature = "L" + signature.replace('.', '/') +  ";";
		}
		buf.append(signature);

		return buf.toString();
	}

	/**
	 * Minimum number of features which must be present in order
	 * to declare two classes similar.
	 */
	public static final int MIN_FEATURES = 5;

	/**
	 * Minimum similarity required to declare two classes similar.
	 */
	public static final double MIN_MATCH = 0.60;

	/**
	 * Similarity of classes which don't have enough features to match
	 * exactly, but whose class names match exactly.
	 */
	public static final double EXACT_CLASS_NAME_MATCH = MIN_MATCH + 0.1;

	public static double similarity(ClassFeatureSet a, ClassFeatureSet b) {
		// Some features must match exactly
		if (a.isInterface() != b.isInterface())
			return 0.0;

		if (a.getNumFeatures() < MIN_FEATURES || b.getNumFeatures() < MIN_FEATURES)
			return a.getClassName().equals(b.getClassName()) ? EXACT_CLASS_NAME_MATCH : 0.0;

		int numMatch = 0;
		int max = Math.max(a.getNumFeatures(), b.getNumFeatures());

		for (Iterator<String> i = a.featureIterator(); i.hasNext();) {
			String feature = i.next();
			if (b.hasFeature(feature)) {
				++numMatch;
			}
		}

		return ((double) numMatch / (double) max);

	}

	public boolean similarTo(ClassFeatureSet other) {
		return similarity(this, other) >= MIN_MATCH;
	}

	public static void main(String[] args) throws Exception {
		if (args.length != 2) {
			System.err.println("Usage: " + ClassFeatureSet.class.getName() + " <class 1> <class 2>");
			System.exit(1);
		}

		JavaClass a = Repository.lookupClass(args[0]);
		JavaClass b = Repository.lookupClass(args[1]);

		ClassFeatureSet aFeatures = new ClassFeatureSet().initialize(a);
		ClassFeatureSet bFeatures = new ClassFeatureSet().initialize(b);

		System.out.println("Similarity is " + similarity(aFeatures, bFeatures));
		System.out.println("Classes are" + (aFeatures.similarTo(bFeatures) ? "" : " not") + " similar");
	}

	public static final String ELEMENT_NAME = "ClassFeatureSet";
	public static final String FEATURE_ELEMENT_NAME = "Feature";

	/* (non-Javadoc)
	 * @see edu.umd.cs.findbugs.xml.XMLWriteable#writeXML(edu.umd.cs.findbugs.xml.XMLOutput)
	 */
	public void writeXML(XMLOutput xmlOutput) throws IOException {
		xmlOutput.openTag(ELEMENT_NAME, new XMLAttributeList().addAttribute("class", className));
		for (Iterator<String> i = featureIterator(); i.hasNext(); ) {
			String feature = i.next();
			xmlOutput.openCloseTag(
					FEATURE_ELEMENT_NAME,
					new XMLAttributeList().addAttribute("value", feature));
		}
		xmlOutput.closeTag(ELEMENT_NAME);
	}
}
