/*
* Copyright 2014 Mingyuan Xia (http://mxia.me) and contributors
*
* 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.
*
* Contributors:
*   Mingyuan Xia
*   Lu Gong
*/

package patdroid.core;

import java.lang.reflect.Modifier;

import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import patdroid.util.Log;

import static com.google.common.base.Preconditions.checkState;

/**
 * The class representation. Each class is uniquely identified by its full name.
 * So given a class full name, there is exactly one ClassInfo representing it.
 * ClassInfo works in a late-bind manner with ClassDetail.
 * A ClassInfo could just refer to a type without any details about its methods, fields, etc.
 * Then later on, when the class details become available, a ClassDetail iobject is created and
 * attached to the ClassInfo.
 * <p>
 * ClassInfos are obtained by find-series functions not created by constructors.
 */
public final class ClassInfo {
    private static final ClassDetail MISSING_DETAIL = new ClassDetail.Builder().build();
    public final FullMethodSignature STATIC_INITIALIZER;
    public final FullMethodSignature DEFAULT_CONSTRUCTOR;

    public final Scope scope;
    public final String fullName;
    public ClassDetail mutableDetail = MISSING_DETAIL;

    /**
     * @param scope the scope that this ClassInfo belongs to
     * @param fullName the full name of the class
     */
    public ClassInfo(Scope scope, String fullName) {
        this.scope = scope;
        this.fullName = fullName;
        this.DEFAULT_CONSTRUCTOR = new FullMethodSignature(scope.primitiveVoid, MethodInfo.CONSTRUCTOR, this);
        this.STATIC_INITIALIZER = new FullMethodSignature(scope.primitiveVoid, MethodInfo.STATIC_INITIALIZER);
    }

    /**
     * A framework class is a class that is not found in the apk being parsed
     * <p>
     * <b>Note:</b> this might start class loading if the class is not loaded yet
     * @return if the class is a framework class
     */
    public boolean isFrameworkClass() {
        return mutableDetail.isFrameworkClass;
    }

    /**
     * Sometimes the apk has missing classes. A missing class is not
     * a framework class and cannot be found in the apk
     * @return if this class is missing
     */
    public boolean isMissing() {
        return mutableDetail == MISSING_DETAIL;
    }

    /**
     * Get the type of a non-static field. This functions will look into the base class.
     * <p>
     * <b>Note:</b> this might start class loading if the class is not loaded yet
     * @param fieldName the name of the field.
     * @return the type, or null if not found or the class is missing
     */
    public ClassInfo getFieldType(String fieldName) {
        return mutableDetail.getFieldType(fieldName);
    }

    /**
     * Get the type of a static field. This functions might look into its base class.
     * <p>
     * <b>Note:</b> this might start class loading if the class is not loaded yet
     * @param fieldName the name of the static field
     * @return the type of the static field, or null if not found or the class is missing
     */
    public ClassInfo getStaticFieldType(String fieldName) {
        return mutableDetail.getStaticFieldType(fieldName);
    }

    /**
     * Get all the fields declared in this class.
     * <p>
     * <b>Note:</b> this might start class loading if the class is not loaded yet
     * @return a key-value store mapping field name to their types
     */
    public ImmutableMap<String, ClassInfo> getAllFieldsHere() {
        return mutableDetail.fields;
    }

    /**
     * Get all the static fields declared in this class.
     * <p>
     * <b>Note:</b> this might start class loading if the class is not loaded yet
     * @return a key-value store mapping static field name to their types
     */
    public ImmutableMap<String, ClassInfo> getAllStaticFieldsHere() {
        return mutableDetail.staticFields;
    }

    /**
     *
     * @return all methods in the class
     */
    public ImmutableCollection<MethodInfo> getAllMethods() {
        return mutableDetail.methods.values();
    }

    /**
     * Find a method declared in this class
     * <p>
     * <b>Note:</b> this might start class loading if the class is not loaded yet
     * @param signature the method signature
     * @return the method in this class, or null if not found or the class is missing
     */
    public MethodInfo findMethodHere(FullMethodSignature signature) {
        return mutableDetail.methods.get(signature);
    }

    /**
     * Find all methods that have the give name and are declared in this class
     * <p>
     * <b>Note:</b> this might start class loading if the class is not loaded yet
     * @param name the method name
     * @return an array of methods, or null if the class is missing.
     * An empty array will be returned in case of not finding any method
     */
    public MethodInfo[] findMethodsHere(String name) {
        return mutableDetail.findMethodsHere(name);
    }

    /**
     * Find all methods that have the give name. This might need to look into base classes
     * <p>
     * <b>Note:</b> this might start class loading if the class is not loaded yet
     * @param name the method name
     * @return an array of methods, or null if the class is missing.
     * An empty array will be returned in case of not finding any method
     */
    public MethodInfo[] findMethods(String name) {
        return mutableDetail.findMethods(name);
    }

