package org.reflection_no_reflection.processor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedOptions;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeMirror;
import org.reflection_no_reflection.Annotation;
import org.reflection_no_reflection.Class;
import org.reflection_no_reflection.Constructor;
import org.reflection_no_reflection.Field;
import org.reflection_no_reflection.GenericDeclarationImpl;
import org.reflection_no_reflection.Method;
import org.reflection_no_reflection.TypeVariable;
import org.reflection_no_reflection.TypeVariableImpl;

/**
 * An annotation processor that detects classes that need to receive injections.
 * It is a {@link AbstractProcessor} that can be triggered for all kinds of annotations.
 * It will create a RNR database of annotated fields, methods and constuctors.
 *
 * @author SNI
 */
@SupportedOptions({"targetAnnotatedClasses"})
public class Processor extends AbstractProcessor {
    /** Contains all classes that contain annotations. */
    private HashSet<Class> annotatedClassSet = new HashSet<>();
    /** Maps annotation type to classes that contain this annotation. */
    private Map<Class<? extends Annotation>, Set<Class<?>>> mapAnnotationTypeToClassContainingAnnotation = new HashMap<>();

    //annotation processing options
    private Set<String> targetAnnotatedClasses = new HashSet<>();
    private int maxLevel = 0;
    private Set<Class> annotationClasses = new HashSet<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        String annotatedClassesString = processingEnv.getOptions().get("annotatedClasses");
        if (annotatedClassesString != null) {
            targetAnnotatedClasses.addAll(Arrays.asList(annotatedClassesString.split(",")));
        }
        Class.clearAllClasses();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // Not sure why, but sometimes we're getting called with an empty list of annotations.
        if (annotations.isEmpty() || roundEnv.processingOver()) {
            return true;
        }

