package org.infinispan.cdi.common.util.annotatedtypebuilder;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.enterprise.inject.spi.Annotated;
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 org.infinispan.cdi.common.util.Reflections;
import org.infinispan.cdi.common.util.logging.Log;
import org.infinispan.commons.logging.LogFactory;

/**
 * <p> Class for constructing a new AnnotatedType. A new instance of builder
 * should be used for each annotated type. </p> <p/> <p> {@link
 * AnnotatedTypeBuilder} is not thread-safe. </p>
 *
 * @author Stuart Douglas
 * @author Pete Muir
 * @see AnnotatedType
 */
public class AnnotatedTypeBuilder<X> {

   private static final Log log = LogFactory.getLog(AnnotatedTypeBuilder.class, Log.class);

   private Class<X> javaClass;
   private final AnnotationBuilder typeAnnotations;

   private final Map<Constructor<?>, AnnotationBuilder> constructors;
   private final Map<Constructor<?>, Map<Integer, AnnotationBuilder>> constructorParameters;
   private final Map<Constructor<?>, Map<Integer, Type>> constructorParameterTypes;

   private final Map<Field, AnnotationBuilder> fields;
   private final Map<Field, Type> fieldTypes;

   private final Map<Method, AnnotationBuilder> methods;
   private final Map<Method, Map<Integer, AnnotationBuilder>> methodParameters;
   private final Map<Method, Map<Integer, Type>> methodParameterTypes;

   /**
    * Create a new builder. A new builder has no annotations and no members.
    *
    * @see #readFromType(AnnotatedType)
    * @see #readFromType(Class)
    * @see #readFromType(AnnotatedType, boolean)
    * @see #readFromType(Class, boolean)
    */
   public AnnotatedTypeBuilder() {
      this.typeAnnotations = new AnnotationBuilder();
      this.constructors = new HashMap<Constructor<?>, AnnotationBuilder>();
      this.constructorParameters = new HashMap<Constructor<?>, Map<Integer, AnnotationBuilder>>();
      this.constructorParameterTypes = new HashMap<Constructor<?>, Map<Integer, Type>>();
      this.fields = new HashMap<Field, AnnotationBuilder>();
      this.fieldTypes = new HashMap<Field, Type>();
      this.methods = new HashMap<Method, AnnotationBuilder>();
      this.methodParameters = new HashMap<Method, Map<Integer, AnnotationBuilder>>();
      this.methodParameterTypes = new HashMap<Method, Map<Integer, Type>>();
   }

   /**
    * Add an annotation to the type declaration.
    *
    * @param annotation the annotation instance to add
    * @throws IllegalArgumentException if the annotation is null
    */
   public AnnotatedTypeBuilder<X> addToClass(Annotation annotation) {
      typeAnnotations.add(annotation);
      return this;
   }

   /**
    * Reads in from an existing AnnotatedType. Any elements not present are
    * added. The javaClass will be read in. If the annotation already exists on
    * that element in the builder the read annotation will be used.
    *
    * @param type the type to read from
    * @throws IllegalArgumentException if type is null
    */
   public AnnotatedTypeBuilder<X> readFromType(AnnotatedType<X> type) {
      return readFromType(type, true);
   }

