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

import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.bcel.generic.ArrayType;
import org.apache.bcel.generic.BasicType;
import org.apache.bcel.generic.ObjectType;
import org.apache.bcel.generic.ReferenceType;
import org.apache.bcel.generic.Type;

import edu.umd.cs.findbugs.Priorities;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.ba.ch.Subtypes2;
import edu.umd.cs.findbugs.ba.generic.GenericObjectType;
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.classfile.IAnalysisCache;

public class IncompatibleTypes {
	private static final ObjectType COLLECTION_TYPE = ObjectTypeFactory.getInstance("java.util.Collection");
	private static final ObjectType MAP_TYPE = ObjectTypeFactory.getInstance("java.util.Map");
	private static final ClassDescriptor LIST_DESCRIPTOR = DescriptorFactory.createClassDescriptor(List.class);
	private static final ClassDescriptor MAP_DESCRIPTOR = DescriptorFactory.createClassDescriptor(Map.class);
	private static final ClassDescriptor SET_DESCRIPTOR = DescriptorFactory.createClassDescriptor(Set.class);

	final int priority;

	final String msg;

	private IncompatibleTypes(String msg, int priority) {
		this.msg = msg;
		this.priority = priority;
	}

	public int getPriority() {
		return priority;
	}

	public String getMsg() {
		return msg;
	}

	@Override
	public String toString() {
		return msg;
	}

	public static final IncompatibleTypes SEEMS_OK = new IncompatibleTypes("Seems OK", Priorities.IGNORE_PRIORITY);

	public static final IncompatibleTypes ARRAY_AND_NON_ARRAY = new IncompatibleTypes("Array and non array",
	        Priorities.HIGH_PRIORITY);
	
	public static final IncompatibleTypes PRIMATIVE_ARRAY_AND_OTHER_ARRAY = new IncompatibleTypes("Primitive array and a non-primitive array",
	        Priorities.HIGH_PRIORITY);
	public static final IncompatibleTypes INCOMPATIBLE_PRIMATIVE_ARRAYS = new IncompatibleTypes("Incompatible primitive arrays",
	        Priorities.HIGH_PRIORITY);

	public static final IncompatibleTypes UNCHECKED = new IncompatibleTypes("Actual compile type time of argument is Object, unchecked",
	        Priorities.LOW_PRIORITY);

	public static final IncompatibleTypes ARRAY_AND_OBJECT = new IncompatibleTypes("Array and Object", Priorities.IGNORE_PRIORITY);

	public static final IncompatibleTypes INCOMPATIBLE_CLASSES = new IncompatibleTypes("Incompatible classes",
	        Priorities.HIGH_PRIORITY);

	public static final IncompatibleTypes UNRELATED_CLASS_AND_INTERFACE = new IncompatibleTypes("Unrelated class and interface",
	        Priorities.NORMAL_PRIORITY);

	public static final IncompatibleTypes UNRELATED_FINAL_CLASS_AND_INTERFACE = new IncompatibleTypes(
	        "Unrelated final class and interface", Priorities.HIGH_PRIORITY);

	public static final IncompatibleTypes UNRELATED_INTERFACES = new IncompatibleTypes("Unrelated interfaces",
	        Priorities.NORMAL_PRIORITY);

	static public @NonNull
	IncompatibleTypes getPriorityForAssumingCompatible(Type lhsType, Type rhsType) {
		return getPriorityForAssumingCompatible(lhsType, rhsType, false);
	}

