package de.robv.android.xposed;

import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
import static de.robv.android.xposed.XposedHelpers.getBooleanField;
import static de.robv.android.xposed.XposedHelpers.getIntField;
import static de.robv.android.xposed.XposedHelpers.getObjectField;
import static de.robv.android.xposed.XposedHelpers.setObjectField;
import static de.robv.android.xposed.XposedHelpers.setStaticObjectField;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.DateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import android.annotation.SuppressLint;
import android.app.ActivityThread;
import android.app.AndroidAppHelper;
import android.app.LoadedApk;
import android.content.ComponentName;
import android.content.pm.ApplicationInfo;
import android.content.res.CompatibilityInfo;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XResources;
import android.content.res.XResources.XTypedArray;
import android.os.Build;
import android.os.Process;
import android.util.Log;

import com.android.internal.os.RuntimeInit;
import com.android.internal.os.ZygoteInit;

import dalvik.system.PathClassLoader;
import de.robv.android.xposed.XC_MethodHook.MethodHookParam;
import de.robv.android.xposed.callbacks.XC_InitPackageResources;
import de.robv.android.xposed.callbacks.XC_InitPackageResources.InitPackageResourcesParam;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
import de.robv.android.xposed.callbacks.XCallback;

public final class XposedBridge {
    public static final String INSTALLER_PACKAGE_NAME = "de.robv.android.xposed.installer";
    public static int XPOSED_BRIDGE_VERSION;

    private static PrintWriter logWriter = null;
    // log for initialization of a few mods is about 500 bytes, so 2*20 kB (2*~350 lines) should be enough
    public static boolean disableResources = false;

    private static final Object[] EMPTY_ARRAY = new Object[0];
    public static final ClassLoader BOOTCLASSLOADER = ClassLoader.getSystemClassLoader();
    @SuppressLint("SdCardPath")
    public static final String BASE_DIR = "/system/xposed/";

    // built-in handlers
    private static final Map<Member, CopyOnWriteSortedSet<XC_MethodHook>> sHookedMethodCallbacks
    = new HashMap<Member, CopyOnWriteSortedSet<XC_MethodHook>>();
    private static final CopyOnWriteSortedSet<XC_LoadPackage> sLoadedPackageCallbacks
    = new CopyOnWriteSortedSet<XC_LoadPackage>();
    private static final CopyOnWriteSortedSet<XC_InitPackageResources> sInitPackageResourcesCallbacks
    = new CopyOnWriteSortedSet<XC_InitPackageResources>();

    /**
     * Called when native methods and other things are initialized, but before preloading classes etc.
     */
    private static void main(String[] args) {
        // the class the VM has been created for or null for the Zygote process
        String startClassName = getStartClassName();

        try {
            log("Loading Xposed (for " + (startClassName == null ? "Zygote" : startClassName) + ")...");
            if (startClassName == null) {
                // Zygote
                log("Running ROM '" + Build.DISPLAY + "' with fingerprint '" + Build.FINGERPRINT + "'");
            }

            if (initNative()) {
                if (startClassName == null) {
                    // Initializations for Zygote
                    initXbridgeZygote();
                }

                loadModules(startClassName);
            } else {
                log("Errors during native Xposed initialization");
            }
        } catch (Throwable t) {
            log("Errors during Xposed initialization");
            log(t);
        }

        // call the original startup code
        if (startClassName == null) {
            log("xposed: calling ZygoteInit");
            ZygoteInit.main(args);
        } else {
            log("xposed: calling RuntimeInit");
            RuntimeInit.main(args);
        }
    }

    private static native String getStartClassName();

    private static int extractIntPart(String str) {
        int result = 0, length = str.length();
        for (int offset = 0; offset < length; offset++) {
            char c = str.charAt(offset);
            if ('0' <= c && c <= '9')
                result = result * 10 + (c - '0');
            else
                break;
        }
        return result;
    }

