/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * 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 android.databinding.tool.reflection.annotation;

import android.databinding.tool.reflection.ModelAnalyzer;
import android.databinding.tool.reflection.ModelClass;
import android.databinding.tool.reflection.ModelField;
import android.databinding.tool.reflection.ModelMethod;
import android.databinding.tool.reflection.TypeUtil;
import android.databinding.tool.util.L;

import java.util.ArrayList;
import java.util.List;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
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.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

/**
 * This is the implementation of ModelClass for the annotation
 * processor. It relies on AnnotationAnalyzer.
 */
class AnnotationClass extends ModelClass {

    final TypeMirror mTypeMirror;

    public AnnotationClass(TypeMirror typeMirror) {
        mTypeMirror = typeMirror;
    }

    @Override
    public String toJavaCode() {
        if (isIncomplete()) {
            return getCanonicalName();
        }
        return mTypeMirror.toString();
    }

    @Override
    public boolean isArray() {
        return mTypeMirror.getKind() == TypeKind.ARRAY;
    }

    @Override
    public AnnotationClass getComponentType() {
        TypeMirror component = null;
        if (isArray()) {
            component = ((ArrayType) mTypeMirror).getComponentType();
        } else if (isList()) {
            for (ModelMethod method : getMethods("get", 1)) {
                ModelClass parameter = method.getParameterTypes()[0];
                if (parameter.isInt() || parameter.isLong()) {
                    ArrayList<ModelClass> parameters = new ArrayList<ModelClass>(1);
                    parameters.add(parameter);
                    return (AnnotationClass) method.getReturnType(parameters);
                }
            }
            // no "get" call found!
            return null;
        } else {
            AnnotationClass mapClass = (AnnotationClass) ModelAnalyzer.getInstance().getMapType();
            DeclaredType mapType = findInterface(mapClass.mTypeMirror);
            if (mapType == null) {
                return null;
            }
            component = mapType.getTypeArguments().get(1);
        }

        return new AnnotationClass(component);
    }

    private DeclaredType findInterface(TypeMirror interfaceType) {
        Types typeUtil = getTypeUtils();
        TypeMirror foundInterface = null;
        if (typeUtil.isSameType(interfaceType, typeUtil.erasure(mTypeMirror))) {
            foundInterface = mTypeMirror;
        } else {
            ArrayList<TypeMirror> toCheck = new ArrayList<TypeMirror>();
            toCheck.add(mTypeMirror);
            while (!toCheck.isEmpty()) {
                TypeMirror typeMirror = toCheck.remove(0);
                if (typeUtil.isSameType(interfaceType, typeUtil.erasure(typeMirror))) {
                    foundInterface = typeMirror;
                    break;
                } else {
                    toCheck.addAll(typeUtil.directSupertypes(typeMirror));
                }
            }
            if (foundInterface == null) {
                L.e("Detected " + interfaceType + " type for " + mTypeMirror +
                        ", but not able to find the implemented interface.");
                return null;
            }
        }
        if (foundInterface.getKind() != TypeKind.DECLARED) {
            L.e("Found " + interfaceType + " type for " + mTypeMirror +
                    ", but it isn't a declared type: " + foundInterface);
            return null;
        }
        return (DeclaredType) foundInterface;
    }

    @Override
    public boolean isNullable() {
        switch (mTypeMirror.getKind()) {
            case ARRAY:
            case DECLARED:
            case NULL:
                return true;
            default:
                return false;
        }
    }

    @Override
    public boolean isPrimitive() {
        switch (mTypeMirror.getKind()) {
            case BOOLEAN:
            case BYTE:
            case SHORT:
            case INT:
            case LONG:
            case CHAR:
            case FLOAT:
            case DOUBLE:
                return true;
            default:
                return false;
        }
    }

    @Override
    public boolean isBoolean() {
        return mTypeMirror.getKind() == TypeKind.BOOLEAN;
    }

    @Override
    public boolean isChar() {
        return mTypeMirror.getKind() == TypeKind.CHAR;
    }

    @Override
    public boolean isByte() {
        return mTypeMirror.getKind() == TypeKind.BYTE;
    }

    @Override
    public boolean isShort() {
        return mTypeMirror.getKind() == TypeKind.SHORT;
    }

    @Override
    public boolean isInt() {
        return mTypeMirror.getKind() == TypeKind.INT;
    }

    @Override
    public boolean isLong() {
        return mTypeMirror.getKind() == TypeKind.LONG;
    }

    @Override
    public boolean isFloat() {
        return mTypeMirror.getKind() == TypeKind.FLOAT;
    }

    @Override
    public boolean isDouble() {
        return mTypeMirror.getKind() == TypeKind.DOUBLE;
    }

    @Override
    public boolean isGeneric() {
        boolean isGeneric = false;
        if (mTypeMirror.getKind() == TypeKind.DECLARED) {
            DeclaredType declaredType = (DeclaredType) mTypeMirror;
            List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
            isGeneric = typeArguments != null && !typeArguments.isEmpty();
        }
        return isGeneric;
    }

    @Override
    public int getMinApi() {
        if (mTypeMirror.getKind() == TypeKind.DECLARED) {
            DeclaredType declaredType = (DeclaredType) mTypeMirror;
            List<? extends AnnotationMirror> annotations =
                    getElementUtils().getAllAnnotationMirrors(declaredType.asElement());

            TypeElement targetApi = getElementUtils().getTypeElement("android.annotation.TargetApi");
            TypeMirror targetApiType = targetApi.asType();
            Types typeUtils = getTypeUtils();
            for (AnnotationMirror annotation : annotations) {
                if (typeUtils.isAssignable(annotation.getAnnotationType(), targetApiType)) {
                    for (AnnotationValue value : annotation.getElementValues().values()) {
                        return (Integer) value.getValue();
                    }
                }
            }
        }
        return super.getMinApi();
    }