	static public @NonNull
	IncompatibleTypes getPriorityForAssumingCompatible(Type expectedType, Type actualType, boolean pointerEquality) {
		if (!(expectedType instanceof ReferenceType))
			return SEEMS_OK;
		if (!(actualType instanceof ReferenceType))
			return SEEMS_OK;

		if (expectedType instanceof BasicType ^ actualType instanceof BasicType) {
			return INCOMPATIBLE_CLASSES;
		}
		while (expectedType instanceof ArrayType && actualType instanceof ArrayType) {
			expectedType = ((ArrayType) expectedType).getElementType();
			actualType = ((ArrayType) actualType).getElementType();
		}

		if (expectedType instanceof BasicType ^ actualType instanceof BasicType) {
			return PRIMATIVE_ARRAY_AND_OTHER_ARRAY;
		}
		if (expectedType instanceof BasicType && actualType instanceof BasicType) {
			if (!expectedType.equals(actualType))
				return INCOMPATIBLE_PRIMATIVE_ARRAYS;
			else return SEEMS_OK;
		}
		if (expectedType instanceof ArrayType) {
			return getPriorityForAssumingCompatibleWithArray(actualType);
		}
		if (actualType instanceof ArrayType) {
			return getPriorityForAssumingCompatibleWithArray(expectedType);
		}
		if (expectedType.equals(actualType))
			return SEEMS_OK;

		// For now, ignore the case where either reference is not
		// of an object type. (It could be either an array or null.)
		if (!(expectedType instanceof ObjectType) || !(actualType instanceof ObjectType))
			return SEEMS_OK;

		return getPriorityForAssumingCompatible((ObjectType) expectedType, (ObjectType) actualType, pointerEquality);

	}

	private static IncompatibleTypes getPriorityForAssumingCompatibleWithArray(Type rhsType) {
	    if (rhsType.equals(ObjectType.OBJECT))
	    	return ARRAY_AND_OBJECT;
	    String sig = rhsType.getSignature();
	    if (sig.equals("Ljava/io/Serializable;") || sig.equals("Ljava/lang/Cloneable;"))
	    		return SEEMS_OK;
	    return ARRAY_AND_NON_ARRAY;
    }

	static @NonNull
	XMethod getInvokedMethod(XClass xClass, String name, String sig, boolean isStatic) throws CheckedAnalysisException {
		IAnalysisCache cache = Global.getAnalysisCache();
		while (true) {
			XMethod result = xClass.findMethod(name, sig, isStatic);
			if (result != null)
				return result;
			if (isStatic)
				throw new CheckedAnalysisException();
			ClassDescriptor superclassDescriptor = xClass.getSuperclassDescriptor();
			if (superclassDescriptor == null)
				throw new CheckedAnalysisException();
			xClass = cache.getClassAnalysis(XClass.class, superclassDescriptor);
		}

	}

	static public @NonNull
	IncompatibleTypes getPriorityForAssumingCompatible(ObjectType expectedType, ObjectType actualType, boolean pointerEquality) {
		if (expectedType.equals(actualType))
			return SEEMS_OK;
		
		if (actualType.equals(ObjectType.OBJECT))
			return IncompatibleTypes.UNCHECKED;
		
		try {

			if (!Hierarchy.isSubtype(expectedType, actualType) && !Hierarchy.isSubtype(actualType, expectedType)) {
				// See if the types are related by inheritance.
				ClassDescriptor lhsDescriptor = DescriptorFactory.createClassDescriptorFromDottedClassName(expectedType.getClassName());
				ClassDescriptor rhsDescriptor = DescriptorFactory.createClassDescriptorFromDottedClassName(actualType.getClassName());

				return getPriorityForAssumingCompatible(pointerEquality, lhsDescriptor, rhsDescriptor);
			}
			
			if (expectedType instanceof GenericObjectType && actualType instanceof GenericObjectType 
					&& (Hierarchy.isSubtype(expectedType, COLLECTION_TYPE) || Hierarchy.isSubtype(expectedType, MAP_TYPE))) {
				List<? extends ReferenceType> lhsParameters = ((GenericObjectType)expectedType).getParameters();
				List<? extends ReferenceType> rhsParameters = ((GenericObjectType)actualType).getParameters();
				if (lhsParameters != null && rhsParameters != null && lhsParameters.size() == rhsParameters.size()) 
					for(int i = 0; i < lhsParameters.size(); i++) {
						IncompatibleTypes r = getPriorityForAssumingCompatible(lhsParameters.get(i), rhsParameters.get(i), pointerEquality);
						if (r.getPriority() <= Priorities.NORMAL_PRIORITY)
							return r;
					}
				
			}

		} catch (ClassNotFoundException e) {
			AnalysisContext.reportMissingClass(e);
		} catch (MissingClassException e) {
			AnalysisContext.reportMissingClass(e.getClassNotFoundException());
		} catch (CheckedAnalysisException e) {
			AnalysisContext.logError("Error checking for incompatible types", e);
		}
		return SEEMS_OK;
	}

