package com.antfortune.freeline;

import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;

import com.antfortune.freeline.gradle.GradleDynamic;
import com.antfortune.freeline.resources.MonkeyPatcher;
import com.antfortune.freeline.util.ActivityManager;
import com.antfortune.freeline.util.AppUtils;
import com.antfortune.freeline.util.DexUtils;
import com.antfortune.freeline.util.FileUtils;
import com.antfortune.freeline.util.NativeUtils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import dalvik.system.PathClassLoader;

/**
 * Created by xianying on 16/3/16.
 */
public class FreelineCore {

    private static final String TAG = "Freeline";

    private static final String DYNAMIC_INFO_FILE_NAME = "FREELINE_DYNAMIC_INFO";

    private static final String SYNC_INFO_FILE_NAME = "FREELINE_SYNC_INFO";

    public static final String DEFAULT_PACKAGE_ID = "base-res.key";

    private static final String DYNAMIC_INFO_DEX_PATH_KEY = "dynamic_dex_key";

    private static final String DYNAMIC_INFO_DEX_DIR_KEY = "dynamic_dex_dir_key";

    private static final String DYNAMIC_INFO_OPT_PATH_KEY = "dynamic_opt_key";

    private static long sApkBuildFlag = 0;

    private static Application sApplication;

    private static Application sRealApplication;

    private static IDynamic sDynamic;

    public static void init(Application app, Application realApplication) {
        sRealApplication = realApplication;
        init(app, new GradleDynamic(app));
    }

    @Deprecated
    public static void init(Application app) {
        init(app, new GradleDynamic(app));
    }

    public static void init(Application app, IDynamic dynamicImpl) {
        Log.i(TAG, "freeline start initial process...");
        sApplication = app;
        setDynamicImpl(dynamicImpl);

        // 子进程也可以应用 patch
        //if (AppUtils.isApkDebugable(app) && AppUtils.isMainProcess(app)) {
        if (AppUtils.isApkDebugable(app)) {
            Log.i(TAG, "freeline init application");
            ActivityManager.initApplication(app);
            MonkeyPatcher.monkeyPatchApplication(app, app, sRealApplication, null);

            try {
                Object mPackageInfo = getPackageInfo(app);
                Field field = mPackageInfo.getClass().getDeclaredField("mClassLoader");
                field.setAccessible(true);
                PathClassLoader origin = (PathClassLoader) field.get(mPackageInfo);

                if (checkVersionChange()) {
                    Log.i(TAG, "the apk has recover, delete cache");
                    clearDynamicCache();
                    clearSyncCache();
                } else {
                    Log.i(TAG, "start to inject dex...");
                    injectDex(origin);
                    Log.i(TAG, "start to inject resources...");
                    injectResources();
                }

                Log.i(TAG, "start to load hackload.dex...");
                injectHackDex(app, origin);
                Log.i(TAG, "start to inject native lib...");
                injectHackNativeLib(app,origin);
            } catch (Exception e) {
                printStackTrace(e);
            }

            Log.i(TAG, "freeline init server");
            startLongLinkServer();
        }
    }

    public static void setDynamicImpl(IDynamic dynamicImpl) {
        sDynamic = dynamicImpl;
    }

    private static void startLongLinkServer() {
        Intent intent = new Intent(sApplication, FreelineService.class);
        sApplication.startService(intent);
    }

    private static String getDynamicDexPath() {
        return getDynamicInfoSp().getString(DYNAMIC_INFO_DEX_PATH_KEY, null);
    }

    private static String getDynamicDexDirPath() {
        return getDynamicInfoSp().getString(DYNAMIC_INFO_DEX_DIR_KEY, null);
    }

    private static String getDynamicDexOptPath() {
        return getDynamicInfoSp().getString(DYNAMIC_INFO_OPT_PATH_KEY, null);
    }

    public static void clearDynamicCache() {
        getDynamicInfoSp().edit().clear().commit();
        FileUtils.rm(new File(getDynamicInfoTempDir()));
        Log.i(TAG, "clear dynamic info sp cache");
    }