   /**
    * Reads in from an existing AnnotatedType. Any elements not present are
    * added. The javaClass will be read in if overwrite is true. If the
    * annotation already exists on that element in the builder, overwrite
    * determines whether the original or read annotation will be used.
    *
    * @param type      the type to read from
    * @param overwrite if true, the read annotation will replace any existing
    *                  annotation
    * @throws IllegalArgumentException if type is null
    */
   public AnnotatedTypeBuilder<X> readFromType(AnnotatedType<X> type, boolean overwrite) {
      if (type == null) {
         throw log.parameterMustNotBeNull("type");
      }
      if (javaClass == null || overwrite) {
         this.javaClass = type.getJavaClass();
      }
      mergeAnnotationsOnElement(type, overwrite, typeAnnotations);
      for (AnnotatedField<? super X> field : type.getFields()) {
         if (fields.get(field.getJavaMember()) == null) {
            fields.put(field.getJavaMember(), new AnnotationBuilder());
         }
         mergeAnnotationsOnElement(field, overwrite, fields.get(field.getJavaMember()));
      }
      for (AnnotatedMethod<? super X> method : type.getMethods()) {
         if (methods.get(method.getJavaMember()) == null) {
            methods.put(method.getJavaMember(), new AnnotationBuilder());
         }
         mergeAnnotationsOnElement(method, overwrite, methods.get(method.getJavaMember()));
         for (AnnotatedParameter<? super X> p : method.getParameters()) {
            if (methodParameters.get(method.getJavaMember()) == null) {
               methodParameters.put(method.getJavaMember(), new HashMap<Integer, AnnotationBuilder>());
            }
            if (methodParameters.get(method.getJavaMember()).get(p.getPosition()) == null) {
               methodParameters.get(method.getJavaMember()).put(p.getPosition(), new AnnotationBuilder());
            }
            mergeAnnotationsOnElement(p, overwrite, methodParameters.get(method.getJavaMember()).get(p.getPosition()));
         }
      }
      for (AnnotatedConstructor<? super X> constructor : type.getConstructors()) {
         if (constructors.get(constructor.getJavaMember()) == null) {
            constructors.put(constructor.getJavaMember(), new AnnotationBuilder());
         }
         mergeAnnotationsOnElement(constructor, overwrite, constructors.get(constructor.getJavaMember()));
         for (AnnotatedParameter<? super X> p : constructor.getParameters()) {
            if (constructorParameters.get(constructor.getJavaMember()) == null) {
               constructorParameters.put(constructor.getJavaMember(), new HashMap<Integer, AnnotationBuilder>());
            }
            if (constructorParameters.get(constructor.getJavaMember()).get(p.getPosition()) == null) {
               constructorParameters.get(constructor.getJavaMember()).put(p.getPosition(), new AnnotationBuilder());
            }
            mergeAnnotationsOnElement(p, overwrite, constructorParameters.get(constructor.getJavaMember()).get(p.getPosition()));
         }
      }
      return this;
   }

   /**
    * Reads the annotations from an existing java type. Annotations already
    * present will be overwritten
    *
    * @param type the type to read from
    * @throws IllegalArgumentException if type is null
    */
   public AnnotatedTypeBuilder<X> readFromType(Class<X> type) {
      return readFromType(type, true);
   }

