/*
 * Copyright 2016 requery.io
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.requery.processor;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.SimpleTypeVisitor6;
import javax.lang.model.util.Types;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * Helper class for working with {@link java.lang.annotation.Annotation) and
 * {@link javax.lang.model.element.AnnotationMirror}.
 *
 * @author Nikhil Purushe
 */
final class Mirrors {

    private Mirrors() {
    }

    static Optional<? extends AnnotationMirror> findAnnotationMirror(
            Element element, Class<? extends Annotation> annotation) {
        return findAnnotationMirror(element, annotation.getName());
    }

    static Optional<? extends AnnotationMirror> findAnnotationMirror(
            Element element, String qualifiedName) {
        return element.getAnnotationMirrors().stream()
            .filter(mirror ->
                namesEqual((TypeElement) mirror.getAnnotationType().asElement(), qualifiedName))
            .findFirst();
    }

    static Optional<AnnotationValue> findAnnotationValue(AnnotationMirror mirror) {
        return findAnnotationValue(mirror, "value");
    }

    static Optional<AnnotationValue> findAnnotationValue(AnnotationMirror mirror, String name) {

        return mirror.getElementValues() == null ? Optional.empty() :
               mirror.getElementValues().entrySet().stream()
                .filter(entry -> entry.getKey().getSimpleName().contentEquals(name))
                .map(entry -> (AnnotationValue)entry.getValue()).findFirst();
    }

    static List<TypeMirror> listGenericTypeArguments(TypeMirror typeMirror) {
        final List<TypeMirror> typeArguments = new ArrayList<>();
        typeMirror.accept(new SimpleTypeVisitor6<Void, Void>() {
            @Override
            public Void visitDeclared(DeclaredType declaredType, Void v) {
                if (!declaredType.getTypeArguments().isEmpty()) {
                    typeArguments.addAll(declaredType.getTypeArguments());
                }
                return null;
            }

            @Override
            protected Void defaultAction(TypeMirror typeMirror, Void v) {
                return null;
            }
        }, null);
        return typeArguments;
    }

    static boolean isInstance(Types types, TypeElement element, Class<?> type) {
        if (element == null) {
            return false;
        }
        String className = type.getCanonicalName();
        if (type.isInterface()) {
            return implementsInterface(types, element, className);
        } else {
            return namesEqual(element, className) || extendsClass(types, element, className);
        }
    }

    static boolean isInstance(Types types, TypeElement element, String className) {
        if (element == null) {
            return false;
        }
        // check name
        if (namesEqual(element, className)) {
            return true;
        }
        // check interfaces then super types
        return implementsInterface(types, element, className) ||
                extendsClass(types, element, className);
    }

    private static boolean implementsInterface(Types types, TypeElement element, String interfaceName) {
        // check name or interfaces
        if (namesEqual(element, interfaceName)) {
            return true;
        }
        List<? extends TypeMirror> interfaces = element.getInterfaces();
        for (TypeMirror interfaceType : interfaces) {
            interfaceType = types.erasure(interfaceType);
            TypeElement typeElement = (TypeElement) types.asElement(interfaceType);
            if (typeElement != null && implementsInterface(types, typeElement, interfaceName)) {
                return true;
            }
        }
        return false;
    }

    private static boolean extendsClass(Types types, TypeElement element, String className) {
        if (namesEqual(element, className)) {
            return true;
        }
        // check super types
        TypeMirror superType = element.getSuperclass();
        while (superType != null && superType.getKind() != TypeKind.NONE) {
            TypeElement superTypeElement = (TypeElement) types.asElement(superType);
            if (namesEqual(superTypeElement, className)) {
                return true;
            }
            superType = superTypeElement.getSuperclass();
        }
        return false;
    }

    static boolean overridesMethod(Types types, TypeElement element, String methodName) {
        while (element != null) {
            if (ElementFilter.methodsIn(element.getEnclosedElements()).stream()
                .anyMatch(method -> method.getSimpleName().contentEquals(methodName))) {
                return true;
            }
            TypeMirror superType = element.getSuperclass();
            if (superType.getKind() == TypeKind.NONE) {
                break;
            } else {
                element = (TypeElement) types.asElement(superType);
            }
            if (namesEqual(element, Object.class.getCanonicalName())) {
                break;
            }
        }
        return false;
    }

    private static boolean namesEqual(TypeElement element, String qualifiedName) {
        return element != null && element.getQualifiedName().contentEquals(qualifiedName);
    }
}

@@ -12,17 +12,17 @@
 ight 201
-6
+7
  requery
@@ -4272,32 +4272,304 @@
 true;%0A        %7D%0A
+%0A        TypeMirror type = element.asType();%0A        while (type != null && type.getKind() != TypeKind.NONE) %7B%0A            TypeElement currentElement = (TypeElement) types.asElement(type);%0A            if (currentElement == null) %7B%0A                break;%0A            %7D%0A    
         List%3C? e
@@ -4633,16 +4633,20 @@
         
+    
 for (Typ
@@ -4695,16 +4695,20 @@
         
+ 
+   
 interfac
@@ -4749,32 +4749,36 @@
 e);%0A            
+   
+ 
 TypeElement type
@@ -4825,32 +4825,36 @@
 interfaceType);%0A
+    
             if (
@@ -4942,32 +4942,36 @@
                 
+    
 return true;%0A   
@@ -4971,33 +4971,102 @@
 ue;%0A            
+  
-%7D
+  %7D%0A            %7D%0A            type = currentElement.getSuperclass();
 %0A        %7D%0A     

Resolve #580 Fix MutuableResult type not detected correctly