    public static void clearSyncCache() {
        getSyncInfoSp().edit().clear().commit();
        Log.i(TAG, "clear sync info sp cache");
    }

    public static long getBuildTime(Context context) {
        // 使用 ApkBuildFlag 作为基点进行对比,判断应用版本是否匹配
        long buildTime = getApkBuildFlag();
        Log.i(TAG, "Build Time is: " + buildTime);
        return buildTime;
    }

    public static long getApkBuildFlag() {
        if (sApkBuildFlag == 0) {
            try {
                InputStream is = sApplication.getAssets().open("apktime");
                int size = is.available();

                // Read the entire asset into a local byte buffer.
                byte[] buffer = new byte[size];
                is.read(buffer);
                is.close();

                String text = new String(buffer, "GB2312");
                Log.i(TAG, "ext:" + text);
                sApkBuildFlag = Long.parseLong(text);
            } catch (Exception e) {
                FreelineCore.printStackTrace(e);
            }
        }
        return sApkBuildFlag;
    }

    private static void copyAssets(Context context, String assetName, String strOutFileName) throws IOException {
        InputStream myInput;
        OutputStream myOutput = new FileOutputStream(strOutFileName);
        myInput = context.getAssets().open(assetName);
        byte[] buffer = new byte[1024];
        int length = myInput.read(buffer);
        while (length > 0) {
            myOutput.write(buffer, 0, length);
            length = myInput.read(buffer);
        }
        myOutput.flush();
        myInput.close();
        myOutput.close();
    }


    private static String getDynamicCacheDir() {
        File dir = new File(sApplication.getCacheDir(), "hack");
        if (!dir.exists()) {
            dir.mkdirs();
        }
        return dir.getAbsolutePath();
    }

    private static long getDynamicTime() {
        // 使用 ApkBuildFlag 作为基点进行对比,判断应用版本是否匹配
        long dynamicTime = getDynamicInfoSp().getLong("dynamicTime", getApkBuildFlag());
        Log.i(TAG, "Dynamic Time is: " + dynamicTime);
        return dynamicTime;
    }

    private static boolean checkVersionChange() {
        return getBuildTime(sApplication) > getDynamicTime();
    }


    private static Object getPackageInfo(Application app) throws NoSuchFieldException,
            IllegalArgumentException, IllegalAccessException {
        Context contextImpl = app.getBaseContext();
        Field mPackageInfoField = contextImpl.getClass().getDeclaredField(
                "mPackageInfo");
        mPackageInfoField.setAccessible(true);
        Object mPackageInfo = mPackageInfoField.get(contextImpl);
        return mPackageInfo;
    }

    private static void injectHackDex(Context context, PathClassLoader origin) {
        File hackDex = new File(getDynamicCacheDir(), "hackload.dex");
        if (!hackDex.exists() || hackDex.length() < 100) {
            try {
                copyAssets(context, "hackload.dex", hackDex.getAbsolutePath());
                Log.i(TAG, "copy hackload dex from assets success");
            } catch (Exception e) {
                printStackTrace(e);
            }
        }
        if (hackDex.exists() && hackDex.length() > 100) {
            File opt = new File(getDynamicCacheDir(), "opt");
            if (!opt.exists()) {
                opt.mkdirs();
            }
            DexUtils.inject(origin, hackDex, opt);
            Log.i(TAG, "load hackload,dex size:" + hackDex.length());
        }
    }

