/*
 * 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 java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.meta.TypeQualifierValidator;
import javax.annotation.meta.When;

import edu.umd.cs.findbugs.SystemProperties;
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.classfile.IAnalysisCache;
import edu.umd.cs.findbugs.classfile.analysis.ClassData;
import edu.umd.cs.findbugs.internalAnnotations.DottedClassName;
import edu.umd.cs.findbugs.log.Profiler;
import edu.umd.cs.findbugs.util.DualKeyHashMap;
import edu.umd.cs.findbugs.util.Util;

/**
 * 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<A extends Annotation> {
    public static final boolean DEBUG = SystemProperties.getBoolean("tqv.debug");
    public static final boolean DEBUG_CLASSLOADING = SystemProperties.getBoolean("tqv.debug.classloading");

    private static final ClassDescriptor EXCLUSIVE_ANNOTATION = DescriptorFactory.instance().getClassDescriptor(
            javax.annotation.meta.Exclusive.class);

    private static final ClassDescriptor EXHAUSTIVE_ANNOTATION = DescriptorFactory.instance().getClassDescriptor(
            javax.annotation.meta.Exhaustive.class);

    public final ClassDescriptor typeQualifier;
    public final Class<A> typeQualifierClass;

    public final A proxy;

    public final @CheckForNull Object value;

    private final boolean isStrict;

    private final boolean isExclusive;

    private final boolean isExhaustive;

    private final @CheckForNull
    TypeQualifierValidator<A> validator;



    private TypeQualifierValue(ClassDescriptor typeQualifier, @CheckForNull Object value) {
        this.typeQualifier = typeQualifier;
        this.value = value;
        /**  will be set to true if this is a strict type qualifier value */
        boolean isStrict1 = false;
        /**  will be set to true if this is an exclusive type qualifier value */
        boolean isExclusive1 = false;
        /** will be set to true if this is an exhaustive type qualifier value */
        boolean isExhaustive1 = false;
        TypeQualifierValidator<A> validator1 = null;
        Class<A> qualifierClass = null;
        XClass xclass  = null;
        A proxy1 = null;
        try {
            xclass = Global.getAnalysisCache().getClassAnalysis(XClass.class, typeQualifier);

            // 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) {
                isStrict1 = true;
            }
            for (XMethod xmethod : xclass.getXMethods()) {
                if (xmethod.getName().equals("value") && xmethod.getSignature().startsWith("()")) {
                    isExhaustive1 = xmethod.getAnnotation(EXHAUSTIVE_ANNOTATION) != null;
                    if (isExhaustive1) {
                        // exhaustive qualifiers are automatically exclusive
                        isExclusive1 = true;
                    } else {
                        // see if there is an explicit @Exclusive annotation
                        isExclusive1 = 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 " + typeQualifier.toDottedClassName(), e);
        }
        this.isStrict = isStrict1;
        this.isExclusive = isExclusive1;
        this.isExhaustive = isExhaustive1;
        
        
        if (xclass != null) {
            ClassDescriptor checkerName = DescriptorFactory.createClassDescriptor(typeQualifier.getClassName() + "$Checker");

            if (!SystemProperties.RUNNING_IN_ECLIPSE) {
                /**   don't do this if running in Eclipse; check below is the quick
                fix for bug 3599258 (Random obscure Eclipse failures during
                 analysis) */

                try {
                    Global.getAnalysisCache().getClassAnalysis(ClassData.class, checkerName);

                    // found it.
                    SecurityManager m = System.getSecurityManager();
                    if (m == null) {
                        if (DEBUG_CLASSLOADING) System.out.println("Setting ValidationSecurityManager");
                        System.setSecurityManager(ValidationSecurityManager.INSTANCE);
                    }

                    Class<?> c = ValidatorClassLoader.INSTANCE.loadClass(checkerName.getDottedClassName());
                    if (TypeQualifierValidator.class.isAssignableFrom(c)) {

                        @SuppressWarnings("unchecked")
                        Class<? extends TypeQualifierValidator<A>> validatorClass = (Class<? extends TypeQualifierValidator<A>>) c
                        .asSubclass(TypeQualifierValidator.class);
                        validator1 = getValidator(validatorClass);
                        qualifierClass = getQualifierClass(typeQualifier);

                        InvocationHandler handler = new InvocationHandler() {

                            public Object invoke(Object arg0, Method arg1, Object[] arg2) throws Throwable {
                                if (arg1.getName() == "value")
                                    return TypeQualifierValue.this.value;
                                throw new UnsupportedOperationException("Can't handle " + arg1);
                            }
                        };

                        proxy1 = qualifierClass.cast(Proxy.newProxyInstance(ValidatorClassLoader.INSTANCE,
                                new Class[] { qualifierClass }, handler));
                    }

                } catch (ClassNotFoundException e) {
                    assert true; // ignore
                } catch (CheckedAnalysisException e) {
                    assert true; // ignore
                } catch (Exception e) {
                    AnalysisContext.logError("Unable to construct type qualifier checker " + checkerName, e);
                } catch (Throwable e) {
                    AnalysisContext.logError("Unable to construct type qualifier checker " + checkerName + " due to "
                            + e.getClass().getSimpleName() + ":" + e.getMessage());
                }
            }
            else if (DEBUG_CLASSLOADING) {
                SecurityManager m = System.getSecurityManager();
                if (m == null) {
                    if (DEBUG_CLASSLOADING) System.out.println("Setting ValidationSecurityManager");
                    System.setSecurityManager(ValidationSecurityManager.INSTANCE);
                }
            }
        }
        this.validator = validator1;
        this.typeQualifierClass = qualifierClass;
        this.proxy = proxy1;
    }

    private static <A extends Annotation> TypeQualifierValidator<A> getValidator(
            Class<? extends TypeQualifierValidator<A>> checkerClass)
            throws InstantiationException, IllegalAccessException {
        return checkerClass.newInstance();
    }

    @SuppressWarnings("unchecked")
    private static <A> Class<A> getQualifierClass(ClassDescriptor typeQualifier) throws ClassNotFoundException {
        @DottedClassName String className = typeQualifier.getDottedClassName();
        if (DEBUG_CLASSLOADING)
            System.out.println("Getting qualifier class for " + className);
        if (className.startsWith("javax.annotation"))
            return (Class<A>) Class.forName(className);
        try {
            Global.getAnalysisCache().getClassAnalysis(ClassData.class, typeQualifier);
        } catch (CheckedAnalysisException e) {
            throw new ClassNotFoundException("No class data found for " + className);
        }
        
        ValidatorClassLoader validatorLoader = ValidatorClassLoader.INSTANCE;
        return (Class<A>) validatorLoader.loadClass(typeQualifier.getDottedClassName());
    }

    static byte[] loadClassData(String name) throws CheckedAnalysisException {
        ClassDescriptor d = DescriptorFactory.createClassDescriptorFromDottedClassName(name);
        ClassData data = Global.getAnalysisCache().getClassAnalysis(ClassData.class, d);
        return data.getData();
    }
    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();
    }

    public boolean canValidate(@CheckForNull Object constantValue) {
        if (validator == null)
            return false;
        return true;
    }

    public When validate(@CheckForNull Object constantValue) {
        if (validator == null)
            throw new IllegalStateException("No validator");
        IAnalysisCache analysisCache = Global.getAnalysisCache();
        Profiler profiler = analysisCache.getProfiler();
        profiler.start(validator.getClass());
        try {
            return ValidationSecurityManager.sandboxedValidation(proxy, validator, constantValue);
        } catch (Exception e) {
            AnalysisContext.logError("Error executing custom validator for " + typeQualifier + " " + constantValue, e);
            return When.UNKNOWN;
        } finally {
            profiler.end(validator.getClass());
        }
    }

    /**
     * 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
     */
    @SuppressWarnings("rawtypes")
    public static @Nonnull
    TypeQualifierValue<?> getValue(ClassDescriptor desc, @CheckForNull  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);
        map.put(desc, value, result);
        instance.get().allKnownTypeQualifiers.add(result);
        return result;
    }
    @SuppressWarnings("unchecked")
    public static @Nonnull <A extends Annotation>
    TypeQualifierValue<A> getValue(Class <A> clazz, @CheckForNull Object value) {
        return (TypeQualifierValue<A>) getValue(DescriptorFactory.createClassDescriptor(clazz), value);
    }

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

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

    /**
     * Return whether or not this TypeQualifierValue denotes a strict qualifier.
     *
     * @return true if type qualifier is strict, false otherwise
     */
    public boolean isStrictQualifier() {
        return isStrict;
    }

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

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

    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();
        buf.append(typeQualifier.toString());
        if (value != null) {
            buf.append(':');
            buf.append(value.toString());
        }
        return buf.toString();
    }

}
