/*
 * 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;

import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.NoSuchElementException;

import edu.umd.cs.findbugs.model.ClassNameRewriter;

/**
 * A slightly more intellegent way of comparing BugInstances from two versions
 * to see if they are the "same".  Uses class and method hashes to try to
 * handle renamings, at least for simple cases.  (<em>Hashes disabled for the
 * time being.</em>)  Uses opcode context to try to identify code that is the
 * same, even if it moves within the method.  Also compares by bug abbreviation
 * rather than bug type, since the "same" bug can change type if the context
 * changes (e.g., "definitely null" to "null on simple path" for a null pointer
 * dereference).  Also, we often change bug types between different versions
 * of FindBugs.
 * 
 * @see edu.umd.cs.findbugs.BugInstance
 * @see edu.umd.cs.findbugs.VersionInsensitiveBugComparator
 * @author David Hovemeyer
 */
public class FuzzyBugComparator implements WarningComparator {
	private static final boolean DEBUG = false;

	// Don't use hashes for now.  Still ironing out issues there.
	private static final boolean USE_HASHES = false;

	private static final long serialVersionUID = 1L;

	/**
	 * Filter ignored BugAnnotations from given Iterator.
	 */
	private static class FilteringBugAnnotationIterator implements Iterator<BugAnnotation> {

		Iterator<BugAnnotation> iter;
		BugAnnotation next;

		public FilteringBugAnnotationIterator(Iterator<BugAnnotation> iter) {
			this.iter = iter;
		}

		private void findNext() {
			if (next == null) {
				while (iter.hasNext()) {
					BugAnnotation candidate = iter.next();
					if (!ignore(candidate)) {
						next = candidate;
						break;
					}
				}
			}
		}

		/* (non-Javadoc)
		 * @see java.util.Iterator#hasNext()
		 */
		public boolean hasNext() {
			findNext();
			return next != null;
		}

		/* (non-Javadoc)
		 * @see java.util.Iterator#next()
		 */
		public BugAnnotation next() {
			findNext();
			if (next == null)
				throw new NoSuchElementException();
			BugAnnotation result = next;
			next = null;
			return result;
		}

		/* (non-Javadoc)
		 * @see java.util.Iterator#remove()
		 */
		public void remove() {
			throw new UnsupportedOperationException();
		}
	}

	/** Keep track of which BugCollections the various BugInstances have come from. */
	private IdentityHashMap<BugInstance, BugCollection> bugCollectionMap;

	private ClassNameRewriter classNameRewriter;

	/**
	 * Map of class hashes to canonicate class names used for comparison purposes.
	 */
	//private Map<ClassHash, String> classHashToCanonicalClassNameMap;

	public FuzzyBugComparator() {
		if (DEBUG) System.out.println("Created fuzzy comparator");
		this.bugCollectionMap = new IdentityHashMap<BugInstance, BugCollection>();
		//this.classHashToCanonicalClassNameMap = new TreeMap<ClassHash, String>();
	}

	/**
	 * Register a BugCollection.  This allows us to find the class and method
	 * hashes for BugInstances to be compared.
	 * 
	 * @param bugCollection a BugCollection
	 */
	public void registerBugCollection(BugCollection bugCollection) {
		// For now, nothing to do
	}

	/* (non-Javadoc)
	 * @see edu.umd.cs.findbugs.WarningComparator#setClassNameRewriter(edu.umd.cs.findbugs.model.MovedClassMap)
	 */
	public void setClassNameRewriter(ClassNameRewriter classNameRewriter) {
		this.classNameRewriter = classNameRewriter;
	}