    private static void injectHackNativeLib(Context context, PathClassLoader classLoader) {
        // 修复so patch不生效问题
        try {
            NativeUtils.installNativeLibraryPath(classLoader, new File(getDynamicNativeDir()), false);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        //NativeUtils.injectHackNativeLib(getDynamicNativeDir(), classLoader);
    }


    private static void injectDex(PathClassLoader origin) {
        String dexDirPath = getDynamicDexDirPath();
        if (!TextUtils.isEmpty(dexDirPath)) {
            File dexDir = new File(dexDirPath);
            if (dexDir.isDirectory()) {
                File[] dexFiles = dexDir.listFiles();

                if (dexFiles.length > 0) {
                    File opt = new File(getDynamicDexOptPath());
                    if (!opt.exists()) {
                        opt.mkdirs();
                    }
                    for (File dexFile : dexFiles) {
                        String dirName = generateStringMD5(dexFile.getName());
                        File dexOptDir = new File(opt, dirName);
                        if (!dexOptDir.exists()) {
                            dexOptDir.mkdirs();
                        }
                        DexUtils.inject(origin, dexFile, dexOptDir);
                    }
                    Log.i(TAG, "find increment package");
                }
            }
        }
    }

    public static void injectResources() {
        Map<String, ?> map = getDynamicInfoSp().getAll();
        Log.i(TAG, "dynamicInfoSp: " + map.toString());
        HashMap<String, String> resMap = new HashMap<String, String>();
        for (String key : map.keySet()) {
            if (key.contains("-")) {
                resMap.put(key, (String) map.get(key));
            }
        }
        Log.i(TAG, "resMap: " + resMap.toString());
        if (!resMap.isEmpty()) {
            applyDynamicRes(resMap);
        }
    }

    public static boolean applyDynamicDex(String dexFileStr, String dexOptDir) {
        Log.i(TAG, "apply dynamicDex " + dexFileStr);
        SharedPreferences sp = getDynamicInfoSp();
        SharedPreferences.Editor editor = sp.edit();
        //editor.putString(DYNAMIC_INFO_DEX_PATH_KEY, dexFileStr);
        editor.putString(DYNAMIC_INFO_DEX_DIR_KEY, dexFileStr);
        editor.putString(DYNAMIC_INFO_OPT_PATH_KEY, dexOptDir);
        editor.commit();
        return true;
    }

    public static void printStackTrace(Throwable aThrowable) {
        Writer result = new StringWriter();
        PrintWriter printWriter = new PrintWriter(result);
        aThrowable.printStackTrace(printWriter);
        String resultStr = result.toString();
        Log.e(TAG, resultStr);
    }


    public static String getDynamicResPath(String packageId) {
        return getDynamicInfoSp().getString(getDynamicResPathKey(packageId), null);
    }

    private static String getDynamicResPathKey(String packageId) {
        return packageId + ".key";
    }

    public static long getLastDynamicSyncId() {
        return getSyncInfoSp().getLong("lastSync", 0);
    }

    public static void saveLastDynamicSyncId(long sync) {
        getSyncInfoSp().edit().putLong("lastSync", sync).commit();
    }

    private static SharedPreferences getDynamicInfoSp() {
        return sApplication.getBaseContext().getSharedPreferences(DYNAMIC_INFO_FILE_NAME, Context.MODE_PRIVATE);
    }

    private static SharedPreferences getSyncInfoSp() {
        return sApplication.getBaseContext().getSharedPreferences(SYNC_INFO_FILE_NAME, Context.MODE_PRIVATE);
    }

    public static String getBundleFilePathByPackageId(String packageId) {
        if (sDynamic != null) {
            return sDynamic.getOriginResPath(packageId);
        }
        return null;
    }

    private static boolean applyDynamicRes(HashMap<String, String> dynamicRes) {
        if (sDynamic != null) {
            return sDynamic.applyDynamicRes(dynamicRes);
        }
        return false;
    }

    public static Application getRealApplication() {
        return sRealApplication;
    }

    public static void updateDynamicTime() {
        // 使用 ApkBuildFlag 作为基点进行对比,判断应用版本是否匹配
        long dynamicTime = getApkBuildFlag();
        Log.i(TAG, "update dynamic time: " + dynamicTime);
        getDynamicInfoSp().edit().putLong("dynamicTime", dynamicTime).commit();
    }

    /***
     * packagid + newResPath
     *
     * @param dynamicRes
     */
    public static boolean saveDynamicResInfo(HashMap<String, String> dynamicRes) {
        boolean result = true;
        SharedPreferences sp = getDynamicInfoSp();
        SharedPreferences.Editor editor = sp.edit();
        for (String packageId : dynamicRes.keySet()) {
            String pendingPath = dynamicRes.get(packageId);
            editor.putString(getDynamicResPathKey(packageId), pendingPath);
        }
        editor.commit();
        Log.i(TAG, "apply res :" + dynamicRes);
        injectResources();
        return result;
    }


    public static String getDynamicInfoTempDir() {
        File dir = new File(sApplication.getCacheDir(), "temp");
        if (!dir.exists()) {
            dir.mkdirs();
        }
        return dir.getAbsolutePath();
    }

    public static String getDynamicDexDir() {
        File dir = new File(getDynamicInfoTempDir(), "dex");
        if (!dir.exists()) {
            dir.mkdirs();
        }
        return dir.getAbsolutePath();
    }

    public static String getDynamicNativeDir() {
        File dir = new File(getDynamicInfoTempDir(), "native");
        if (!dir.exists()) {
            dir.mkdirs();
        }
        return dir.getAbsolutePath();
    }

    public static String getDynamicInfoTempPath(String packageId) {
        File dir;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            dir = new File(getDynamicInfoTempDir(), packageId + ".jar");
        } else {
            dir = new File(getDynamicInfoTempDir(), packageId);
        }
        return dir.getAbsolutePath();
    }

