package org.cf.apkfile.dex;


import gnu.trove.map.TIntIntMap;
import gnu.trove.map.TObjectIntMap;
import gnu.trove.map.hash.TIntIntHashMap;
import gnu.trove.map.hash.TObjectIntHashMap;
import org.cf.apkfile.analysis.DexReaderEntropyCalculator;
import org.cf.apkfile.utils.Utils;
import org.jf.dexlib2.Opcode;
import org.jf.dexlib2.ReferenceType;
import org.jf.dexlib2.dexbacked.DexBackedMethod;
import org.jf.dexlib2.dexbacked.DexBackedMethodImplementation;
import org.jf.dexlib2.iface.instruction.Instruction;
import org.jf.dexlib2.iface.instruction.ReferenceInstruction;
import org.jf.dexlib2.iface.reference.FieldReference;
import org.jf.dexlib2.iface.reference.MethodReference;
import org.jf.dexlib2.iface.reference.StringReference;
import org.jf.dexlib2.util.ReferenceUtil;

import javax.annotation.Nonnull;
import java.lang.reflect.Field;

public class DexMethod {

    private static final String[] API_PACKAGES = new String[] {
            "Landroid/", "Lcom/android/", "Lcom/google/", "Lcom/sec/android/", "Lcom/sun/", "Ldalvik/", "Lgov/",
            "Ljava/", "Ljavax/", "Ljunit/", "Llibcore/", "Lorg/apache/", "Lorg/ccil/", "Lorg/json/",
            "Lorg/kxml2/", "Lorg/spongycastle/", "Lorg/w3c/", "Lorg/xml/", "Lorg/xmlpull/", "Lsun/"
    };

    private final transient DexBackedMethod method;
    private final transient boolean shortMethodSignatures;

    private final int accessFlags;
    private final int annotationCount;
    private final TObjectIntMap<MethodReference> frameworkApiCounts;
    private final TObjectIntMap<FieldReference> frameworkFieldReferenceCounts;
    private final TIntIntMap opCounts;
    private final TObjectIntMap<StringReference> stringReferenceCounts;
    private final int size;

    private int debugItemCount = 0;
    private int instructionCount = 0;
    private int registerCount = 0;
    private int tryCatchCount = 0;
    private int cyclomaticComplexity = -1;
    private double codeEntropy = 0.0D;
    private double codePerplexity = 0.0D;

    DexMethod(DexBackedMethod method, boolean shortMethodSignatures) {
        this.method = method;
        this.shortMethodSignatures = shortMethodSignatures;

        size = method.getSize();
        accessFlags = method.getAccessFlags();
        annotationCount = method.getAnnotations().size();
        frameworkApiCounts = new TObjectIntHashMap<>();
        frameworkFieldReferenceCounts = new TObjectIntHashMap<>();
        opCounts = new TIntIntHashMap();
        stringReferenceCounts = new TObjectIntHashMap<>();
        if (method.getImplementation() != null) {
            analyze(method.getImplementation());
        }
    }

    private void analyze(@Nonnull DexBackedMethodImplementation implementation) {
        debugItemCount = Utils.makeCollection(implementation.getDebugItems()).size();
        registerCount = implementation.getRegisterCount();
        tryCatchCount = implementation.getTryBlocks().size();

        for (Instruction instruction : implementation.getInstructions()) {
            instructionCount++;
            Opcode op = instruction.getOpcode();
            opCounts.adjustOrPutValue(op.apiToValueMap.get(DexFile.TARGET_API), 1, 1);

            if (instruction instanceof ReferenceInstruction) {
                ReferenceInstruction refInstr = (ReferenceInstruction) instruction;
                if (op.referenceType == ReferenceType.METHOD || op.referenceType == ReferenceType.FIELD) {
                    boolean isApiPackage = false;
                    for (String apiPackage : API_PACKAGES) {
                        String refStr = ReferenceUtil.getReferenceString(refInstr.getReference());
                        if (refStr.startsWith(apiPackage)) {
                            isApiPackage = true;
                            break;
                        }
                    }
                    if (!isApiPackage) {
                        continue;
                    }
                }

                switch (op.referenceType) {
                    case ReferenceType.METHOD:
                        MethodReference methodRef = (MethodReference) refInstr.getReference();
                        if (shortMethodSignatures) {
                            ShortMethodReference shortMethodRef = new ShortMethodReference(methodRef);
                            frameworkApiCounts.adjustOrPutValue(shortMethodRef, 1, 1);
                        } else {
                            frameworkApiCounts.adjustOrPutValue(methodRef, 1, 1);
                        }
                        break;
                    case ReferenceType.FIELD:
                        FieldReference fieldRef = (FieldReference) refInstr.getReference();
                        frameworkFieldReferenceCounts.adjustOrPutValue(fieldRef, 1, 1);
                        break;
                    case ReferenceType.STRING:
                        StringReference stringRef = (StringReference) refInstr.getReference();
                        stringReferenceCounts.adjustOrPutValue(stringRef, 1, 1);
                        break;
                }
            }
        }

        analyzeEntropy();
    }

    private void analyzeEntropy() {
        try {
            Field f = DexBackedMethodImplementation.class.getDeclaredField("codeOffset");
            f.setAccessible(true);
            int codeOffset = (Integer) f.get(method.getImplementation());
            DexReaderEntropyCalculator calculator = new DexReaderEntropyCalculator(method.dexFile.getDataBuffer().readerAt(codeOffset));
            int implementationSize = method.getImplementation().getSize();
            calculator.calculate(codeOffset, implementationSize);
            codeEntropy = calculator.entropy();
            codePerplexity = calculator.perplexity();
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public int getAccessFlags() {
        return accessFlags;
    }

    public int getAnnotationCount() {
        return annotationCount;
    }

    public TObjectIntMap<MethodReference> getFrameworkApiCounts() {
        return frameworkApiCounts;
    }

    public int getCyclomaticComplexity() {
        return cyclomaticComplexity;
    }

    public int getDebugItemCount() {
        return debugItemCount;
    }

    public TObjectIntMap<FieldReference> getFrameworkFieldReferenceCounts() {
        return frameworkFieldReferenceCounts;
    }

    public int getInstructionCount() {
        return instructionCount;
    }

    public DexBackedMethod getMethod() {
        return method;
    }

    public int getSize() {
        return size;
    }

    public TIntIntMap getOpCounts() {
        return opCounts;
    }

    public double getCodeEntropy() {
        return codeEntropy;
    }

    public double getCodePerplexity() {
        return codePerplexity;
    }

    public int getRegisterCount() {
        return registerCount;
    }

    public TObjectIntMap<StringReference> getStringReferenceCounts() {
        return stringReferenceCounts;
    }

    public int getTryCatchCount() {
        return tryCatchCount;
    }

    public void setCyclomaticComplexity(int cyclomaticComplexity) {
        this.cyclomaticComplexity = cyclomaticComplexity;
    }

    public String toString() {
        return ReferenceUtil.getMethodDescriptor(getMethod());
    }

}