package com.topjohnwu.magisk;

import static com.topjohnwu.magisk.BuildConfig.APPLICATION_ID;

import android.app.AppComponentFactory;
import android.app.Application;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;

import com.topjohnwu.magisk.utils.APKInstall;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;

import io.michaelrocks.paranoid.Obfuscate;

@Obfuscate
@SuppressWarnings("ResultOfMethodCallIgnored")
public class DynLoad {

    static Object componentFactory;
    static ClassLoader activeClassLoader = DynLoad.class.getClassLoader();

    static StubApk.Data createApkData() {
        var data = new StubApk.Data();
        data.setVersion(BuildConfig.STUB_VERSION);
        data.setClassToComponent(new HashMap<>());
        data.setRootService(DelegateRootService.class);
        return data;
    }

    static void attachContext(Object o, Context context) {
        if (!(o instanceof ContextWrapper))
            return;
        try {
            Method m = ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class);
            m.setAccessible(true);
            m.invoke(o, context);
        } catch (Exception ignored) { /* Impossible */ }
    }

    // Dynamically load APK from internal, external storage, or previous app
    static AppClassLoader loadApk(Context context) {
        File apk = StubApk.current(context);
        File update = StubApk.update(context);

        if (update.exists()) {
            // Rename from update
            update.renameTo(apk);
        }

        // Copy from external for easier development
        if (BuildConfig.DEBUG) {
            File external = new File(context.getExternalFilesDir(null), "magisk.apk");
            if (external.exists()) {
                try {
                    var in = new FileInputStream(external);
                    var out = new FileOutputStream(apk);
                    try (in; out) {
                        APKInstall.transfer(in, out);
                    }
                } catch (IOException e) {
                    Log.e(DynLoad.class.getSimpleName(), "", e);
                    apk.delete();
                } finally {
                    external.delete();
                }
            }
        }

        if (apk.exists()) {
            return new AppClassLoader(apk);
        }

        // If no APK is loaded, attempt to copy from previous app
        if (!context.getPackageName().equals(APPLICATION_ID)) {
            try {
                var info = context.getPackageManager().getApplicationInfo(APPLICATION_ID, 0);
                var src = new FileInputStream(info.sourceDir);
                var out = new FileOutputStream(apk);
                try (src; out) {
                    APKInstall.transfer(src, out);
                }
                return new AppClassLoader(apk);
            } catch (PackageManager.NameNotFoundException ignored) {
            } catch (IOException e) {
                Log.e(DynLoad.class.getSimpleName(), "", e);
                apk.delete();
            }
        }

        return null;
    }

    // Dynamically load APK and create the Application instance from the loaded APK
    static Application createAndSetupApp(Application context) {
        // On API >= 29, AppComponentFactory will replace the ClassLoader for us
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
            replaceClassLoader(context);

        // noinspection InlinedApi
        int flags = PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES
                | PackageManager.GET_PROVIDERS | PackageManager.GET_RECEIVERS
                | PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
        var pm = context.getPackageManager();

        final PackageInfo info;
        try {
            // noinspection WrongConstant
            info = pm.getPackageInfo(context.getPackageName(), flags);
        } catch (PackageManager.NameNotFoundException e) {
            // Impossible
            throw new RuntimeException(e);
        }

        File apk = StubApk.current(context);

        final var cl = loadApk(context);
        if (cl != null) try {
            // noinspection WrongConstant
            var pkgInfo = pm.getPackageArchiveInfo(apk.getPath(), flags);
            cl.updateComponentMap(info, pkgInfo);

            var appInfo = pkgInfo.applicationInfo;

            var data = createApkData();
            var map = data.getClassToComponent();
            // Create the inverse mapping (class to component name)
            for (var e : cl.mapping.entrySet()) {
                map.put(e.getValue(), e.getKey());
            }

            // Create the receiver Application
            var app = (Application) cl.loadClass(appInfo.className)
                    .getConstructor(Object.class)
                    .newInstance(data.getObject());

            // Create the receiver component factory
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && componentFactory != null) {
                Object factory = cl.loadClass(appInfo.appComponentFactory).newInstance();
                var delegate = (DelegateComponentFactory) componentFactory;
                delegate.receiver = (AppComponentFactory) factory;
            }

            activeClassLoader = cl;

            // Send real application to attachBaseContext
            attachContext(app, context);

            return app;
        } catch (Exception e) {
            Log.e(DynLoad.class.getSimpleName(), "", e);
            apk.delete();
        }

        activeClassLoader = new StubClassLoader(info);
        return null;
    }

    // Replace LoadedApk mClassLoader
    private static void replaceClassLoader(Context context) {
        // Get ContextImpl
        while (context instanceof ContextWrapper) {
            context = ((ContextWrapper) context).getBaseContext();
        }

        try {
            Field mInfo = context.getClass().getDeclaredField("mPackageInfo");
            mInfo.setAccessible(true);
            Object loadedApk = mInfo.get(context);
            assert loadedApk != null;
            Field mcl = loadedApk.getClass().getDeclaredField("mClassLoader");
            mcl.setAccessible(true);
            mcl.set(loadedApk, new DelegateClassLoader());
        } catch (Exception e) {
            // Actually impossible as this method is only called on API < 29,
            // and API 21 - 28 do not restrict access to these fields.
            Log.e(DynLoad.class.getSimpleName(), "", e);
        }
    }
}
