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

import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.ba.AnalysisContext;
import edu.umd.cs.findbugs.ba.MissingClassException;
import edu.umd.cs.findbugs.ba.XClass;
import edu.umd.cs.findbugs.ba.XMethod;
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.util.DualKeyHashMap;
import edu.umd.cs.findbugs.util.Util;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;

/**
 * A TypeQualifierValue is a pair specifying a type qualifier annotation
 * and a value.  Each TypeQualifierValue is effectively a different
 * type qualifier.  For example, if Foo is a type qualifier annotation
 * having an int value, then Foo(0), Foo(1), etc. are all
 * different type qualifiers which must be checked separately.
 *
 * @author William Pugh
 */
public class TypeQualifierValue {
	public static final boolean DEBUG = SystemProperties.getBoolean("tqv.debug");
	
	private static final ClassDescriptor EXCLUSIVE_ANNOTATION =
		DescriptorFactory.instance().getClassDescriptor("javax/annotation/meta/Exclusive");
	private static final ClassDescriptor EXHAUSTIVE_ANNOTATION =
		DescriptorFactory.instance().getClassDescriptor("javax/annotation/meta/Exhaustive");
	
	public final ClassDescriptor typeQualifier;
	public final @CheckForNull Object value;
	private boolean isStrict;
	private boolean isExclusive;
	private boolean isExhaustive;

	private TypeQualifierValue(ClassDescriptor typeQualifier, @CheckForNull Object value) {
		this.typeQualifier =  typeQualifier;
		this.value = value;
		this.isStrict = false; // will be set to true if this is a strict type qualifier value
		this.isExclusive = false; // will be set to true if this is an exclusive type qualifier value
		this.isExhaustive = false; // will be set to true if this is an exhaustive type qualifier value
	}

	static class Data {
		/**
		 * Cache in which constructed TypeQualifierValues are interned.
		 */
		DualKeyHashMap <ClassDescriptor, Object, TypeQualifierValue> typeQualifierMap =
			new DualKeyHashMap <ClassDescriptor, Object, TypeQualifierValue>();
		
		/**
		 * Set of all known TypeQualifierValues.
		 */
		Set<TypeQualifierValue> allKnownTypeQualifiers =
			new HashSet<TypeQualifierValue>();
	}

	private static ThreadLocal<Data> instance = new ThreadLocal<Data>() {
		@Override
		protected Data initialValue() {
			return new Data();
		}
	};
	
	public static void clearInstance() {
		instance.remove();
	}

	/**
	 * Given a ClassDescriptor/value pair, return the
	 * interned TypeQualifierValue representing that pair.
	 *
	 * @param desc  a ClassDescriptor denoting a type qualifier annotation
	 * @param value a value
	 * @return an interned TypeQualifierValue object
	 */
	public static @NonNull TypeQualifierValue getValue(ClassDescriptor desc, Object value) {
		DualKeyHashMap<ClassDescriptor, Object, TypeQualifierValue> map = instance.get().typeQualifierMap;
		TypeQualifierValue result = map.get(desc, value);
		if (result != null) return result;
		result = new TypeQualifierValue(desc, value);
		determineIfQualifierIsStrict(desc, result);
		determineIfQualifierIsExclusiveOrExhaustive(desc, result);
		map.put(desc, value, result);
		instance.get().allKnownTypeQualifiers.add(result);
		return result;
	}
	
	/**
	 * Get Collection of all known TypeQualifierValues.
	 * 
	 * @return Collection of all known TypeQualifierValues
	 */
	public static Collection<TypeQualifierValue> getAllKnownTypeQualifiers() {
		return Collections.unmodifiableSet(instance.get().allKnownTypeQualifiers);
	}
	
	/**
	 * Get the "complementary" TypeQualifierValues for given exclusive type qualifier.
	 * 
	 * @param tqv a type qualifier (which must be exclusive)
	 * @return Collection of complementary exclusive type qualifiers
	 */
	public static Collection<TypeQualifierValue> getComplementaryExclusiveTypeQualifierValue(TypeQualifierValue tqv) {
		assert tqv.isExclusiveQualifier();
		
		LinkedList<TypeQualifierValue> result = new LinkedList<TypeQualifierValue>();
		
		for (TypeQualifierValue t : instance.get().allKnownTypeQualifiers) {
			//
			// Any TypeQualifierValue with the same
			// annotation class but a different value is a complementary
			// type qualifier.
			//
			if (t.typeQualifier.equals(tqv.typeQualifier) && !Util.nullSafeEquals(t.value, tqv.value)) {
				result.add(t);
			}
		}
		
		return result;
	}

	/**
	 * Determine whether or not given TypeQualifierValue has multiple variants.
	 * I.e., if Color is a type qualifier having values RED, GREEN, and BLUE,
	 * then there are 3 variants, Color(RED), Color(GREEN), and COLOR(BLUE).
	 *
	 * @param tqv a TypeQualifierValue
	 * @return true if there are multiple variants of this type qualifier, false otherwise
	 */
	public static boolean hasMultipleVariants(TypeQualifierValue tqv) {
		int count = 0;
		for (TypeQualifierValue t : instance.get().allKnownTypeQualifiers) {
			if (t.typeQualifier.equals(tqv.typeQualifier)) {
				count++;
			}
		}
		return count > 1;
	}
	
