package com.android.reverse.collecter;

import android.os.Build;
import android.util.Log;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Iterator;

import com.android.reverse.hook.HookHelperFacktory;
import com.android.reverse.hook.HookHelperInterface;
import com.android.reverse.hook.HookParam;
import com.android.reverse.hook.MethodHookCallBack;
import com.android.reverse.smali.MemoryBackSmali;
import com.android.reverse.util.Logger;
import com.android.reverse.util.NativeFunction;
import com.android.reverse.util.RefInvoke;

import dalvik.system.DexFile;
import dalvik.system.PathClassLoader;

public class DexFileInfoCollecter {

    private static PathClassLoader pathClassLoader;// TODO 测试自定义Classloader的应用,如有热修复的QQ
    private static HashMap<Long, DexFileInfo> dynLoadedDexInfo = new HashMap<Long, DexFileInfo>();
    private static DexFileInfoCollecter collecter;
    private HookHelperInterface hookhelper = HookHelperFacktory.getHookHelper();
    private final static String DVMLIB_LIB = "dvmnative";

    private DexFileInfoCollecter() {

    }

    public static DexFileInfoCollecter getInstance() {
        if (collecter == null)
            collecter = new DexFileInfoCollecter();
        return collecter;
    }

    public void start() throws Throwable {

        pathClassLoader = (PathClassLoader) ModuleContext.getInstance().getBaseClassLoader();


        hookdefineClassNativeMethod();
        hookOpenDexFileNativeMethod();

    }


    public void hookdefineClassNativeMethod() {
        Method defineClassNativeMethod;

//            (Build.VERSION.SDK_INT >= 23)
//            private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,DexFile dexFile)
        defineClassNativeMethod = RefInvoke.findMethodExact("dalvik.system.DexFile", ClassLoader.getSystemClassLoader(), "defineClassNative",
                String.class, ClassLoader.class, Object.class, DexFile.class);

        if (defineClassNativeMethod == null) {
//                (Build.VERSION.SDK_INT >= 20)
            defineClassNativeMethod = RefInvoke.findMethodExact("dalvik.system.DexFile", ClassLoader.getSystemClassLoader(), "defineClassNative",
                    String.class, ClassLoader.class, long.class);
        }

        if (defineClassNativeMethod == null) {
//            dalvik
            defineClassNativeMethod = RefInvoke.findMethodExact("dalvik.system.DexFile", ClassLoader.getSystemClassLoader(), "defineClassNative",
                    String.class, ClassLoader.class, int.class);
        }

        if (defineClassNativeMethod == null) {
            Logger.log("error at " + DexFileInfoCollecter.class.getName() + "#" + "hookOpenDexFileNativeMethod() :");
            Logger.log("unable to find suit method to hook, may be the Zjdroid is too old");
        }

        hookhelper.hookMethod(defineClassNativeMethod, new MethodHookCallBack() {
            @Override
            public void beforeHookedMethod(HookParam param) {

            }

            @Override
            public void afterHookedMethod(HookParam param) {
                if (!param.hasThrowable()) {

                    long[] mCookies = parseMCookies(param.args[2]);
                    if (mCookies != null) {
                        for (int index = 0; index < mCookies.length; ++index) {
                            if (mCookies[index] != 0) {
                                setDefineClassLoader(mCookies[index], (ClassLoader) param.args[1]);
                            }
                        }
                    }
                }
            }

        });

    }

    public void hookOpenDexFileNativeMethod() {

        Method openDexFileNativeMethod = null;

        try {
//          (Build.VERSION.SDK_INT >= 23) art
//          private static native java.lang.Object dalvik.system.DexFile.openDexFileNative(java.lang.String,java.lang.String,int,java.lang.ClassLoader,dalvik.system.DexPathList$Element[])
            openDexFileNativeMethod = RefInvoke.findMethodExact("dalvik.system.DexFile", ClassLoader.getSystemClassLoader(), "openDexFileNative",
                    String.class, String.class, int.class, ClassLoader.class, Class.forName("[Ldalvik.system.DexPathList$Element;"));
        } catch (ClassNotFoundException e) {
        }

//        dalvik
        if (openDexFileNativeMethod == null) {
            openDexFileNativeMethod = RefInvoke.findMethodExact("dalvik.system.DexFile", ClassLoader.getSystemClassLoader(), "openDexFileNative",
                    String.class, String.class, int.class);
        }

        if (openDexFileNativeMethod == null) {
            Logger.log("error at " + DexFileInfoCollecter.class.getName() + "#" + "hookOpenDexFileNativeMethod() :");
            Logger.log("unable to find suit method to hook, may be the Zjdroid is too old");
        } else {

            hookhelper.hookMethod(openDexFileNativeMethod, new MethodHookCallBack() {
                @Override
                public void beforeHookedMethod(HookParam param) {

                }

                @Override
                public void afterHookedMethod(HookParam param) {

                    String dexPath = (String) param.args[0];

                    long[] mCookies = parseMCookies(param.getResult());
                    if (mCookies != null) {
                        for (int index = 0; index < mCookies.length; ++index) {
                            if (mCookies[index] != 0) {
                                dynLoadedDexInfo.put(mCookies[index], new DexFileInfo(dexPath, mCookies[index]));
                            }
                            Logger.log("openDexFileNative() is invoked with filepath:" + param.args[0] + " result: long[" + index + "]" + mCookies[index]);
                        }
                    }
                }
            });
        }
    }

    /**
     * @param mCookies
     * @return 所有可用的dexFile的mCookie
     */

