/*
 * Copyright 2012 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Eclipse Public License version 1.0, available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.jboss.forge.bus.cdi;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.AnnotatedCallable;
import javax.enterprise.inject.spi.AnnotatedConstructor;
import javax.enterprise.inject.spi.AnnotatedField;
import javax.enterprise.inject.spi.AnnotatedMethod;
import javax.enterprise.inject.spi.AnnotatedParameter;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.inject.spi.Extension;
import javax.enterprise.inject.spi.ProcessAnnotatedType;
import javax.inject.Singleton;

import org.jboss.forge.bus.event.BusEvent;
import org.jboss.forge.bus.util.Annotations;

/**
 * @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
 */
@Singleton
@SuppressWarnings({ "rawtypes", "unchecked" })
public class ObserverCaptureExtension implements Extension
{
   private final Map<Class<?>, List<BusManaged>> eventQualifierMap = new HashMap<Class<?>, List<BusManaged>>();
   private int rollingIdentifier = 0;

   public <T> void scan(@Observes final ProcessAnnotatedType<T> event)
   {
      AnnotatedType<Object> originalType = (AnnotatedType<Object>) event.getAnnotatedType();
      if (isAnnotated(originalType, Observes.class)) {
          AnnotatedType<Object> newType;
          List<AnnotatedMethod> obsoleteMethods = new ArrayList<AnnotatedMethod>();
          List<AnnotatedMethod> replacementMethods = new ArrayList<AnnotatedMethod>();
    
          for (AnnotatedMethod<?> method : getOrderedMethods(originalType))
          {
             for (AnnotatedParameter<?> param : method.getParameters())
             {
                if (param.isAnnotationPresent(Observes.class))
                {
                   Set<Type> typeClosure = param.getTypeClosure();
                   for (Type type : typeClosure) {
                      if (type instanceof Class)
                      {
                         if (Annotations.isAnnotationPresent((Class<?>) type, BusEvent.class))
                         {
                            replacementMethods.add(qualifyObservedEvent(method, param));
                            obsoleteMethods.add(method);
                            break;
                         }
                      }
                      else if (type instanceof ParameterizedType)
                      {
                         Type rawType = ((ParameterizedType) type).getRawType();
                         if (rawType instanceof Class)
                         {
                            if (Annotations.isAnnotationPresent((Class<?>) rawType, (BusEvent.class)))
                            {
                               replacementMethods.add(qualifyObservedEvent(method, param));
                               obsoleteMethods.add(method);
                               break;
                            }
                         }
                      }
                   }
                }
             }
          }
    
          newType = removeMethodsFromType(originalType, obsoleteMethods);
          newType = addReplacementMethodsToType(newType, replacementMethods);
    
          event.setAnnotatedType((AnnotatedType<T>) newType);
      } else {
          event.setAnnotatedType((AnnotatedType<T>) originalType);
      }
   }

   private boolean isAnnotated(AnnotatedType<?> type, Class<? extends Annotation> annotation) {
       for (AnnotatedMethod<?> m : type.getMethods()) {
           for (AnnotatedParameter<?> p : m.getParameters()) {
               if (p.getAnnotation(annotation) != null)
                   return true;
           }
       }
       return false;
   }

   private List<AnnotatedMethod<? super Object>> getOrderedMethods(final AnnotatedType<Object> originalType)
   {
      Set<AnnotatedMethod<? super Object>> methods = originalType.getMethods();

      List<AnnotatedMethod<? super Object>> result = new ArrayList<AnnotatedMethod<? super Object>>();
      result.addAll(methods);

      Collections.sort(result, new Comparator<Object>()
      {
         @Override
         public int compare(final Object left, final Object right)
         {
            String lid = ((AnnotatedMethod<? super Object>) left).getJavaMember().toGenericString();
            String rid = ((AnnotatedMethod<? super Object>) right).getJavaMember().toGenericString();
            return lid.compareTo(rid);
         }
      });

      return result;
   }

   private AnnotatedType<Object> removeMethodsFromType(final AnnotatedType type,
            final List<AnnotatedMethod> targetedMethods)
   {

      final Set<AnnotatedMethod> methods = new HashSet<AnnotatedMethod>();
      methods.addAll(type.getMethods());
      methods.removeAll(targetedMethods);

      return new AnnotatedType()
      {
         @Override
         public Class getJavaClass()
         {
            return type.getJavaClass();
         }

         @Override
         public Set<AnnotatedConstructor> getConstructors()
         {
            return type.getConstructors();
         }

         @Override
         public Set<AnnotatedMethod> getMethods()
         {
            return methods;
         }

         @Override
         public Set<AnnotatedField> getFields()
         {
            return type.getFields();
         }

         @Override
         public Type getBaseType()
         {
            return type.getBaseType();
         }

         @Override
         public Set<Type> getTypeClosure()
         {
            return type.getTypeClosure();
         }

         @Override
         public <T extends Annotation> T getAnnotation(final Class<T> annotationType)
         {
            return type.getAnnotation(annotationType);
         }

         @Override
         public Set<Annotation> getAnnotations()
         {
            return type.getAnnotations();
         }

         @Override
         public boolean isAnnotationPresent(final Class<? extends Annotation> annotationType)
         {
            return type.isAnnotationPresent(annotationType);
         }
      };
   }

