/*
* JBoss, Home of Professional Open Source
* Copyright 2006, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This 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 software 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 software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.deployers.plugins.annotations;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.jboss.deployers.spi.annotations.AnnotationEnvironment;
import org.jboss.deployers.spi.annotations.Element;
import org.jboss.logging.Logger;
import org.jboss.metadata.spi.signature.Signature;
import org.jboss.util.collection.CollectionsFactory;

/**
 * DefaultAnnotationEnvironment.
 *
 * @author <a href="mailto:ales.justin@jboss.com">Ales Justin</a>
 */
public class DefaultAnnotationEnvironment extends WeakClassLoaderHolder implements AnnotationEnvironment, Serializable
{
   /** The serial version UID */
   private static final long serialVersionUID = 1L;
   /** The log */
   private static final Logger log = Logger.getLogger(DefaultAnnotationEnvironment.class);
   /** The info map */
   private transient Map<Class<? extends Annotation>, Map<ElementType, Set<ClassSignaturePair>>> env;
   /** The checked class names */
   private transient Set<String> checkedClassNames;
   /** Should we keep the annotation */
   private boolean keepAnnotations;

   public DefaultAnnotationEnvironment(ClassLoader classLoader)
   {
      super(classLoader);
      env = new HashMap<Class<? extends Annotation>, Map<ElementType, Set<ClassSignaturePair>>>();
      checkedClassNames = new HashSet<String>();
   }

   /**
    * Set the keep annotations flag.
    *
    * @param keepAnnotations the keep annotations flag
    */
   public void setKeepAnnotations(boolean keepAnnotations)
   {
      this.keepAnnotations = keepAnnotations;
   }

   /**
    * Get env map.
    *
    * @return the env map
    */
   protected Map<Class<? extends Annotation>, Map<ElementType, Set<ClassSignaturePair>>> getEnv()
   {
      if (env == null)
         throw new IllegalArgumentException("Null env, previously serialized?");

      return env;
   }

   /**
    * Was class name already checked.
    *
    * @param className the class name
    * @return true if already checked, false otherwise
    */
   boolean isAlreadyChecked(String className)
   {
      return checkedClassNames.contains(className);
   }

   /**
    * Put the annotation info.
    *
    * @param annotation the annotation
    * @param type the annotation type
    * @param className the class name
    * @param signature the signature
    */
   void putAnnotation(Annotation annotation, ElementType type, String className, Signature signature)
   {
      Class<? extends Annotation> annClass = annotation.annotationType();

      if (log.isTraceEnabled())
         log.trace("Adding annotation @" + annClass.getSimpleName() + " for " + className + " at type " + type + ", signature: " + signature);

      // add to checked
      checkedClassNames.add(className);

      Map<Class<? extends Annotation>, Map<ElementType, Set<ClassSignaturePair>>> env = getEnv();

      Map<ElementType, Set<ClassSignaturePair>> elements = env.get(annClass);
      if (elements == null)
      {
         elements = new HashMap<ElementType, Set<ClassSignaturePair>>();
         env.put(annClass, elements);
      }

      Set<ClassSignaturePair> classes = elements.get(type);
      if (classes == null)
      {
         classes = CollectionsFactory.createLazySet();
         elements.put(type, classes);
      }

      ClassSignaturePair pair;
      if (keepAnnotations)
         pair = new ClassSignaturePair(className, signature, annotation);
      else
         pair = new ClassSignaturePair(className, signature);
      classes.add(pair);
   }

   /**
    * Get matching cs pairs.
    *
    * @param annClass the annotation class
    * @param type the annotation type
    * @return class names
    */
   protected Set<ClassSignaturePair> getCSPairs(Class<? extends Annotation> annClass, ElementType type)
   {
      Set<ClassSignaturePair> pairs = null;

      Map<ElementType, Set<ClassSignaturePair>> elements = getEnv().get(annClass);
      if (elements != null)
         pairs = elements.get(type);

      return (pairs != null) ? pairs : Collections.<ClassSignaturePair>emptySet();
   }