	public int compare(BugInstance lhs, BugInstance rhs) {
		int cmp;

		if (DEBUG) System.out.println("Fuzzy comparison");

		// Bug abbreviations must match.
		BugPattern lhsPattern = lhs.getBugPattern();
		BugPattern rhsPattern = rhs.getBugPattern();

		if (lhsPattern == null || rhsPattern == null) {
			if (DEBUG) {
				if (lhsPattern == null)
					System.out.println("Missing pattern: " + lhs.getType());
				if (rhsPattern == null)
					System.out.println("Missing pattern: " + rhs.getType());
			}
			String lhsCode = getCode(lhs.getType());
			String rhsCode = getCode(rhs.getType());
			if ((cmp = lhsCode.compareTo(rhsCode)) != 0)
				return cmp;
		} else {
			if ((cmp = lhsPattern.getAbbrev().compareTo(rhsPattern.getAbbrev())) != 0)
				return cmp;
		}

		BugCollection lhsCollection = bugCollectionMap.get(lhs);
		BugCollection rhsCollection = bugCollectionMap.get(rhs);

		// Scan through bug annotations, comparing fuzzily if possible

		Iterator<BugAnnotation> lhsIter = new FilteringBugAnnotationIterator(lhs.annotationIterator());
		Iterator<BugAnnotation> rhsIter = new FilteringBugAnnotationIterator(rhs.annotationIterator());

		while (lhsIter.hasNext() && rhsIter.hasNext()) {
			BugAnnotation lhsAnnotation = lhsIter.next();
			BugAnnotation rhsAnnotation = rhsIter.next();

			if (DEBUG) System.out.println("Compare annotations: " + lhsAnnotation + "," + rhsAnnotation);

			// Annotation classes must match exactly
			cmp = lhsAnnotation.getClass().getName().compareTo(rhsAnnotation.getClass().getName());
			if (cmp != 0) {
				if (DEBUG) System.out.println("annotation class mismatch: " + lhsAnnotation.getClass().getName() +
						"," + rhsAnnotation.getClass().getName());
				return cmp;
			}

			if (lhsAnnotation.getClass() == ClassAnnotation.class)
				cmp = compareClasses(lhsCollection, rhsCollection, (ClassAnnotation) lhsAnnotation, (ClassAnnotation) rhsAnnotation);
			else if (lhsAnnotation.getClass() == MethodAnnotation.class)
				cmp = compareMethods(lhsCollection, rhsCollection, (MethodAnnotation) lhsAnnotation, (MethodAnnotation) rhsAnnotation);
			else if (lhsAnnotation.getClass() == SourceLineAnnotation.class)
				cmp = compareSourceLines(lhsCollection, rhsCollection, (SourceLineAnnotation) lhsAnnotation, (SourceLineAnnotation) rhsAnnotation);
			else
				// everything else just compare directly
				cmp = lhsAnnotation.compareTo(rhsAnnotation);

			if (cmp != 0)
				return cmp;
		}

		// Number of bug annotations must match
		if (!lhsIter.hasNext() && !rhsIter.hasNext()) {
			if (DEBUG) System.out.println("Match!");
			return 0;
		} else
			return (lhsIter.hasNext() ? 1 : -1);
	}

	/**
	 * @param type
	 * @return the code of the Bug
	 */
	private String getCode(String type) {
		int bar = type.indexOf('_');
		if (bar < 0)
			return "";
		else
			return type.substring(0, bar);
	}

	private static int compareNullElements(Object a, Object b) {
		if (a != null)
			return 1;
		else if (b != null)
			return -1;
		else
			return 0;
	}

	public int compareClasses(BugCollection lhsCollection, BugCollection rhsCollection, ClassAnnotation lhsClass, ClassAnnotation rhsClass) {
		if (lhsClass == null || rhsClass == null) {
			return compareNullElements(lhsClass, rhsClass);
		} else {
			return compareClassesByName(lhsCollection, rhsCollection, lhsClass.getClassName(), rhsClass.getClassName());
		}
	}

	// Compare classes: either exact fully qualified name must match, or class hash must match
	public int compareClassesByName(BugCollection lhsCollection, BugCollection rhsCollection, String lhsClassName, String rhsClassName) {

		lhsClassName = rewriteClassName(lhsClassName);
		rhsClassName = rewriteClassName(rhsClassName);

		return lhsClassName.compareTo(rhsClassName);
	}