	private static void determineIfQualifierIsStrict(ClassDescriptor desc, TypeQualifierValue result) {
		if (DEBUG) {
			System.out.print("Checking to see if " + desc + " requires strict checking...");
		}
		// Check to see if the type qualifier should be checked strictly
		try {
			XClass xclass = Global.getAnalysisCache().getClassAnalysis(XClass.class, desc);

			// Annotation elements appear as abstract methods in the annotation class (interface).
			// So, if the type qualifier annotation has specified a default When value,
			// it will appear as an abstract method called "when".
			XMethod whenMethod = xclass.findMethod("when", "()Ljavax/annotation/meta/When;", false);
			if (whenMethod == null) {
				result.setIsStrict();
			}
		} catch (MissingClassException e) {
			AnalysisContext.currentAnalysisContext().getLookupFailureCallback().reportMissingClass(e.getClassNotFoundException());
		} catch (CheckedAnalysisException e) {
			AnalysisContext.logError("Error looking up annotation class " + desc.toDottedClassName(), e);
		}
		if (DEBUG) {
			System.out.println(result.isStrictQualifier() ? "yes" : "no");
		}
	}
	
	private static void determineIfQualifierIsExclusiveOrExhaustive(ClassDescriptor desc, TypeQualifierValue result) {
		if (DEBUG) {
			System.out.print("Checking to see if " + desc + " is exclusive or exhaustive...");
		}
		
		boolean isExclusive = false;
		boolean isExhaustive = false;
		
		try {
			XClass xclass = Global.getAnalysisCache().getClassAnalysis(XClass.class, desc); 

			// If the value() method is annotated as @Exhaustive, the type qualifier is exhaustive and exclusive.
			// If the value() method is annotated as @Exclusive, the type qualifier is exclusive.
			for (XMethod xmethod : xclass.getXMethods()) {
				if (xmethod.getName().equals("value") && xmethod.getSignature().startsWith("()")) {
					isExhaustive = xmethod.getAnnotation(EXHAUSTIVE_ANNOTATION) != null;
					if (isExhaustive) {
						// exhaustive qualifiers are automatically exclusive
						isExclusive = true;
					} else {
						// see if there is an explicit @Exclusive annotation
						isExclusive = xmethod.getAnnotation(EXCLUSIVE_ANNOTATION) != null;
					}
					
					break;
				}
			}
		} catch (MissingClassException e) {
			AnalysisContext.currentAnalysisContext().getLookupFailureCallback().reportMissingClass(e.getClassNotFoundException());
		} catch (CheckedAnalysisException e) {
			AnalysisContext.logError("Error looking up annotation class " + desc.toDottedClassName(), e);
		}
		
		if (isExclusive) {
			result.setIsExclusive();
		}
		if (isExhaustive) {
			result.setIsExhaustive();
		}
		
		if (DEBUG) {
			//System.out.println(result.isExclusiveQualifier() ? "yes" : "no");
			if (isExhaustive) {
				System.out.println("exhaustive,exclusive");
			} else if (isExclusive) {
				System.out.println("exclusive");
			} else {
				System.out.println("neither");
			}
		}
	}

	/**
	 * Get the ClassDescriptor which specifies the type qualifier annotation.
	 *
	 * @return ClassDescriptor which specifies the type qualifier annotation
	 */
	public ClassDescriptor getTypeQualifierClassDescriptor() {
		return typeQualifier;
	}

	/**
	 * Mark this as a type qualifier value that should
	 * be checked strictly.
	 */
	private void setIsStrict() {
		this.isStrict = true;
	}

	/**
	 * Return whether or not this TypeQualifierValue denotes
	 * a strict qualifier.
	 *
	 * @return true if type qualifier is strict, false otherwise
	 */
	public boolean isStrictQualifier() {
		return isStrict;
	}
	
	private void setIsExclusive() {
		this.isExclusive = true;
	}
	
	/**
	 * Return whether or not this TypeQualifierValue denotes
	 * an exclusive qualifier.
	 * 
	 * @return true if type qualifier is exclusive, false otherwise
	 */
	public boolean isExclusiveQualifier() {
		return isExclusive;
	}

	
	private void setIsExhaustive() {
		this.isExhaustive = true;
	}

	/**
	 * Return whether or not this TypeQualifierValue denotes
	 * an exhaustive qualifier.
	 * 
	 * @return true if type qualifier is exhaustive, false otherwise
	 */
	public boolean isExhaustiveQualifier() {
		return isExhaustive;
	}

	@Override
	public int hashCode() {
		int result = typeQualifier.hashCode();
		if (value != null) result += 37*value.hashCode();
		return result;
	}

	@Override
	public boolean equals(Object o) {
		if (!(o instanceof TypeQualifierValue)) return false;
		TypeQualifierValue other = (TypeQualifierValue) o;
		return typeQualifier.equals(other.typeQualifier) && Util.nullSafeEquals(value, other.value);
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		StringBuilder buf = new StringBuilder();
		buf.append(typeQualifier.toString());
		if (value != null) {
			buf.append(':');
			buf.append(value.toString());
		}
		return buf.toString();
	}


}