   /**
    * Transform class names into classes.
    *
    * @param <A> the annotation type
    * @param <M> the annotated element type
    * @param type the annotation type
    * @param annClass the annotation class
    * @param aoClass the ao class
    * @return classes
    */
   protected <A extends Annotation, M extends AnnotatedElement> Set<Element<A, M>> transformToElements(
         ElementType type,
         Class<A> annClass,
         Class<M> aoClass
   )
   {
      Set<ClassSignaturePair> pairs = getCSPairs(annClass, type);
      if (pairs.isEmpty())
         return Collections.emptySet();

      ClassLoader classLoader = getClassLoader();
      Set<Element<A, M>> elements = new HashSet<Element<A, M>>();
      for (ClassSignaturePair pair : pairs)
      {
         String className = pair.getClassName();
         A annotation = annClass.cast(pair.getAnnotation());

         Element<A, M> element;
         if (type == ElementType.TYPE)
            element = new ClassElement<A, M>(classLoader, className, annClass, annotation);
         else if (type == ElementType.PARAMETER)
            element = new ParametersElement<A,M>(classLoader, className, pair.getSignature(), annClass, annotation, aoClass);
         else
            element = new DefaultElement<A,M>(classLoader, className, pair.getSignature(), annClass, annotation, aoClass);
         elements.add(element);
      }
      return elements;
   }

   public boolean hasClassAnnotatedWith(Class<? extends Annotation> annotation)
   {
      return getCSPairs(annotation, ElementType.TYPE).isEmpty() == false;
   }

   @SuppressWarnings("unchecked")
   public <A extends Annotation> Set<Element<A, Class<?>>> classIsAnnotatedWith(Class<A> annotation)
   {
      return (Set) transformToElements(ElementType.TYPE, annotation, Class.class);
   }

   @SuppressWarnings("unchecked")
   public <A extends Annotation> Set<Element<A, Constructor<?>>> classHasConstructorAnnotatedWith(Class<A> annotation)
   {
      return (Set) transformToElements(ElementType.CONSTRUCTOR, annotation, Constructor.class);
   }

   public <A extends Annotation> Set<Element<A, Field>> classHasFieldAnnotatedWith(Class<A> annotation)
   {
      return transformToElements(ElementType.FIELD, annotation, Field.class);
   }

   public <A extends Annotation> Set<Element<A, Method>> classHasMethodAnnotatedWith(Class<A> annotation)
   {
      return transformToElements(ElementType.METHOD, annotation, Method.class);
   }

   public <A extends Annotation> Set<Element<A, AnnotatedElement>> classHasParameterAnnotatedWith(Class<A> annotation)
   {
      return transformToElements(ElementType.PARAMETER, annotation, AnnotatedElement.class);
   }

   /**
    * Load the annotation class.
    *
    * @param annotationName the annoation class name
    * @return annotation class
    */
   @SuppressWarnings("unchecked")
   protected Class<Annotation> getAnnotationClass(String annotationName)
   {
      Class<?> clazz = loadClass(annotationName);
      if (Annotation.class.isAssignableFrom(clazz) == false)
         throw new IllegalArgumentException("Annotation name " + annotationName + " doesn't extend Annotation class.");
      return (Class<Annotation>)clazz;
   }

   public boolean hasClassAnnotatedWith(String annotationName)
   {
      return hasClassAnnotatedWith(getAnnotationClass(annotationName));
   }

   public Set<Element<Annotation, Class<?>>> classIsAnnotatedWith(String annotationName)
   {
      return classIsAnnotatedWith(getAnnotationClass(annotationName));
   }

   public Set<Element<Annotation, Constructor<?>>> classHasConstructorAnnotatedWith(String annotationName)
   {
      return classHasConstructorAnnotatedWith(getAnnotationClass(annotationName));
   }

   public Set<Element<Annotation, Field>> classHasFieldAnnotatedWith(String annotationName)
   {
      return classHasFieldAnnotatedWith(getAnnotationClass(annotationName));
   }

   public Set<Element<Annotation, Method>> classHasMethodAnnotatedWith(String annotationName)
   {
      return classHasMethodAnnotatedWith(getAnnotationClass(annotationName));
   }

   public Set<Element<Annotation, AnnotatedElement>> classHasParameterAnnotatedWith(String annotationName)
   {
      return classHasParameterAnnotatedWith(getAnnotationClass(annotationName));
   }
}