	/**
     * @param pointerEquality
     * @param lhsDescriptor
     * @param rhsDescriptor
     * @throws CheckedAnalysisException
     * @throws ClassNotFoundException
     */
    public static IncompatibleTypes getPriorityForAssumingCompatible(boolean pointerEquality, ClassDescriptor lhsDescriptor,
            ClassDescriptor rhsDescriptor) throws CheckedAnalysisException, ClassNotFoundException {
    	AnalysisContext analysisContext = AnalysisContext.currentAnalysisContext();
 	   Subtypes2 subtypes2 = analysisContext.getSubtypes2();
		
        IAnalysisCache cache = Global.getAnalysisCache();
		XClass lhs = cache.getClassAnalysis(XClass.class, lhsDescriptor);
		XClass rhs = cache.getClassAnalysis(XClass.class, rhsDescriptor);
		 // Look up the classes
	    XMethod lhsEquals = getInvokedMethod(lhs, "equals", "(Ljava/lang/Object;)Z", false);
	    XMethod rhsEquals = getInvokedMethod(rhs, "equals", "(Ljava/lang/Object;)Z", false);
	    String lhsClassName = lhsEquals.getClassName();
	    if (lhsEquals.equals(rhsEquals)) {
	    	if (lhsClassName.equals("java.lang.Enum"))
	    		return INCOMPATIBLE_CLASSES;
	    	if (!pointerEquality && !lhsClassName.equals("java.lang.Object"))
	    		return SEEMS_OK;
	    }
		
    	if ((subtypes2.isSubtype(lhsDescriptor, SET_DESCRIPTOR)
    			&& subtypes2.isSubtype(rhsDescriptor, SET_DESCRIPTOR)
    			|| subtypes2.isSubtype(lhsDescriptor, MAP_DESCRIPTOR)
    			&& subtypes2.isSubtype(rhsDescriptor, MAP_DESCRIPTOR)
    			|| subtypes2.isSubtype(lhsDescriptor, LIST_DESCRIPTOR)
    			&& subtypes2.isSubtype(rhsDescriptor, LIST_DESCRIPTOR)))
    		return SEEMS_OK;

	    if (!lhs.isInterface() && !rhs.isInterface()) {
	    	// Both are class types, and therefore there is no possible
	    	// way
	    	// the compared objects can have the same runtime type.
	    	return INCOMPATIBLE_CLASSES;
	    } else {

	    	// Look up the common subtypes of the two types. If the
	    	// intersection does not contain at least one instantiable
	    	// class,
	    	// then issue a warning of the appropriate type.
	    	Set<ClassDescriptor> commonSubtypes = subtypes2.getTransitiveCommonSubtypes(
	    	        lhsDescriptor, rhsDescriptor);

	    	if (!containsAtLeastOneInstantiableClass(commonSubtypes)) {
	    		if (lhs.isInterface() && rhs.isInterface())
	    			return UNRELATED_INTERFACES;
	    		else if (lhs.isFinal() || rhs.isFinal())
	    			return UNRELATED_FINAL_CLASS_AND_INTERFACE;
	    		else
	    			return UNRELATED_CLASS_AND_INTERFACE;
	    	}

	    }
	    return SEEMS_OK;
    }

	private static boolean containsAtLeastOneInstantiableClass(Set<ClassDescriptor> commonSubtypes)
	        throws CheckedAnalysisException {
		IAnalysisCache cache = Global.getAnalysisCache();
		for (ClassDescriptor classDescriptor : commonSubtypes) {

			XClass xclass = cache.getClassAnalysis(XClass.class, classDescriptor);

			if (!xclass.isInterface() && !xclass.isAbstract())
				return true;
		}
		return false;
	}

}