    /**
     * Hook some methods which we want to create an easier interface for developers.
     */
    private static void initXbridgeZygote() throws Throwable {
        final HashSet<String> loadedPackagesInProcess = new HashSet<String>(1);

        // normal process initialization (for new Activity, Service,
        // BroadcastReceiver etc.)
        findAndHookMethod(ActivityThread.class, "hello", Integer.TYPE, Integer.TYPE, Integer.TYPE, Integer.TYPE, new XC_MethodHook() {
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    Log.e("xposed_bridge", "beforeHookedMethod, do nothing");
                }
            });
        findAndHookMethod(ActivityThread.class, "hello2", Integer.TYPE, Integer.TYPE, new XC_MethodHook() {
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    Log.e("xposed_bridge", "beforeHookedMethod 2, do nothing");
                }
            });
        findAndHookMethod(ActivityThread.class, "hello3", Integer.TYPE, Integer.TYPE, Integer.TYPE, Integer.TYPE,new XC_MethodHook() {
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    Log.e("xposed_bridge", "beforeHookedMethod 3, do nothing");
                }
            });
        findAndHookMethod(ActivityThread.class, "hello4", String.class, String.class, new XC_MethodHook() {
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    Log.e("xposed_bridge", "beforeHookedMethod 4, do nothing");
                }
            });
        findAndHookMethod(ActivityThread.class, "hello5", String.class, String.class, new XC_MethodHook() {
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                Log.e("xposed_bridge", "beforeHookedMethod 5, do nothing");
                Thread.dumpStack();
                // new Throwable().fillInStackTrace();
            }
        });

        findAndHookMethod(ActivityThread.class, "handleBindApplication", "android.app.ActivityThread.AppBindData", new XC_MethodHook() {
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    Log.e("xposed_bridge", "before handleBindApplication");
                    ActivityThread activityThread = (ActivityThread) param.thisObject;
                    ApplicationInfo appInfo = (ApplicationInfo) getObjectField(param.args[0], "appInfo");
                    ComponentName instrumentationName = (ComponentName) getObjectField(param.args[0], "instrumentationName");
                    if (instrumentationName != null) {
                        XposedBridge.log("Instrumentation detected, disabling framework for " + appInfo.packageName);
                        return;
                    }
                    CompatibilityInfo compatInfo = (CompatibilityInfo) getObjectField(param.args[0], "compatInfo");
                    if (appInfo.sourceDir == null)
                        return;

                    setObjectField(activityThread, "mBoundApplication", param.args[0]);
                    loadedPackagesInProcess.add(appInfo.packageName);
                    LoadedApk loadedApk = activityThread.getPackageInfoNoCheck(appInfo, compatInfo);
                    // XResources.setPackageNameForResDir(appInfo.packageName, loadedApk.getResDir());

                    LoadPackageParam lpparam = new LoadPackageParam(sLoadedPackageCallbacks);
                    lpparam.packageName = appInfo.packageName;
                    lpparam.processName = (String) getObjectField(param.args[0], "processName");
                    lpparam.classLoader = loadedApk.getClassLoader();
                    lpparam.appInfo = appInfo;
                    lpparam.isFirstApplication = true;
                    XC_LoadPackage.callAll(lpparam);

                    if (appInfo.packageName.equals(INSTALLER_PACKAGE_NAME))
                        hookXposedInstaller(lpparam.classLoader);
                }
            });


        // system thread initialization
        // findAndHookMethod("com.android.server.ServerThread", null,
        //                   Build.VERSION.SDK_INT < 19 ? "run" : "initAndLoop", new XC_MethodHook() {
        //                           @Override
        //                           protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        //                               loadedPackagesInProcess.add("android");

        //                               LoadPackageParam lpparam = new LoadPackageParam(sLoadedPackageCallbacks);
        //                               lpparam.packageName = "android";
        //                               lpparam.processName = "android"; // it's actually system_server, but other functions return this as well
        //                               lpparam.classLoader = BOOTCLASSLOADER;
        //                               lpparam.appInfo = null;
        //                               lpparam.isFirstApplication = true;
        //                               XC_LoadPackage.callAll(lpparam);
        //                           }
        //                       });

        // when a package is loaded for an existing process, trigger the callbacks as well
        // hookAllConstructors(LoadedApk.class, new XC_MethodHook() {
        //     @Override
        //     protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        //         LoadedApk loadedApk = (LoadedApk) param.thisObject;

        //         String packageName = loadedApk.getPackageName();
        //         XResources.setPackageNameForResDir(packageName, loadedApk.getResDir());
        //         if (packageName.equals("android") || !loadedPackagesInProcess.add(packageName))
        //             return;

        //         if ((Boolean) getBooleanField(loadedApk, "mIncludeCode") == false)
        //             return;

        //         LoadPackageParam lpparam = new LoadPackageParam(sLoadedPackageCallbacks);
        //         lpparam.packageName = packageName;
        //         lpparam.processName = AndroidAppHelper.currentProcessName();
        //         lpparam.classLoader = loadedApk.getClassLoader();
        //         lpparam.appInfo = loadedApk.getApplicationInfo();
        //         lpparam.isFirstApplication = false;
        //         XC_LoadPackage.callAll(lpparam);
        //     }
        // });

        disableResources = true;

    }

    private static void hookXposedInstaller(ClassLoader classLoader) {
        try {
            findAndHookMethod(INSTALLER_PACKAGE_NAME + ".XposedApp", classLoader, "getActiveXposedVersion",
                              XC_MethodReplacement.returnConstant(XPOSED_BRIDGE_VERSION));
        } catch (Throwable t) { XposedBridge.log(t); }
    }

    /**
     * Try to load all modules defined in <code>BASE_DIR/conf/modules.list</code>
     */
    private static void loadModules(String startClassName) throws IOException {
        BufferedReader apks = new BufferedReader(new FileReader(BASE_DIR + "conf/modules.list"));
        String apk;
        log(">>>load modules");
        while ((apk = apks.readLine()) != null) {
            log("load modules: "+apk);
            loadModule(apk, startClassName);
        }
        log("<<<load modules");
        apks.close();
    }

    /**
     * Load a module from an APK by calling the init(String) method for all classes defined
     * in <code>assets/xposed_init</code>.
     */
    @SuppressWarnings("deprecation")
    private static void loadModule(String apk, String startClassName) {
        log("Loading modules from " + apk);

        if (!new File(apk).exists()) {
            log("  File does not exist");
            return;
        }

        ClassLoader mcl = new PathClassLoader(apk, BOOTCLASSLOADER);
        InputStream is = mcl.getResourceAsStream("assets/xposed_init");
        if (is == null) {
            log("assets/xposed_init not found in the APK");
            return;
        }

        BufferedReader moduleClassesReader = new BufferedReader(new InputStreamReader(is));
        try {
            String moduleClassName;
            while ((moduleClassName = moduleClassesReader.readLine()) != null) {
                moduleClassName = moduleClassName.trim();
                if (moduleClassName.isEmpty() || moduleClassName.startsWith("#"))
                    continue;

                try {
                    log ("  Loading class " + moduleClassName);
                    Class<?> moduleClass = mcl.loadClass(moduleClassName);

                    if (!IXposedMod.class.isAssignableFrom(moduleClass)) {
                        log ("    This class doesn't implement any sub-interface of IXposedMod, skipping it");
                        continue;
                    } else if (disableResources && IXposedHookInitPackageResources.class.isAssignableFrom(moduleClass)) {
                        log ("    This class requires resource-related hooks (which are disabled), skipping it.");
                        continue;
                    }

                    // call the init(String) method of the module
                    final Object moduleInstance = moduleClass.newInstance();
                    if (startClassName == null) {
                        if (moduleInstance instanceof IXposedHookZygoteInit) {
                            IXposedHookZygoteInit.StartupParam param = new IXposedHookZygoteInit.StartupParam();
                            param.modulePath = apk;
                            ((IXposedHookZygoteInit) moduleInstance).initZygote(param);
                        }

                        if (moduleInstance instanceof IXposedHookLoadPackage)
                            hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance));

                        if (moduleInstance instanceof IXposedHookInitPackageResources)
                            hookInitPackageResources(new IXposedHookInitPackageResources.Wrapper((IXposedHookInitPackageResources) moduleInstance));
                    } else {
                        if (moduleInstance instanceof IXposedHookCmdInit) {
                            IXposedHookCmdInit.StartupParam param = new IXposedHookCmdInit.StartupParam();
                            param.modulePath = apk;
                            param.startClassName = startClassName;
                            ((IXposedHookCmdInit) moduleInstance).initCmdApp(param);
                        }
                    }
                } catch (Throwable t) {
                    log(t);
                }
            }
        } catch (IOException e) {
            log(e);
        } finally {
            try {
                is.close();
            } catch (IOException ignored) {}
        }
    }

    /**
     * Writes a message to the Xposed error log.
     *
     * <p>DON'T FLOOD THE LOG!!! This is only meant for error logging.
     * If you want to write information/debug messages, use logcat.
     *
     * @param text The log message.
     */
    public synchronized static void log(String text) {
        Log.i("Xposed", text);
    }

    /**
     * Logs a stack trace to the Xposed error log.
     *
     * <p>DON'T FLOOD THE LOG!!! This is only meant for error logging.
     * If you want to write information/debug messages, use logcat.
     *
     * @param t The Throwable object for the stack trace.
     */
    public synchronized static void log(Throwable t) {
        Log.i("Xposed", Log.getStackTraceString(t));
    }

    /**
     * Hook any method with the specified callback
     *
     * @param hookMethod The method to be hooked
     * @param callback
     */
    public static XC_MethodHook.Unhook hookMethod(Member hookMethod, XC_MethodHook callback) {
        if (!(hookMethod instanceof Method) && !(hookMethod instanceof Constructor<?>)) {
            throw new IllegalArgumentException("Only methods and constructors can be hooked: " + hookMethod.toString());
        } else if (hookMethod.getDeclaringClass().isInterface()) {
            throw new IllegalArgumentException("Cannot hook interfaces: " + hookMethod.toString());
        } else if (Modifier.isAbstract(hookMethod.getModifiers())) {
            throw new IllegalArgumentException("Cannot hook abstract methods: " + hookMethod.toString());
        }

        boolean newMethod = false;
        CopyOnWriteSortedSet<XC_MethodHook> callbacks;
        synchronized (sHookedMethodCallbacks) {
            callbacks = sHookedMethodCallbacks.get(hookMethod);
            if (callbacks == null) {
                callbacks = new CopyOnWriteSortedSet<XC_MethodHook>();
                sHookedMethodCallbacks.put(hookMethod, callbacks);
                newMethod = true;
            }
        }
        callbacks.add(callback);
        if (newMethod) {
            Class<?> declaringClass = hookMethod.getDeclaringClass();
            // int slot = (int) getIntField(hookMethod, "slot");

            Class<?>[] parameterTypes;
            Class<?> returnType;
            if (hookMethod instanceof Method) {
                parameterTypes = ((Method) hookMethod).getParameterTypes();
                returnType = ((Method) hookMethod).getReturnType();
            } else {
                parameterTypes = ((Constructor<?>) hookMethod).getParameterTypes();
                returnType = null;
            }

            AdditionalHookInfo additionalInfo = new AdditionalHookInfo(callbacks);
            hookMethodNative(hookMethod, additionalInfo);
        }

        return callback.new Unhook(hookMethod);
    }

    /**
     * Removes the callback for a hooked method
     * @param hookMethod The method for which the callback should be removed
     * @param callback The reference to the callback as specified in {@link #hookMethod}
     */
    public static void unhookMethod(Member hookMethod, XC_MethodHook callback) {
        CopyOnWriteSortedSet<XC_MethodHook> callbacks;
        synchronized (sHookedMethodCallbacks) {
            callbacks = sHookedMethodCallbacks.get(hookMethod);
            if (callbacks == null)
                return;
        }
        callbacks.remove(callback);
    }

    public static Set<XC_MethodHook.Unhook> hookAllMethods(Class<?> hookClass, String methodName, XC_MethodHook callback) {
        Set<XC_MethodHook.Unhook> unhooks = new HashSet<XC_MethodHook.Unhook>();
        for (Member method : hookClass.getDeclaredMethods())
            if (method.getName().equals(methodName))
                unhooks.add(hookMethod(method, callback));
        return unhooks;
    }

    public static Set<XC_MethodHook.Unhook> hookAllConstructors(Class<?> hookClass, XC_MethodHook callback) {
        Set<XC_MethodHook.Unhook> unhooks = new HashSet<XC_MethodHook.Unhook>();
        for (Member constructor : hookClass.getDeclaredConstructors())
            unhooks.add(hookMethod(constructor, callback));
        return unhooks;
    }

    /**
     * This method is called as a replacement for hooked methods.
     */
    public static Object handleHookedMethod(Member method, Object additionalInfoObj,
                                             Object thisObject, Object[] args) throws Throwable {
        Log.e("xposed_bridge", "handleHookedMethod");
        AdditionalHookInfo additionalInfo = (AdditionalHookInfo) additionalInfoObj;
        Log.e("xposed_bridge", "method: "+method+" additionalInfo: "+additionalInfoObj+" this: "+ thisObject);
        for (Object o: args) {
            Log.e("xposed_bridge", "args:"+o);
        }
        Log.e("xposed_bridge","handleHookedMethod 1");
        Object[] callbacksSnapshot = additionalInfo.callbacks.getSnapshot();
        final int callbacksLength = callbacksSnapshot.length;
        if (callbacksLength == 0) {
            Log.e("xposed_bridge"," handleHookedMethod 2");
            return invokeOriginalMethod(method, thisObject, args);
        }

        Log.e("xposed_bridge","handleHookedMethod 3");
        MethodHookParam param = new MethodHookParam();
        param.method = method;
        param.thisObject = thisObject;
        param.args = args;

        // // call "before method" callbacks
        int beforeIdx = 0;
        do {
            try {
                Log.e("xposed_bridge","handleHookedMethod 4");
                ((XC_MethodHook) callbacksSnapshot[beforeIdx]).beforeHookedMethod(param);
            } catch (Throwable t) {
                XposedBridge.log(t);

                // reset result (ignoring what the unexpectedly exiting callback did)
                param.setResult(null);
                param.returnEarly = false;
                continue;
            }

            if (param.returnEarly) {
                // skip remaining "before" callbacks and corresponding "after" callbacks
                beforeIdx++;
                break;
            }
        } while (++beforeIdx < callbacksLength);

        Log.e("xposed_bridge","handleHookedMethod 5");
        if (!param.returnEarly) {
            Log.e("xposed_bridge","handleHookedMethod 6");
            param.setResult(invokeOriginalMethod(method, param.thisObject, param.args));
        }

        int afterIdx = beforeIdx - 1;
        do {
            Object lastResult =  param.getResult();
            Throwable lastThrowable = param.getThrowable();

            try {
                Log.e("xposed_bridge","handleHookedMethod 7");
                ((XC_MethodHook) callbacksSnapshot[afterIdx]).afterHookedMethod(param);
            } catch (Throwable t) {
                XposedBridge.log(t);

                // reset to last result (ignoring what the unexpectedly exiting callback did)
                if (lastThrowable == null)
                    param.setResult(lastResult);
                else
                    param.setThrowable(lastThrowable);
            }
        } while (--afterIdx >= 0);

        Log.e("xposed_bridge","handleHookedMethod 8");
        // // return
        if (param.hasThrowable()) {
            throw param.getThrowable();
        } else {
            Log.e("xposed_bridge","handleHookedMethod 9");
            return param.getResult();
        }
    }

    /**
     * Get notified when a package is loaded. This is especially useful to hook some package-specific methods.
     */
    public static XC_LoadPackage.Unhook hookLoadPackage(XC_LoadPackage callback) {
        synchronized (sLoadedPackageCallbacks) {
            sLoadedPackageCallbacks.add(callback);
        }
        return callback.new Unhook();
    }

    public static void unhookLoadPackage(XC_LoadPackage callback) {
        synchronized (sLoadedPackageCallbacks) {
            sLoadedPackageCallbacks.remove(callback);
        }
    }

    /**
     * Get notified when the resources for a package are loaded. In callbacks, resource replacements can be created.
     * @return
     */
    public static XC_InitPackageResources.Unhook hookInitPackageResources(XC_InitPackageResources callback) {
        synchronized (sInitPackageResourcesCallbacks) {
            sInitPackageResourcesCallbacks.add(callback);
        }
        return callback.new Unhook();
    }

    public static void unhookInitPackageResources(XC_InitPackageResources callback) {
        synchronized (sInitPackageResourcesCallbacks) {
            sInitPackageResourcesCallbacks.remove(callback);
        }
    }

    private native static boolean initNative();

    private native synchronized static void hookMethodNative(Member method,Object additionalInfo);

    private native static Object invokeOriginalMethod(Member method, Object thisObject, Object[] args);

    public static class CopyOnWriteSortedSet<E> {
        private transient volatile Object[] elements = EMPTY_ARRAY;

        public synchronized boolean add(E e) {
            int index = indexOf(e);
            if (index >= 0)
                return false;

            Object[] newElements = new Object[elements.length + 1];
            System.arraycopy(elements, 0, newElements, 0, elements.length);
            newElements[elements.length] = e;
            Arrays.sort(newElements);
            elements = newElements;
            return true;
        }

        public synchronized boolean remove(E e) {
            int index = indexOf(e);
            if (index == -1)
                return false;

            Object[] newElements = new Object[elements.length - 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index, elements.length - index - 1);
            elements = newElements;
            return true;
        }

        private int indexOf(Object o) {
            for (int i = 0; i < elements.length; i++) {
                if (o.equals(elements[i]))
                    return i;
            }
            return -1;
        }

        public Object[] getSnapshot() {
            return elements;
        }
    }

    private static class AdditionalHookInfo {
        final CopyOnWriteSortedSet<XC_MethodHook> callbacks;
        private AdditionalHookInfo(CopyOnWriteSortedSet<XC_MethodHook> callbacks) {
            this.callbacks = callbacks;
        }
    }
}