/*
* 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.kernel.plugins.annotations;

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jboss.beans.info.spi.BeanInfo;
import org.jboss.beans.info.spi.PropertyInfo;
import org.jboss.beans.metadata.api.annotations.MCAnnotations;
import org.jboss.logging.Logger;
import org.jboss.metadata.spi.MetaData;
import org.jboss.metadata.spi.signature.ConstructorSignature;
import org.jboss.metadata.spi.signature.DeclaredMethodSignature;
import org.jboss.metadata.spi.signature.FieldSignature;
import org.jboss.metadata.spi.signature.Signature;
import org.jboss.reflect.spi.AnnotatedInfo;
import org.jboss.reflect.spi.ClassInfo;
import org.jboss.reflect.spi.ConstructorInfo;
import org.jboss.reflect.spi.FieldInfo;
import org.jboss.reflect.spi.MethodInfo;
import org.jboss.reflect.spi.TypeInfo;
import org.jboss.reflect.spi.TypeInfoFactory;

/**
 * Common bean annotation handler.
 *
 * @param <T> exact annotation plugin type
 * @param <U> exact handle type
 * @author <a href="mailto:ales.justin@jboss.com">Ales Justin</a>
 */
public abstract class CommonAnnotationAdapter<T extends MetaDataAnnotationPlugin<?, ?>, U>
{
   /** The log */
   protected Logger log = Logger.getLogger(getClass());

   /** The annotation plugins */
   private final Map<ElementType, Set<T>> pluginsMap = new HashMap<ElementType, Set<T>>();

   /** The property annotation plugin filter */
   private static final AnnotationPluginFilter PROPERTY_FILTER = new PropertyAnnotationPluginFilter();

   /** The method annotation plugin filter */
   private static final AnnotationPluginFilter METHOD_FILTER = new MethodAnnotationPluginFilter();

   /**
    * Add the annotation plugin.
    * Breaks down the plugin usage into
    * different ElementType support collections.
    *
    * @param plugin the annotation plugin
    */
   public void addAnnotationPlugin(T plugin)
   {
      if (plugin == null)
         throw new IllegalArgumentException("Null plugin.");

      Class<? extends Annotation> annotation = plugin.getAnnotation();
      if (annotation == null)
         throw new IllegalArgumentException("Null annotation class: " + plugin);

      if (annotation.getAnnotation(Target.class) == null)
         log.warn("Annotation " + annotation + " missing @Target annotation!");
      if (annotation.getAnnotation(Retention.class) == null)
         log.warn("Annotation " + annotation + " missing @Retention annotation!");

      Set<ElementType> supported = plugin.getSupportedTypes();
      if (supported == null || supported.isEmpty())
         throw new IllegalArgumentException("Null or empty support types: " + plugin);

      synchronized (pluginsMap)
      {
         for (ElementType type : supported)
         {
            Set<T> plugins = pluginsMap.get(type);
            if (plugins == null)
            {
               plugins = new HashSet<T>();
               pluginsMap.put(type, plugins);
            }
            plugins.add(plugin);
         }
      }
   }

   /**
    * Remove the plugin.
    *
    * @param plugin the annotation plugin
    */
   public void removeAnnotationPlugin(T plugin)
   {
      if (plugin == null)
         return;

      Set<ElementType> supported = plugin.getSupportedTypes();
      if (supported == null || supported.isEmpty())
         throw new IllegalArgumentException("Null or empty support types: " + plugin);

      synchronized (pluginsMap)
      {
         for (ElementType type : supported)
         {
            Set<T> plugins = pluginsMap.get(type);
            if (plugins != null)
            {
               plugins.remove(plugin);

               if (plugins.isEmpty())
                  pluginsMap.remove(type);
            }
         }
      }
   }

   /**
    * Apply plugin.
    *
    * @param plugin the plugin
    * @param info the bean info
    * @param retrieval the metadata
    * @param handle the handle
    * @throws Throwable for any error
    */
   protected abstract void applyPlugin(T plugin, AnnotatedInfo info, MetaData retrieval, U handle) throws Throwable;

   /**
    * Clean plugin.
    *
    * @param plugin the plugin
    * @param info the bean info
    * @param retrieval the metadata
    * @param handle the handle
    * @throws Throwable for any error
    */
   protected abstract void cleanPlugin(T plugin, AnnotatedInfo info, MetaData retrieval, U handle) throws Throwable;