    @Override
    public List<ModelClass> getTypeArguments() {
        List<ModelClass> types = null;
        if (mTypeMirror.getKind() == TypeKind.DECLARED) {
            DeclaredType declaredType = (DeclaredType) mTypeMirror;
            List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
            if (typeArguments != null && !typeArguments.isEmpty()) {
                types = new ArrayList<ModelClass>();
                for (TypeMirror typeMirror : typeArguments) {
                    types.add(new AnnotationClass(typeMirror));
                }
            }
        }
        return types;
    }

    @Override
    public boolean isTypeVar() {
        return mTypeMirror.getKind() == TypeKind.TYPEVAR;
    }

    @Override
    public boolean isWildcard() {
        return mTypeMirror.getKind() == TypeKind.WILDCARD;
    }

    @Override
    public boolean isInterface() {
        return mTypeMirror.getKind() == TypeKind.DECLARED &&
                ((DeclaredType)mTypeMirror).asElement().getKind() == ElementKind.INTERFACE;
    }

    @Override
    public boolean isVoid() {
        return mTypeMirror.getKind() == TypeKind.VOID;
    }

    @Override
    public AnnotationClass unbox() {
        if (!isNullable()) {
            return this;
        }
        try {
            return new AnnotationClass(getTypeUtils().unboxedType(mTypeMirror));
        } catch (IllegalArgumentException e) {
            // I'm being lazy. This is much easier than checking every type.
            return this;
        }
    }

    @Override
    public AnnotationClass box() {
        if (!isPrimitive()) {
            return this;
        }
        return new AnnotationClass(getTypeUtils().boxedClass((PrimitiveType) mTypeMirror).asType());
    }

    @Override
    public boolean isAssignableFrom(ModelClass that) {
        if (that == null) {
            return false;
        }
        AnnotationClass thatAnnotationClass = (AnnotationClass) that;
        return getTypeUtils().isAssignable(thatAnnotationClass.mTypeMirror, this.mTypeMirror);
    }

    @Override
    public ModelMethod[] getDeclaredMethods() {
        final ModelMethod[] declaredMethods;
        if (mTypeMirror.getKind() == TypeKind.DECLARED) {
            DeclaredType declaredType = (DeclaredType) mTypeMirror;
            Elements elementUtils = getElementUtils();
            TypeElement typeElement = (TypeElement) declaredType.asElement();
            List<? extends Element> members = elementUtils.getAllMembers(typeElement);
            List<ExecutableElement> methods = ElementFilter.methodsIn(members);
            declaredMethods = new ModelMethod[methods.size()];
            for (int i = 0; i < declaredMethods.length; i++) {
                declaredMethods[i] = new AnnotationMethod(declaredType, methods.get(i));
            }
        } else {
            declaredMethods = new ModelMethod[0];
        }
        return declaredMethods;
    }

    @Override
    public AnnotationClass getSuperclass() {
        if (mTypeMirror.getKind() == TypeKind.DECLARED) {
            DeclaredType declaredType = (DeclaredType) mTypeMirror;
            TypeElement typeElement = (TypeElement) declaredType.asElement();
            TypeMirror superClass = typeElement.getSuperclass();
            if (superClass.getKind() == TypeKind.DECLARED) {
                return new AnnotationClass(superClass);
            }
        }
        return null;
    }

    @Override
    public String getCanonicalName() {
        return getTypeUtils().erasure(mTypeMirror).toString();
    }

    @Override
    public ModelClass erasure() {
        final TypeMirror erasure = getTypeUtils().erasure(mTypeMirror);
        if (erasure == mTypeMirror) {
            return this;
        } else {
            return new AnnotationClass(erasure);
        }
    }

    @Override
    public String getJniDescription() {
        return TypeUtil.getInstance().getDescription(this);
    }

    @Override
    protected ModelField[] getDeclaredFields() {
        final ModelField[] declaredFields;
        if (mTypeMirror.getKind() == TypeKind.DECLARED) {
            DeclaredType declaredType = (DeclaredType) mTypeMirror;
            Elements elementUtils = getElementUtils();
            TypeElement typeElement = (TypeElement) declaredType.asElement();
            List<? extends Element> members = elementUtils.getAllMembers(typeElement);
            List<VariableElement> fields = ElementFilter.fieldsIn(members);
            declaredFields = new ModelField[fields.size()];
            for (int i = 0; i < declaredFields.length; i++) {
                declaredFields[i] = new AnnotationField(typeElement, fields.get(i));
            }
        } else {
            declaredFields = new ModelField[0];
        }
        return declaredFields;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof AnnotationClass) {
            return getTypeUtils().isSameType(mTypeMirror, ((AnnotationClass) obj).mTypeMirror);
        } else {
            return false;
        }
    }

    @Override
    public int hashCode() {
        return mTypeMirror.toString().hashCode();
    }

    private static Types getTypeUtils() {
        return AnnotationAnalyzer.get().mProcessingEnv.getTypeUtils();
    }

    private static Elements getElementUtils() {
        return AnnotationAnalyzer.get().mProcessingEnv.getElementUtils();
    }

    @Override
    public String toString() {
        return mTypeMirror.toString();
    }
}