    public static String getUuid() {
        return String.valueOf(getApkBuildFlag());
    }

    private static String generateStringMD5(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] bytes = md.digest(input.getBytes());
            StringBuilder sb = new StringBuilder();
            for (byte aByte : bytes) {
                sb.append(Integer.toHexString((aByte & 0xFF) | 0x100).substring(1, 3));
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            Log.e(TAG, "MD5 algorithm not found.");
            return input;
        }
    }

    public static void clearResourcesCache() {
        if (sDynamic != null) {
            sDynamic.clearResourcesCache();
        }
    }

    public static Application getApplication() {
        return sApplication;
    }

    public static void updateActivity(String bundleName, String path) {
        Intent intent = new Intent();
        intent.setAction("android.intent.action.FreelineReceiver");
        intent.putExtra(FreelineReceiver.UUID, getUuid());
        intent.putExtra(FreelineReceiver.ACTION_KEY, FreelineReceiver.ACTION_UPDATE_ACTIVITY);
        intent.putExtra(FreelineReceiver.SP_KEY, bundleName);
        intent.putExtra(FreelineReceiver.SP_VALUE, path);
        sApplication.sendBroadcast(intent);
    }

    public static void restartApplication(String bundleName, String path, String dexPath, String dirPath) {
        Intent intent = new Intent();
        intent.setAction("android.intent.action.FreelineReceiver");
        intent.putExtra(FreelineReceiver.UUID, getUuid());
        intent.putExtra(FreelineReceiver.ACTION_KEY, FreelineReceiver.ACTION_RESTART_APPLICATION);
        intent.putExtra(FreelineReceiver.SP_KEY, bundleName);
        intent.putExtra(FreelineReceiver.SP_VALUE, path);
        intent.putExtra(FreelineReceiver.DEX_VALUE, dexPath);
        intent.putExtra(FreelineReceiver.OPT_VALUE, dirPath);
        sApplication.sendBroadcast(intent);
    }

    @Deprecated
    public static void saveDynamicInfo(String bundleName, String path) {
        Intent intent = new Intent();
        intent.setAction("android.intent.action.FreelineReceiver");
        intent.putExtra(FreelineReceiver.UUID, getUuid());
        intent.putExtra(FreelineReceiver.ACTION_KEY, FreelineReceiver.ACTION_SAVE_DYNAMIC_INFO);
        intent.putExtra(FreelineReceiver.SP_KEY, bundleName);
        intent.putExtra(FreelineReceiver.SP_VALUE, path);
        sApplication.sendBroadcast(intent);
    }

}