    /**
     * Find a method with given function prototype. This might need to look into base classes
     * <p>
     * <b>Note:</b> this might start class loading if the class is not loaded yet
     * @param signature the method signature
     * @return  the method representation, or null if not found or the class is missing
     */
    public MethodInfo findMethod(FullMethodSignature signature) {
        return mutableDetail.findMethod(signature);
    }

    /**
     * TypeA is convertible to TypeB if and only if TypeB is an indirect
     * base type or an indirect interface of TypeA.
     * <p>
     * <b>Note:</b> this might start class loading if the class is not loaded yet
     * @param type type B
     * @return if this class can be converted to the other.
     */
    public boolean isConvertibleTo(ClassInfo type) {
        if (type.isPrimitive()) {
            return (type == scope.primitiveVoid || isPrimitive());
        } else {
            return mutableDetail.isConvertibleTo(type);
        }
    }

    @Override
    public String toString() {
        return fullName;
    }

    /**
     * @return if this class is an array type
     */
    public boolean isArray() {
        return fullName.startsWith("[");
    }

    /**
     * Return the element type given this class as an array type.
     * @return the element type
     */
    public ClassInfo getElementClass() {
        checkState(isArray(), "Try getting the element class of a non-array class " + this);
        final char first = fullName.charAt(1);
        switch (first) {
        case 'C': return scope.primitiveChar;
        case 'I': return scope.primitiveInt;
        case 'B': return scope.primitiveByte;
        case 'Z': return scope.primitiveBoolean;
        case 'F': return scope.primitiveFloat;
        case 'D': return scope.primitiveDouble;
        case 'S': return scope.primitiveShort;
        case 'J': return scope.primitiveLong;
        case 'V': return scope.primitiveVoid;
        case 'L': return scope.findOrCreateClass(fullName.substring(2, fullName.length() - 1));
        case '[': return scope.findOrCreateClass(fullName.substring(1));
        default:
            Log.err("unknown element type for:" + fullName);
            return null;
        }
    }

    /**
     *
     * <b>Note:</b> this might cause class loading if the class is not loaded yet
     * @return the base type, or null if this class is java.lang.Object
     */
    public ClassInfo getBaseType() {
        return mutableDetail.baseType;
    }

    /**
     * Get the interfaces that the current class implements
     * @return interfaces
     */
    public ImmutableList<ClassInfo> getInterfaces() { return mutableDetail.interfaces; }

    /**
     * Change the super class of this class to a new super class, the
     * derivedClasses will be updated accordingly.
     * @param baseType new super class for this class
     */
    public void setBaseType(ClassInfo baseType) {
        ClassDetail origDetails = mutableDetail;
        origDetails.removeDerivedClasses(this);
        mutableDetail = origDetails.changeBaseType(baseType);
        mutableDetail.updateDerivedClasses(this);
    }

    /**
     * @return if this class is an inner class
     */
    public boolean isInnerClass() {
        return fullName.lastIndexOf('$') != -1;
    }

    /**
     * @return the outer class
     */
    public ClassInfo getOuterClass() {
        checkState(isInnerClass(), "Try getting the outer class from a non-inner class" + this);
        return scope.findOrCreateClass(fullName.substring(0, fullName.lastIndexOf('$')));
    }

    /**
     * @return true if the class is a primitive type
     */
    public boolean isPrimitive() {
        return scope.primitives.contains(this);
    }

    /**
     * @return if the class is final
     */
    public boolean isFinal() {
        return Modifier.isFinal(mutableDetail.accessFlags);
    }

    /**
     * @return if the class is an interface
     */
    public boolean isInterface() {
        return Modifier.isInterface(mutableDetail.accessFlags);
    }

    public boolean isAbstract() {
        return Modifier.isAbstract(mutableDetail.accessFlags);
    }

    /**
     * Get the default constructor
     * <p>
     * <b>Note:</b> this might start class loading if the class is not loaded yet
     * @return the default constructor, or null if not found
     */
    public MethodInfo getDefaultConstructor() {
        return findMethodHere(DEFAULT_CONSTRUCTOR);
    }

    /**
     * Find the static initializer method of the class
     * <p>
     * <b>Note:</b> this might start class loading if the class is not loaded yet
     * @return the static initializer or null if not found
     */
    public MethodInfo getStaticInitializer() {
        return findMethod(STATIC_INITIALIZER);
    }

    /**
     * Get the short name of the class. The short name is the part after the last
     * '.' in the full name of the class
     * @return the short name
     */
    public String getShortName() {
        final int idx = fullName.lastIndexOf('.');
        if (idx == -1) {
            return fullName;
        } else {
            return fullName.substring(idx+1, fullName.length());
        }
    }

    /**
     * An almost final class has no derived classes in the current class tree
     * @return if a class is "almost final"
     */
    public boolean isAlmostFinal() {
        return mutableDetail.derivedClasses.isEmpty();
    }
}