   /**
    * Reads the annotations from an existing java type. If overwrite is true
    * then existing annotations will be overwritten
    *
    * @param type      the type to read from
    * @param overwrite if true, the read annotation will replace any existing
    *                  annotation
    */
   public AnnotatedTypeBuilder<X> readFromType(Class<X> type, boolean overwrite) {
      if (type == null) {
         throw log.parameterMustNotBeNull("type");
      }
      if (javaClass == null || overwrite) {
         this.javaClass = type;
      }
      for (Annotation annotation : type.getAnnotations()) {
         if (overwrite || !typeAnnotations.isAnnotationPresent(annotation.annotationType())) {
            typeAnnotations.add(annotation);
         }
      }

      for (Field field : Reflections.getAllDeclaredFields(type)) {
         AnnotationBuilder annotationBuilder = fields.get(field);
         if (annotationBuilder == null) {
            annotationBuilder = new AnnotationBuilder();
            fields.put(field, annotationBuilder);
         }
         field.setAccessible(true);
         for (Annotation annotation : field.getAnnotations()) {
            if (overwrite || !annotationBuilder.isAnnotationPresent(annotation.annotationType())) {
               annotationBuilder.add(annotation);
            }
         }
      }

      for (Method method : Reflections.getAllDeclaredMethods(type)) {
         AnnotationBuilder annotationBuilder = methods.get(method);
         if (annotationBuilder == null) {
            annotationBuilder = new AnnotationBuilder();
            methods.put(method, annotationBuilder);
         }
         method.setAccessible(true);
         for (Annotation annotation : method.getAnnotations()) {
            if (overwrite || !annotationBuilder.isAnnotationPresent(annotation.annotationType())) {
               annotationBuilder.add(annotation);
            }
         }

         Map<Integer, AnnotationBuilder> parameters = methodParameters.get(method);
         if (parameters == null) {
            parameters = new HashMap<Integer, AnnotationBuilder>();
            methodParameters.put(method, parameters);
         }
         for (int i = 0; i < method.getParameterTypes().length; ++i) {
            AnnotationBuilder parameterAnnotationBuilder = parameters.get(i);
            if (parameterAnnotationBuilder == null) {
               parameterAnnotationBuilder = new AnnotationBuilder();
               parameters.put(i, parameterAnnotationBuilder);
            }
            for (Annotation annotation : method.getParameterAnnotations()[i]) {
               if (overwrite || !parameterAnnotationBuilder.isAnnotationPresent(annotation.annotationType())) {
                  parameterAnnotationBuilder.add(annotation);
               }
            }
         }
      }

      for (Constructor<?> constructor : type.getDeclaredConstructors()) {
         AnnotationBuilder annotationBuilder = constructors.get(constructor);
         if (annotationBuilder == null) {
            annotationBuilder = new AnnotationBuilder();
            constructors.put(constructor, annotationBuilder);
         }
         constructor.setAccessible(true);
         for (Annotation annotation : constructor.getAnnotations()) {
            if (overwrite || !annotationBuilder.isAnnotationPresent(annotation.annotationType())) {
               annotationBuilder.add(annotation);
            }
         }
         Map<Integer, AnnotationBuilder> mparams = constructorParameters.get(constructor);
         if (mparams == null) {
            mparams = new HashMap<Integer, AnnotationBuilder>();
            constructorParameters.put(constructor, mparams);
         }
         for (int i = 0; i < constructor.getParameterTypes().length; ++i) {
            AnnotationBuilder parameterAnnotationBuilder = mparams.get(i);
            if (parameterAnnotationBuilder == null) {
               parameterAnnotationBuilder = new AnnotationBuilder();
               mparams.put(i, parameterAnnotationBuilder);
            }
            for (Annotation annotation : constructor.getParameterAnnotations()[i]) {
               if (overwrite || !parameterAnnotationBuilder.isAnnotationPresent(annotation.annotationType())) {
                  annotationBuilder.add(annotation);
               }
            }
         }
      }
      return this;
   }

   protected void mergeAnnotationsOnElement(Annotated annotated, boolean overwriteExisting, AnnotationBuilder typeAnnotations) {
      for (Annotation annotation : annotated.getAnnotations()) {
         if (typeAnnotations.getAnnotation(annotation.annotationType()) != null) {
            if (overwriteExisting) {
               typeAnnotations.remove(annotation.annotationType());
               typeAnnotations.add(annotation);
            }
         } else {
            typeAnnotations.add(annotation);
         }
      }
   }