   /**
    * Get the name from handle.
    *
    * @param handle the handle
    * @return handle's name
    */
   protected abstract Object getName(U handle);

   /**
    * Get plugins.
    *
    * @param type the element type to match
    * @param filter possible plugins filter
    * @param annotationClasses possible annotations
    * @return iterable matching plugins
    */
   protected Iterable<T> getPlugins(ElementType type, AnnotationPluginFilter filter, Collection<Class<? extends Annotation>> annotationClasses)
   {
      Set<T> plugins;
      synchronized (pluginsMap)
      {
         plugins = pluginsMap.get(type);
      }
      
      if (plugins == null || plugins.isEmpty())
         return Collections.emptySet();

      if (filter != null)
      {
         List<T> result = new ArrayList<T>();
         for (T plugin : plugins)
         {
            if (filter.accept(plugin) && (annotationClasses == null || annotationClasses.contains(plugin.getAnnotation())))
            {
               result.add(plugin);
            }
         }
         return result;
      }
      else if (annotationClasses != null)
      {
         List<T> result = new ArrayList<T>();
         for (T plugin : plugins)
         {
            if (annotationClasses.contains(plugin.getAnnotation()))
            {
               result.add(plugin);
            }
         }
         return result;
      }
      else
      {
         return plugins;
      }
   }

   /**
    * Handle apply or cleanup of annotations.
    *
    * @param info the bean info
    * @param retrieval the metadata
    * @param handle the handle to use in a plugin
    * @param isApplyPhase is this apply phase
    * @throws Throwable for any error
    */
   protected void handleAnnotations(BeanInfo info, MetaData retrieval, U handle, boolean isApplyPhase) throws Throwable
   {
      if (info == null)
         throw new IllegalArgumentException("Null bean info.");
      if (retrieval == null)
         throw new IllegalArgumentException("Null metadata.");
      if (handle == null)
         throw new IllegalArgumentException("Null handle.");

      boolean trace = log.isTraceEnabled();
      if (trace)
         log.trace(getName(handle) + " apply annotations");

      // limit the annotations
      MCAnnotations annotations = retrieval.getAnnotation(MCAnnotations.class);
      if (annotations != null && (annotations.ignore() || annotations.value().length == 0))
      {
         if (trace)
            log.trace("Ignoring annotations lookup: " + annotations);

         return;
      }

      Collection<Class<? extends Annotation>> annotationClasses = (annotations != null ? Arrays.asList(annotations.value()) : null);

      // class
      ClassInfo classInfo = info.getClassInfo();
      for(T plugin : getPlugins(ElementType.TYPE, null, annotationClasses))
      {
         if (isApplyPhase)
            applyPlugin(plugin, classInfo, retrieval, handle);
         else
            cleanPlugin(plugin, classInfo, retrieval, handle);
      }

      // constructors
      Set<ConstructorInfo> constructors = info.getConstructors();
      if (constructors != null && constructors.isEmpty() == false)
      {
         for(ConstructorInfo ci : constructors)
         {
            Signature cis = new ConstructorSignature(ci);
            MetaData cmdr = retrieval.getComponentMetaData(cis);
            if (cmdr != null)
            {
               for(T plugin : getPlugins(ElementType.CONSTRUCTOR, null, annotationClasses))
               {
                  if (isApplyPhase)
                     applyPlugin(plugin, ci, cmdr, handle);
                  else
                     cleanPlugin(plugin, ci, cmdr, handle);
               }
            }
            else if (trace)
               log.trace("No annotations for " + ci);
         }
      }
      else if (trace)
         log.trace("No constructors");

      // properties
      Set<MethodInfo> visitedMethods = new HashSet<MethodInfo>();
      Set<PropertyInfo> properties = info.getProperties();
      if (properties != null && properties.isEmpty() == false)
      {
         for(PropertyInfo pi : properties)
         {
            FieldInfo field = pi.getFieldInfo();
            if (field != null)
            {
               Signature sis = new FieldSignature(field);
               MetaData cmdr = retrieval.getComponentMetaData(sis);
               if (cmdr != null)
               {
                  for(T plugin : getPlugins(ElementType.FIELD, null, annotationClasses))
                  {
                     if (isApplyPhase)
                        applyPlugin(plugin, field, cmdr, handle);
                     else
                        cleanPlugin(plugin, field, cmdr, handle);
                  }
               }
               else if (trace)
                  log.trace("No annotations for field " + field.getName());
            }
            // apply setter and getter as well - if they exist
            handleMethod(retrieval, handle, isApplyPhase, trace, visitedMethods, pi, pi.getSetter(), "setter", annotationClasses);
            handleMethod(retrieval, handle, isApplyPhase, trace, visitedMethods, pi, pi.getGetter(), "getter", annotationClasses);
         }
      }
      else if (trace)
         log.trace("No properties");

      // get Object's class info - it's cached so it shouldn't take much
      TypeInfoFactory tif = classInfo.getTypeInfoFactory();
      TypeInfo objectTI = tif.getTypeInfo(Object.class);

      // method plugins
      Iterable<T> methodPlugins = null;

      // methods
      Set<MethodInfo> methods = info.getMethods();
      if (methods != null && methods.isEmpty() == false)
      {
         for(MethodInfo mi : methods)
         {
            ClassInfo declaringCI = mi.getDeclaringClass();
            // direct == check is OK
            if (declaringCI != objectTI && visitedMethods.contains(mi) == false)
            {
               Signature mis = new DeclaredMethodSignature(mi);
               MetaData cmdr = retrieval.getComponentMetaData(mis);
               if (cmdr != null)
               {
                  if (methodPlugins == null)
                     methodPlugins = getPlugins(ElementType.METHOD, METHOD_FILTER, annotationClasses);
                  
                  for(T plugin : methodPlugins)
                  {
                     if (isApplyPhase)
                        applyPlugin(plugin, mi, cmdr, handle);
                     else
                        cleanPlugin(plugin, mi, cmdr, handle);
                  }
               }
               else if (trace)
                  log.trace("No annotations for " + mi);
            }
         }
      }
      else if (trace)
         log.trace("No methods");

      // static methods
      MethodInfo[] staticMethods = getStaticMethods(classInfo);
      if (staticMethods != null && staticMethods.length != 0)
      {
         for(MethodInfo smi : staticMethods)
         {
            if (smi.isStatic() && smi.isPublic())
            {
               Signature mis = new DeclaredMethodSignature(smi);
               MetaData cmdr = retrieval.getComponentMetaData(mis);
               if (cmdr != null)
               {
                  if (methodPlugins == null)
                     methodPlugins = getPlugins(ElementType.METHOD, METHOD_FILTER, annotationClasses);
                  
                  for(T plugin : methodPlugins)
                  {
                     if (isApplyPhase)
                        applyPlugin(plugin, smi, cmdr, handle);
                     else
                        cleanPlugin(plugin, smi, cmdr, handle);
                  }
               }
               else if (trace)
                  log.trace("No annotations for " + smi);
            }
         }
      }
      else if (trace)
         log.trace("No static methods");

      // fields - if accessible - are already handled with propertys
   }