    public static long[] parseMCookies(Object mCookies) {

        if (mCookies instanceof Integer) {
            return new long[]{(Integer) mCookies};
        } else if (mCookies instanceof Long) {
            return new long[]{(Long) mCookies};
        } else if (mCookies instanceof long[]) {
            long[] cookies = ((long[]) mCookies);
            long[] longs = new long[cookies.length - 1];
//            sdk23开始的art虚拟机中,mCookie为long[],其中第一个为oatFile,余下的为(o)dexFile
//            摘自源码:
//            constexpr size_t kOatFileIndex = 0;
//            constexpr size_t kDexFileIndexStart = 1;
            System.arraycopy(cookies, 1, longs, 0, longs.length);
            return longs;
        } else {
            //没有满足的情况
            Logger.log("bad mCookies at " + DexFileInfoCollecter.class.getName() + "#" + "parseMCookies(Object) :" + mCookies);
            return null;
        }


    }

    public HashMap<Long, DexFileInfo> dumpDexFileInfo() {
        HashMap<Long, DexFileInfo> dexs = new HashMap<Long, DexFileInfo>(dynLoadedDexInfo);
        Object dexPathList = RefInvoke.getFieldOjbect("dalvik.system.BaseDexClassLoader", pathClassLoader, "pathList");
        Object[] dexElements = (Object[]) RefInvoke.getFieldOjbect("dalvik.system.DexPathList", dexPathList, "dexElements");
        DexFile dexFile = null;
        for (int i = 0; i < dexElements.length; i++) {
            dexFile = (DexFile) RefInvoke.getFieldOjbect("dalvik.system.DexPathList$Element", dexElements[i], "dexFile");
            String mFileName = (String) RefInvoke.getFieldOjbect("dalvik.system.DexFile", dexFile, "mFileName");
            Object mCookie = RefInvoke.getFieldOjbect("dalvik.system.DexFile", dexFile, "mCookie");

            long[] mCookies = parseMCookies(mCookie);
            if (mCookies != null) {

                for (int index = 0; index < mCookies.length; ++index) {
                    if (mCookies[index] != 0) {
                        DexFileInfo dexinfo = new DexFileInfo(mFileName, mCookies[index], dexElements[i].toString(), pathClassLoader);
                        dexs.put(mCookies[index], dexinfo);
                    }
                }
            }
        }
        return dexs;
    }

    public String[] dumpLoadableClass(String dexPath) {
        int mCookie = Integer.parseInt(dexPath);
//		int mCookie = this.getCookie(dexPath);
        if (mCookie != 0) {
            return (String[]) RefInvoke.invokeStaticMethod("dalvik.system.DexFile", "getClassNameList", new Class[]{int.class},
                    new Object[]{mCookie});
        } else {
            Logger.log("the cookie is not right");
        }
        return null;

    }

    public void backsmaliDexFile(String filename, String dexPath) {
        File file = new File(filename);
        try {
            if (!file.exists())
                file.createNewFile();

            MemoryBackSmali.disassembleDexFile(Long.parseLong(dexPath), filename);
//			int mCookie = this.getCookie(dexPath);
//			if (mCookie != 0) {
//				MemoryBackSmali.disassembleDexFile(mCookie, filename);
//			} else {
//				Logger.log("the cookie is not right");
//			}


        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void dumpDexFile(String filename, long mCookie) {
        File file = new File(filename);
        try {
            if (!file.exists())
                file.createNewFile();
//			int mCookie = this.getCookie(dexPath);
            if (mCookie != 0) {
                FileOutputStream out = new FileOutputStream(file);
                ByteBuffer data = NativeFunction.dumpDexFileByCookie(mCookie, ModuleContext.getInstance().getApiLevel());
                data.order(ByteOrder.LITTLE_ENDIAN);
                byte[] buffer = new byte[8192];
                data.clear();
                while (data.hasRemaining()) {
                    int count = Math.min(buffer.length, data.remaining());
                    data.get(buffer, 0, count);
                    try {
                        out.write(buffer, 0, count);
                    } catch (IOException e1) {
                        e1.printStackTrace();
                    }
                }
            } else {
                Logger.log("the cookie is not right");
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 已被废弃
     *
     * @param dexPath
     * @return
     */
    private Object getCookie(String dexPath) {

        if (dynLoadedDexInfo.containsKey(dexPath)) {
            DexFileInfo dexFileInfo = dynLoadedDexInfo.get(dexPath);
            return dexFileInfo.getmCookie();
        } else {
            Object dexPathList = RefInvoke.getFieldOjbect("dalvik.system.BaseDexClassLoader", pathClassLoader, "pathList");
            Object[] dexElements = (Object[]) RefInvoke.getFieldOjbect("dalvik.system.DexPathList", dexPathList, "dexElements");
            DexFile dexFile = null;
            for (int i = 0; i < dexElements.length; i++) {
                dexFile = (DexFile) RefInvoke.getFieldOjbect("dalvik.system.DexPathList$Element", dexElements[i], "dexFile");
                String mFileName = (String) RefInvoke.getFieldOjbect("dalvik.system.DexFile", dexFile, "mFileName");
                if (mFileName.equals(dexPath)) {
                    return RefInvoke.getFieldOjbect("dalvik.system.DexFile", dexFile, "mCookie");
                }

            }
            return 0;
        }

    }

    private void setDefineClassLoader(long mCookie, ClassLoader classLoader) {
        Iterator<DexFileInfo> dexinfos = dynLoadedDexInfo.values().iterator();
        DexFileInfo info = null;
        while (dexinfos.hasNext()) {
            info = dexinfos.next();
            if (mCookie == info.getmCookie()) {
                if (info.getDefineClassLoader() == null)
                    info.setDefineClassLoader(classLoader);
            }
        }
    }

}