        int level = 0;
        for (TypeElement annotation : annotations) {
            for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
                mapElementToReflection(element, level);
            }
        }

        return true;
    }

    private void mapElementToReflection(Element element, int level) {
        if (element.getEnclosingElement() instanceof TypeElement && element instanceof VariableElement) {
            addFieldToAnnotationDatabase(element, level);
        } else if (element.getEnclosingElement() instanceof ExecutableElement && element instanceof VariableElement) {
            addParameterToAnnotationDatabase(element, level);
        } else if (element instanceof ExecutableElement) {
            addMethodOrConstructorToAnnotationDatabase((ExecutableElement) element, level);
        } else if (element instanceof TypeElement) {
            addClassToAnnotationDatabase(element, level);
        }
    }

    private void addClassToAnnotationDatabase(Element classElement, int level) {
        Class newClass = createClass(classElement.asType(), level);
        annotatedClassSet.add(newClass);
        final List<Annotation> annotations = extractAnnotations(classElement, level);
        newClass.setRnRAnnotationList(annotations);
    }

    private void addFieldToAnnotationDatabase(Element fieldElement, int level) {
        Class fieldClass;
        //System.out.printf("Type: %s, injection: %s \n",typeElementName, fieldName);
        fieldClass = createClass(fieldElement.asType(), level);

        final Set<Modifier> modifiers = fieldElement.getModifiers();
        String fieldName = fieldElement.getSimpleName().toString();
        TypeElement declaringClassElement = (TypeElement) fieldElement.getEnclosingElement();
        String declaringClassName = declaringClassElement.getQualifiedName().toString();
        final List<Annotation> annotations = extractAnnotations(fieldElement, level);
        int modifiersInt = convertModifiersFromAnnotationProcessing(modifiers);
        final Class<?> enclosingClass = Class.forNameSafe(declaringClassName, level + 1);
        if (level == 0) {
            final Class<? extends Annotation> annotationType = annotations.get(0).rnrAnnotationType();
            Set<Class<?>> classes = mapAnnotationTypeToClassContainingAnnotation.get(annotationType);
            if (classes == null) {
                classes = new HashSet<>();
            }
            classes.add(enclosingClass);
            mapAnnotationTypeToClassContainingAnnotation.put(annotationType, classes);
        }
        final Field field = new Field(fieldName, fieldClass, enclosingClass, modifiersInt, annotations);
        enclosingClass.addField(field);
        annotatedClassSet.add(enclosingClass);
    }

    private void addParameterToAnnotationDatabase(Element paramElement, int level) {
        Element enclosing = paramElement.getEnclosingElement();
        String methodName = enclosing.getSimpleName().toString();
        //System.out.printf("Type: %s, injection: %s \n",typeElementName, methodName);
        final ExecutableElement methodOrConstructor = (ExecutableElement) paramElement.getEnclosingElement();
        if (methodName.startsWith("<init>")) {
            addConstructor(methodOrConstructor, level);
        } else {
            addMethod(methodOrConstructor, level);
        }
    }

    private Class[] getParameterTypes(ExecutableElement methodElement, int level) {
        final List<? extends VariableElement> parameters = methodElement.getParameters();
        Class[] paramTypes = new Class[parameters.size()];
        for (int indexParam = 0; indexParam < parameters.size(); indexParam++) {
            VariableElement parameter = parameters.get(indexParam);
            paramTypes[indexParam] = createClass(parameter.asType(), level);
        }
        return paramTypes;
    }

    private Class[] getExceptionTypes(ExecutableElement methodElement, int level) {
        final List<? extends TypeMirror> exceptionTypes = methodElement.getThrownTypes();
        Class[] paramTypes = new Class[exceptionTypes.size()];
        for (int indexParam = 0; indexParam < exceptionTypes.size(); indexParam++) {
            TypeMirror exceptionType = exceptionTypes.get(indexParam);
            paramTypes[indexParam] = createClass(exceptionType, level);
        }
        return paramTypes;
    }

    private void addMethodOrConstructorToAnnotationDatabase(ExecutableElement methodOrConstructorElement, int level) {
        String methodOrConstructorName = methodOrConstructorElement.getSimpleName().toString();
        //System.out.printf("Type: %s, injection: %s \n",typeElementName, methodOrConstructorName);
        if (methodOrConstructorName.startsWith("<init>")) {
            addConstructor(methodOrConstructorElement, level);
        } else {
            addMethod(methodOrConstructorElement, level);
        }
    }

    private void addConstructor(ExecutableElement methodElement, int level) {
        final Element enclosing = methodElement.getEnclosingElement();
        final TypeElement declaringClassElement = (TypeElement) enclosing;
        final Class[] paramTypes = getParameterTypes(methodElement, level);
        final Class[] exceptionTypes = getExceptionTypes(methodElement, level);
        final Class<?> classContainingMethod = Class.forNameSafe(declaringClassElement.asType().toString(), level + 1);
        final Constructor constructor = new Constructor(classContainingMethod,
                                                        paramTypes,
                                                        exceptionTypes,
                                                        convertModifiersFromAnnotationProcessing(methodElement.getModifiers()));

        final List<Annotation> annotations = extractAnnotations(methodElement, level);

        constructor.setRnRAnnotationList(annotations);

        classContainingMethod.addConstructor(constructor);
        annotatedClassSet.add(classContainingMethod);
    }

    private void addMethod(ExecutableElement methodElement, int level) {
        final Element enclosing = methodElement.getEnclosingElement();
        final String methodName = methodElement.getSimpleName().toString();
        final TypeElement declaringClassElement = (TypeElement) enclosing;
        final Class[] paramTypes = getParameterTypes(methodElement, level);
        final Class[] exceptionTypes = getExceptionTypes(methodElement, level);
        final String returnTypeName = methodElement.getReturnType().toString();
        final Class<?> declaringClass = Class.forNameSafe(declaringClassElement.asType().toString(), level + 1);
        final Method method = new Method(declaringClass,
                                         methodName,
                                         paramTypes,
                                         Class.forNameSafe(returnTypeName, level),
                                         exceptionTypes,
                                         convertModifiersFromAnnotationProcessing(methodElement.getModifiers()));

        final List<Annotation> annotations = extractAnnotations(methodElement, level);

        method.setRnRAnnotationList(annotations);
        method.setIsVarArgs(methodElement.isVarArgs());

        declaringClass.addMethod(method);
        annotatedClassSet.add(declaringClass);
    }

    private List<Annotation> extractAnnotations(Element annotatedElement, int level) {
        final List<Annotation> annotations = new ArrayList<>();
        for (AnnotationMirror annotationMirror : annotatedElement.getAnnotationMirrors()) {
            final Map<Method, Object> mapMethodToValue = new HashMap<>();

            final Class<?> annotationClass = createClass(annotationMirror.getAnnotationType(), level);
            annotationClass.setModifiers(annotationClass.getModifiers() | Class.ANNOTATION);
            annotationClasses.add(annotationClass);
            for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : annotationMirror.getElementValues().entrySet()) {
                final String methodOfAnnotationName = entry.getKey().getSimpleName().toString();

                //RnR 2
                final Method methodOfAnnotation = new Method(annotationClass,
                                                             methodOfAnnotationName,
                                                             //TODO : param types
                                                             new Class[0],
                                                             Class.forNameSafe(entry.getKey().getReturnType().toString(), level),
                                                             //TODO : exception types
                                                             new Class[0],
                                                             java.lang.reflect.Modifier.PUBLIC
                );
                mapMethodToValue.put(methodOfAnnotation, entry.getValue().getValue());
            }

            final Annotation annotation = new Annotation(annotationClass, mapMethodToValue);
            annotations.add(annotation);
        }
        return annotations;
    }

    private Class createClass(TypeMirror typeMirror, int level) {
        Class result;
        String className = null;
        boolean isPrimitive = false;
        boolean isArray = false;
        boolean isInterface = false;
        Class component = null;
        Class superClass = null;
        GenericDeclarationImpl declaration = null;
        Class[] superInterfaces = null;

        if (typeMirror instanceof DeclaredType) {
            DeclaredType declaredType = (DeclaredType) typeMirror;
            className = ((TypeElement) declaredType.asElement()).getQualifiedName().toString();
            if (!declaredType.getTypeArguments().isEmpty()) {
                declaration = new GenericDeclarationImpl();
                TypeVariable[] typesVariables = new TypeVariable[declaredType.getTypeArguments().size()];
                int index = 0;
                for (TypeMirror mirror : declaredType.getTypeArguments()) {
                    TypeVariableImpl typeVariableImpl = new TypeVariableImpl();
                    typesVariables[index] = typeVariableImpl;
                    typeVariableImpl.setName(mirror.toString());
                    index++;
                }

                declaration.setTypeParameters(typesVariables);
            }
            isInterface = ((com.sun.tools.javac.code.Type) typeMirror).isInterface();
            final int indexOfChevron = className.indexOf('<');
            if (indexOfChevron != -1) {
                className = className.substring(0, indexOfChevron);
            }
            TypeElement typeElement = (TypeElement) declaredType.asElement();
            if (level + 1 <= maxLevel) {
                TypeMirror superclass = typeElement.getSuperclass();
                superClass = createClass(superclass, level + 1);
            }
            if (level + 1 <= maxLevel) {
                superInterfaces = new Class[typeElement.getInterfaces().size()];
                int indexInterface = 0;
                for (TypeMirror superInterface : typeElement.getInterfaces()) {
                    superInterfaces[indexInterface++] = createClass(superInterface, level +1);
                }
            }
            if (level + 1 <= maxLevel) {
                final List<? extends Element> enclosedElements = ((TypeElement) declaredType.asElement()).getEnclosedElements();
                for (Element enclosedElement : enclosedElements) {
                    mapElementToReflection(enclosedElement, level + 1);
                }
            }
        } else if (typeMirror instanceof ArrayType) {
            //warning, this must come before Primitive as arrays are also primitives (here)
            isArray = true;
            className = ((ArrayType) typeMirror).getComponentType().toString() + "[]";
            component = createClass(((ArrayType) typeMirror).getComponentType(), level);
        } else if (typeMirror instanceof PrimitiveType) {
            isPrimitive = true;
            className = typeMirror.toString();
        }

        result = Class.forNameSafe(className, level);

        if (superClass!=null) {
            result.setSuperclass(superClass);
        }

        if (superInterfaces!=null) {
            result.setInterfaces(superInterfaces);
        }

        if (isArray) {
            result.setIsArray(true);
        }
        if (isPrimitive) {
            result.setIsPrimitive(true);
        }
        if (component != null) {
            result.setComponentType(component);
        }

        if (declaration != null) {
            result.setGenericInfo(declaration);
        }
        if (isInterface) {
            result.setIsInterface(true);
        }
        return result;
    }

    /*Visible for testing*/ int convertModifiersFromAnnotationProcessing(Set<Modifier> modifiers) {
        int result = 0;
        for (Modifier modifier : modifiers) {
            switch (modifier) {
                case ABSTRACT:
                    result |= java.lang.reflect.Modifier.ABSTRACT;
                    break;
                case PUBLIC:
                    result |= java.lang.reflect.Modifier.PUBLIC;
                    break;
                case PRIVATE:
                    result |= java.lang.reflect.Modifier.PRIVATE;
                    break;
                case STATIC:
                    result |= java.lang.reflect.Modifier.STATIC;
                    break;
                case PROTECTED:
                    result |= java.lang.reflect.Modifier.PROTECTED;
                    break;
                case FINAL:
                    result |= java.lang.reflect.Modifier.FINAL;
                    break;
                case SYNCHRONIZED:
                    result |= java.lang.reflect.Modifier.SYNCHRONIZED;
                    break;
                case VOLATILE:
                    result |= java.lang.reflect.Modifier.VOLATILE;
                    break;
                default:
            }
        }
        return result;
    }

    private String getTypeName(Element typeElement) {
        String injectedClassName = null;
        final TypeMirror fieldTypeMirror = typeElement.asType();
        if (fieldTypeMirror instanceof DeclaredType) {
            injectedClassName = ((TypeElement) ((DeclaredType) fieldTypeMirror).asElement()).getQualifiedName().toString();
        } else if (fieldTypeMirror instanceof PrimitiveType) {
            injectedClassName = fieldTypeMirror.toString();
        } else if (fieldTypeMirror instanceof ArrayType) {
            injectedClassName = ((ArrayType) fieldTypeMirror).getComponentType().toString() + "[]";
        }
        return injectedClassName;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        //http://stackoverflow.com/a/8188860/693752
        return SourceVersion.latest();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return targetAnnotatedClasses;
    }

    public void setTargetAnnotatedClasses(Set<String> targetAnnotatedClasses) {
        this.targetAnnotatedClasses = targetAnnotatedClasses;
    }

    /**
     * Level 0 : everything that is annotated will place every class referenced in its signature in the pool.
     * Level N : everything that is of level N-1 will place every class referenced in the signature of its members in the pool.
     *
     * A class that is only referenced in the pool, but empty (of level N), is called partially referenced or partial.
     * A class whose members are fully known (of level 0-->N-1), is called fully referenced or full.
     *
     * @param maxLevel
     */
    public void setMaxLevel(int maxLevel) {
        this.maxLevel = maxLevel;
    }

    public Set<String> getTargetAnnotatedClasses() {
        return targetAnnotatedClasses;
    }

    public Set<Class> getAnnotatedClassSet() {
        return annotatedClassSet;
    }

    public Set<Class> getAnnotationClasses() {
        return annotationClasses;
    }

    public Set<Class<?>> getClassesContainingAnnotation(Class<? extends Annotation> annotationType) {
        return mapAnnotationTypeToClassContainingAnnotation.get(annotationType);
    }

    public Map<Class<? extends Annotation>, Set<Class<?>>> getMapAnnotationTypeToClassContainingAnnotation() {
        return mapAnnotationTypeToClassContainingAnnotation;
    }

}