   private AnnotatedType<Object> addReplacementMethodsToType(final AnnotatedType newType,
            final List<AnnotatedMethod> replacementMethods)
   {
      newType.getMethods().addAll(replacementMethods);
      return newType;
   }

   private AnnotatedMethod<Object> qualifyObservedEvent(
            final AnnotatedMethod method, final AnnotatedParameter param)
   {
      final List<AnnotatedParameter> parameters = new ArrayList<AnnotatedParameter>();

      parameters.addAll(method.getParameters());
      parameters.set(parameters.indexOf(param), addUniqueQualifier(method, param));
      parameters.remove(param);

      return new AnnotatedMethod()
      {
         @Override
         public List<AnnotatedParameter> getParameters()
         {
            return parameters;
         }

         @Override
         public AnnotatedType<Object> getDeclaringType()
         {
            return method.getDeclaringType();
         }

         @Override
         public boolean isStatic()
         {
            return method.isStatic();
         }

         @Override
         public <T extends Annotation> T getAnnotation(final Class<T> annotation)
         {
            return method.getAnnotation(annotation);
         }

         @Override
         public Set<Annotation> getAnnotations()
         {
            return method.getAnnotations();
         }

         @Override
         public Type getBaseType()
         {
            return method.getBaseType();
         }

         @Override
         public Set<Type> getTypeClosure()
         {
            return method.getTypeClosure();
         }

         @Override
         public boolean isAnnotationPresent(final Class<? extends Annotation> annotation)
         {
            return method.isAnnotationPresent(annotation);
         }

         @Override
         public Method getJavaMember()
         {
            return method.getJavaMember();
         }
      };
   }

   private AnnotatedParameter addUniqueQualifier(final AnnotatedMethod method,
            final AnnotatedParameter param)
   {
      final String identifier = String.valueOf(rollingIdentifier++);
      final String methodName = method.getJavaMember().getName();
      final BusManaged qualifier = new BusManaged()
      {
         @Override
         public Class<? extends Annotation> annotationType()
         {
            return BusManaged.class;
         }

         @Override
         public String value()
         {
            return identifier;
         }

         @Override
         public String method()
         {
            return methodName;
         }
      };

      addQualifierToMap(method, param, qualifier);

      final Set<Annotation> annotations = new HashSet<Annotation>();
      annotations.addAll(param.getAnnotations());
      annotations.add(qualifier);

      return new AnnotatedParameter<Object>()
      {
         @Override
         public <T extends Annotation> T getAnnotation(final Class<T> clazz)
         {
            if (BusManaged.class.isAssignableFrom(clazz))
            {
               return (T) qualifier;
            }
            return param.getAnnotation(clazz);
         }

         @Override
         public Set<Annotation> getAnnotations()
         {
            return annotations;
         }

         @Override
         public Type getBaseType()
         {
            return param.getBaseType();
         }

         @Override
         public Set<Type> getTypeClosure()
         {
            return param.getTypeClosure();
         }

         @Override
         public boolean isAnnotationPresent(final Class<? extends Annotation> clazz)
         {
            if (BusManaged.class.isAssignableFrom(clazz))
            {
               return true;
            }
            return param.isAnnotationPresent(clazz);
         }

         @Override
         public AnnotatedCallable<Object> getDeclaringCallable()
         {
            return param.getDeclaringCallable();
         }

         @Override
         public int getPosition()
         {
            return param.getPosition();
         }
      };
   }

   private void addQualifierToMap(final AnnotatedMethod annotatedMethod,
            final AnnotatedParameter param, final BusManaged qualifier)
   {
      Method method = annotatedMethod.getJavaMember();
      Class<?> clazz = method.getParameterTypes()[param.getPosition()];
      List<BusManaged> qualifiers = eventQualifierMap.get(clazz);
      if (qualifiers == null)
      {
         qualifiers = new ArrayList<BusManaged>();
      }
      qualifiers.add(qualifier);
      eventQualifierMap.put(clazz, qualifiers);
   }

   /**
    * Return a list containing instances of {@link BusManaged} annotations corresponding to the given event type.
    */
   public List<BusManaged> getEventQualifiers(final Class<?> clazz)
   {
      List<BusManaged> result = new ArrayList<BusManaged>();
      for (Entry<Class<?>, List<BusManaged>> entry : eventQualifierMap.entrySet())
      {
         Class<?> key = entry.getKey();
         List<BusManaged> value = entry.getValue();
         if (key.isAssignableFrom(clazz))
         {
            result.addAll(value);
         }
      }
      return result;
   }

   /**
    * Return the entire map of Event Types and their corresponding lists of {@link BusManaged} annotation instances.
    * This map can be used to implement a strategy for custom firing of events.
    */
   public Map<Class<?>, List<BusManaged>> getEventQualifierMap()
   {
      return eventQualifierMap;
   }
}