   /**
    * Handle setter or getter on property.
    *
    * @param retrieval the metadata
    * @param handle the handle
    * @param isApplyPhase is apply phase
    * @param trace is trace enabled
    * @param visitedMethods visited methods
    * @param pi the property info
    * @param method the method info
    * @param type method type
    * @param annotationClasses the possible annotation classes
    * @throws Throwable for any error
    */
   protected void handleMethod(
         MetaData retrieval,
         U handle,
         boolean isApplyPhase,
         boolean trace,
         Set<MethodInfo> visitedMethods,
         PropertyInfo pi,
         MethodInfo method,
         String type,
         Collection<Class<? extends Annotation>> annotationClasses)
         throws Throwable
   {
      if (method == null)
         return;
      
      visitedMethods.add(method);
      Signature sis = new DeclaredMethodSignature(method);
      MetaData cmdr = retrieval.getComponentMetaData(sis);
      if (cmdr != null)
      {
         for(T plugin : getPlugins(ElementType.METHOD, PROPERTY_FILTER, annotationClasses))
         {
            if (isApplyPhase)
               applyPlugin(plugin, pi, cmdr, handle);
            else
               cleanPlugin(plugin, pi, cmdr, handle);
         }
      }
      else if (trace)
         log.trace("No annotations for " + type + ": " + pi.getName());
   }

   /**
    * Get the static methods of class info.
    *
    * @param classInfo the class info
    * @return the static methods
    */
   protected MethodInfo[] getStaticMethods(ClassInfo classInfo)
   {
      return classInfo.getDeclaredMethods();
   }
}