	/**
	 * @param className
	 * @return the rewritten class name
	 */
	private String rewriteClassName(String className) {
		if (classNameRewriter != null) {
			className = classNameRewriter.rewriteClassName(className);
		}
		return className;
	}

	// Compare methods: either exact name and signature must match, or method hash must match
	public int compareMethods(BugCollection lhsCollection, BugCollection rhsCollection, MethodAnnotation lhsMethod, MethodAnnotation rhsMethod) {
		if (lhsMethod == null || rhsMethod == null) {
			return compareNullElements(lhsMethod, rhsMethod);
		}

		// Compare for exact match
		int cmp = lhsMethod.compareTo(rhsMethod);

		return cmp;
	}

	/**
	 * For now, just look at the 2 preceeding and succeeding opcodes
	 * for fuzzy source line matching.
	 */
	private static final int NUM_CONTEXT_OPCODES = 2;

	/**
	 * Compare source line annotations.
	 * 
	 * @param rhsCollection lhs BugCollection
	 * @param lhsCollection rhs BugCollection
	 * @param lhs           a SourceLineAnnotation
	 * @param rhs           another SourceLineAnnotation
	 * @return comparison of lhs and rhs
	 */
	public int compareSourceLines(BugCollection lhsCollection, BugCollection rhsCollection, SourceLineAnnotation lhs, SourceLineAnnotation rhs) {
		if (lhs == null || rhs == null) {
			return compareNullElements(lhs, rhs);
		}

		// Classes must match fuzzily.
		int cmp = compareClassesByName(lhsCollection, rhsCollection, lhs.getClassName(), rhs.getClassName());
		if (cmp != 0)
			return cmp;

		return 0;
	}

	// See "FindBugsAnnotationDescriptions.properties"
	private static final HashSet<String> significantDescriptionSet = new HashSet<String>();
	static {
		// Classes, methods, and fields are significant.
		significantDescriptionSet.add("CLASS_DEFAULT");
		significantDescriptionSet.add("CLASS_EXCEPTION");
		significantDescriptionSet.add("CLASS_REFTYPE");
		significantDescriptionSet.add(ClassAnnotation.SUPERCLASS_ROLE);
		significantDescriptionSet.add(ClassAnnotation.IMPLEMENTED_INTERFACE_ROLE);
		significantDescriptionSet.add(ClassAnnotation.INTERFACE_ROLE);
		significantDescriptionSet.add("METHOD_DEFAULT");
		significantDescriptionSet.add(MethodAnnotation.METHOD_CALLED);
		significantDescriptionSet.add("METHOD_DANGEROUS_TARGET"); // but do NOT use safe targets
		significantDescriptionSet.add("METHOD_DECLARED_NONNULL");
		significantDescriptionSet.add("FIELD_DEFAULT");
		significantDescriptionSet.add("FIELD_ON");
		significantDescriptionSet.add("FIELD_SUPER");
		significantDescriptionSet.add("FIELD_MASKED");
		significantDescriptionSet.add("FIELD_MASKING");
		significantDescriptionSet.add("FIELD_STORED");
		significantDescriptionSet.add("TYPE_DEFAULT");
		significantDescriptionSet.add("TYPE_EXPECTED");
		significantDescriptionSet.add("TYPE_FOUND");

		significantDescriptionSet.add("LOCAL_VARIABLE_NAMED");


		// Many int annotations are NOT significant: e.g., sync %, biased locked %, bytecode offset, etc.
		// The null parameter annotations, however, are definitely significant.
		significantDescriptionSet.add("INT_NULL_ARG");
		significantDescriptionSet.add("INT_MAYBE_NULL_ARG");
		significantDescriptionSet.add("INT_NONNULL_PARAM");
		// Only DEFAULT source line annotations are significant.
		significantDescriptionSet.add("SOURCE_LINE_DEFAULT");
	}

	public static boolean ignore(BugAnnotation annotation) {
		return !significantDescriptionSet.contains(annotation.getDescription());
	}
}