   /**
    * Create an {@link AnnotatedType}. Any public members present on the
    * underlying class and not overridden by the builder will be automatically
    * added.
    */
   public AnnotatedType<X> create() {
      Map<Constructor<?>, Map<Integer, AnnotationStore>> constructorParameterAnnnotations = new HashMap<Constructor<?>, Map<Integer, AnnotationStore>>();
      Map<Constructor<?>, AnnotationStore> constructorAnnotations = new HashMap<Constructor<?>, AnnotationStore>();
      Map<Method, Map<Integer, AnnotationStore>> methodParameterAnnnotations = new HashMap<Method, Map<Integer, AnnotationStore>>();
      Map<Method, AnnotationStore> methodAnnotations = new HashMap<Method, AnnotationStore>();
      Map<Field, AnnotationStore> fieldAnnotations = new HashMap<Field, AnnotationStore>();

      for (Entry<Field, AnnotationBuilder> field : fields.entrySet()) {
         fieldAnnotations.put(field.getKey(), field.getValue().create());
      }

      for (Entry<Method, AnnotationBuilder> method : methods.entrySet()) {
         methodAnnotations.put(method.getKey(), method.getValue().create());
      }
      for (Entry<Method, Map<Integer, AnnotationBuilder>> parameters : methodParameters.entrySet()) {
         Map<Integer, AnnotationStore> parameterAnnotations = new HashMap<Integer, AnnotationStore>();
         methodParameterAnnnotations.put(parameters.getKey(), parameterAnnotations);
         for (Entry<Integer, AnnotationBuilder> parameter : parameters.getValue().entrySet()) {
            parameterAnnotations.put(parameter.getKey(), parameter.getValue().create());
         }
      }

      for (Entry<Constructor<?>, AnnotationBuilder> constructor : constructors.entrySet()) {
         constructorAnnotations.put(constructor.getKey(), constructor.getValue().create());
      }
      for (Entry<Constructor<?>, Map<Integer, AnnotationBuilder>> parameters : constructorParameters.entrySet()) {
         Map<Integer, AnnotationStore> parameterAnnotations = new HashMap<Integer, AnnotationStore>();
         constructorParameterAnnnotations.put(parameters.getKey(), parameterAnnotations);
         for (Entry<Integer, AnnotationBuilder> parameter : parameters.getValue().entrySet()) {
            parameterAnnotations.put(parameter.getKey(), parameter.getValue().create());
         }
      }

      return new AnnotatedTypeImpl<X>(javaClass, typeAnnotations.create(), fieldAnnotations, methodAnnotations, methodParameterAnnnotations, constructorAnnotations, constructorParameterAnnnotations, fieldTypes, methodParameterTypes, constructorParameterTypes);
   }

   /**
    * Remove an annotation from the type
    *
    * @param annotationType the annotation type to remove
    * @throws IllegalArgumentException if the annotationType
    */
   public AnnotatedTypeBuilder<X> removeFromClass(Class<? extends Annotation> annotationType) {
       typeAnnotations.remove(annotationType);
       return this;
   }

   /**
    * Remove an annotation from the specified method.
    *
    * @param method         the method to remove the annotation from
    * @param annotationType the annotation type to remove
    * @throws IllegalArgumentException if the annotationType is null or if the
    *                                  method is not currently declared on the type
    */
   public AnnotatedTypeBuilder<X> removeFromMethod(Method method, Class<? extends Annotation> annotationType) {
       if (methods.get(method) == null) {
           throw new IllegalArgumentException("Method not present " + method.toString() + " on " + javaClass);
       } else {
           methods.get(method).remove(annotationType);
       }
       return this;
   }

   /**
    * Remove an annotation from the specified method.
    *
    * @param method         the method to remove the annotation from
    * @param annotationType the annotation type to remove
    * @throws IllegalArgumentException if the annotationType is null or if the
    *                                  method is not currently declared on the type
    */
   public AnnotatedTypeBuilder<X> removeFromMethod(AnnotatedMethod<? super X> method, Class<? extends Annotation> annotationType) {
       return removeFromMethod(method.getJavaMember(), annotationType);
   }

   /**
    * Add an annotation to the specified method. If the method is not already
    * present, it will be added.
    *
    * @param method     the method to add the annotation to
    * @param annotation the annotation to add
    * @throws IllegalArgumentException if the annotation is null
    */
   public AnnotatedTypeBuilder<X> addToMethod(Method method, Annotation annotation) {
       if (methods.get(method) == null) {
           methods.put(method, new AnnotationBuilder());
       }
       methods.get(method).add(annotation);
       return this;
   }

   /**
    * Add an annotation to the specified method. If the method is not already
    * present, it will be added.
    *
    * @param method     the method to add the annotation to
    * @param annotation the annotation to add
    * @throws IllegalArgumentException if the annotation is null
    */
   public AnnotatedTypeBuilder<X> addToMethod(AnnotatedMethod<? super X> method, Annotation annotation) {
       return addToMethod(method.getJavaMember(), annotation);
   }

   /**
    * Add an annotation to the specified method parameter. If the method is not
    * already present, it will be added. If the method parameter is not already
    * present, it will be added.
    *
    * @param method     the method to add the annotation to
    * @param position   the position of the parameter to add
    * @param annotation the annotation to add
    * @throws IllegalArgumentException if the annotation is null
    */
   public AnnotatedTypeBuilder<X> addToMethodParameter(Method method, int position, Annotation annotation) {
       if (!methods.containsKey(method)) {
           methods.put(method, new AnnotationBuilder());
       }
       if (methodParameters.get(method) == null) {
           methodParameters.put(method, new HashMap<Integer, AnnotationBuilder>());
       }
       if (methodParameters.get(method).get(position) == null) {
           methodParameters.get(method).put(position, new AnnotationBuilder());
       }
       methodParameters.get(method).get(position).add(annotation);
       return this;
   }

   /**
    * Remove an annotation from the specified method parameter.
    *
    * @param method         the method to remove the annotation from
    * @param position       the position of the parameter to remove
    * @param annotationType the annotation type to remove
    * @throws IllegalArgumentException if the annotationType is null, if the
    *                                  method is not currently declared on the type or if the
    *                                  parameter is not declared on the method
    */
   public AnnotatedTypeBuilder<X> removeFromMethodParameter(Method method, int position, Class<? extends Annotation> annotationType) {
       if (methods.get(method) == null) {
           throw new IllegalArgumentException("Method not present " + method +  " on " + javaClass);
       } else {
           if (methodParameters.get(method).get(position) == null) {
               throw new IllegalArgumentException("Method parameter " + position + " not present on " + method + " on " + javaClass);
           } else {
               methodParameters.get(method).get(position).remove(annotationType);
           }
       }
       return this;
   }

   /**
    * Add an annotation to the specified field. If the field is not already
    * present, it will be added.
    *
    * @param field      the field to add the annotation to
    * @param annotation the annotation to add
    * @throws IllegalArgumentException if the annotation is null
    */
   public AnnotatedTypeBuilder<X> addToField(Field field, Annotation annotation) {
       if (fields.get(field) == null) {
           fields.put(field, new AnnotationBuilder());
       }
       fields.get(field).add(annotation);
       return this;
   }

   /**
    * Add an annotation to the specified field. If the field is not already
    * present, it will be added.
    *
    * @param field      the field to add the annotation to
    * @param annotation the annotation to add
    * @throws IllegalArgumentException if the annotation is null
    */
   public AnnotatedTypeBuilder<X> addToField(AnnotatedField<? super X> field, Annotation annotation) {
       return addToField(field.getJavaMember(), annotation);
   }

   /**
    * Remove an annotation from the specified field.
    *
    * @param field          the field to remove the annotation from
    * @param annotationType the annotation type to remove
    * @throws IllegalArgumentException if the annotationType is null or if the
    *                                  field is not currently declared on the type
    */
   public AnnotatedTypeBuilder<X> removeFromField(Field field, Class<? extends Annotation> annotationType) {
       if (fields.get(field) == null) {
           throw new IllegalArgumentException("Field not present " + field + " on " + javaClass);
       } else {
           fields.get(field).remove(annotationType);
       }
       return this;
   }

   /**
    * Remove an annotation from the specified field.
    *
    * @param field          the field to remove the annotation from
    * @param annotationType the annotation type to remove
    * @throws IllegalArgumentException if the annotationType is null or if the
    *                                  field is not currently declared on the type
    */
   public AnnotatedTypeBuilder<X> removeFromField(AnnotatedField<? super X> field, Class<? extends Annotation> annotationType) {
       return removeFromField(field.getJavaMember(), annotationType);
   }

}
