package me.iacn.biliroaming;

import android.content.Context;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;

import de.robv.android.xposed.XposedHelpers.ClassNotFoundError;

import static de.robv.android.xposed.XposedHelpers.findClass;
import static me.iacn.biliroaming.Constant.TAG;

/**
 * Created by iAcn on 2019/4/5
 * Email [email protected]
 */
public class BiliBiliPackage {

    private static volatile BiliBiliPackage sInstance;

    private ClassLoader mClassLoader;
    private Map<String, String> mHookInfo;

    private WeakReference<Class<?>> bangumiApiResponseClass;
    private WeakReference<Class<?>> fastJsonClass;
    private WeakReference<Class<?>> bangumiUniformSeasonClass;
    private WeakReference<Class<?>> themeHelperClass;

    private boolean mHasModulesInResult;

    private BiliBiliPackage() {
    }

    public static BiliBiliPackage getInstance() {
        if (sInstance == null) {
            synchronized (BiliBiliPackage.class) {
                if (sInstance == null) {
                    sInstance = new BiliBiliPackage();
                }
            }
        }
        return sInstance;
    }

    void init(ClassLoader classLoader, Context context) {
        this.mClassLoader = classLoader;

        readHookInfo(context);
        if (checkHookInfo()) {
            writeHookInfo(context);
        }
    }

    public String retrofitResponse() {
        return mHookInfo.get("class_retrofit_response");
    }

    public String fastJsonParse() {
        return mHookInfo.get("method_fastjson_parse");
    }

    public String colorArray() {
        return mHookInfo.get("field_color_array");
    }

    public String themeListClickListener() {
        return mHookInfo.get("class_theme_list_click");
    }

    public Class<?> bangumiApiResponse() {
        bangumiApiResponseClass = checkNullOrReturn(bangumiApiResponseClass,
                "com.bilibili.bangumi.data.common.api.BangumiApiResponse");
        return bangumiApiResponseClass.get();
    }

    public Class<?> bangumiUniformSeason() {
        if (bangumiUniformSeasonClass == null || bangumiUniformSeasonClass.get() == null) {
            Class<?> clazz = findClass(
                    "com.bilibili.bangumi.data.page.detail.entity.BangumiUniformSeason", mClassLoader);
            bangumiUniformSeasonClass = new WeakReference<>(clazz);

            try {
                clazz.getField("modules");
                mHasModulesInResult = true;
            } catch (NoSuchFieldException ignored) {
            }
        }
        return bangumiUniformSeasonClass.get();
    }

    public Class<?> fastJson() {
        fastJsonClass = checkNullOrReturn(fastJsonClass, mHookInfo.get("class_fastjson"));
        return fastJsonClass.get();
    }

    public Class<?> themeHelper() {
        themeHelperClass = checkNullOrReturn(themeHelperClass, "tv.danmaku.bili.ui.theme.a");
        return themeHelperClass.get();
    }

    public boolean hasModulesInResult() {
        Log.d(TAG, "hasModulesInResult: " + mHasModulesInResult);
        return mHasModulesInResult;
    }

    private WeakReference<Class<?>> checkNullOrReturn(WeakReference<Class<?>> clazz, String className) {
        if (clazz == null || clazz.get() == null) {
            clazz = new WeakReference<>(findClass(className, mClassLoader));
        }
        return clazz;
    }

    private void readHookInfo(Context context) {
        try {
            File hookInfoFile = new File(context.getCacheDir(), Constant.HOOK_INFO_FILE_NAME);
            Log.d(TAG, "Reading hook info: " + hookInfoFile);
            long startTime = System.currentTimeMillis();

            if (hookInfoFile.isFile() && hookInfoFile.canRead()) {
                long lastUpdateTime = context.getPackageManager().getPackageInfo(Constant.BILIBILI_PACKAGENAME, 0).lastUpdateTime;
                ObjectInputStream stream = new ObjectInputStream(new FileInputStream(hookInfoFile));

                if (stream.readLong() == lastUpdateTime)
                    mHookInfo = (Map<String, String>) stream.readObject();
            }

            long endTime = System.currentTimeMillis();
            Log.d(TAG, "Read hook info completed: take " + (endTime - startTime) + " ms");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * @return Whether to update the serialization file.
     */
    private boolean checkHookInfo() {
        boolean needUpdate = false;

        if (mHookInfo == null) {
            mHookInfo = new HashMap<>();
            needUpdate = true;
        }
        if (!mHookInfo.containsKey("class_retrofit_response")) {
            mHookInfo.put("class_retrofit_response", findRetrofitResponseClass());
            needUpdate = true;
        }
        if (!mHookInfo.containsKey("class_fastjson")) {
            Class<?> fastJsonClass = findFastJsonClass();
            boolean notObfuscated = "JSON".equals(fastJsonClass.getSimpleName());
            mHookInfo.put("class_fastjson", fastJsonClass.getName());
            mHookInfo.put("method_fastjson_parse", notObfuscated ? "parseObject" : "a");
            needUpdate = true;
        }
        if (!mHookInfo.containsKey("field_color_array")) {
            mHookInfo.put("field_color_array", findColorArrayField());
            needUpdate = true;
        }
        if (!mHookInfo.containsKey("class_theme_list_click")) {
            mHookInfo.put("class_theme_list_click", findThemeListClickClass());
            needUpdate = true;
        }

        Log.d(TAG, "Check hook info completed: needUpdate = " + needUpdate);
        return needUpdate;
    }

    private void writeHookInfo(Context context) {
        try {
            File hookInfoFile = new File(context.getCacheDir(), Constant.HOOK_INFO_FILE_NAME);
            long lastUpdateTime = context.getPackageManager().getPackageInfo(Constant.BILIBILI_PACKAGENAME, 0).lastUpdateTime;

            if (hookInfoFile.exists()) hookInfoFile.delete();

            ObjectOutputStream stream = new ObjectOutputStream(new FileOutputStream(hookInfoFile));
            stream.writeLong(lastUpdateTime);
            stream.writeObject(mHookInfo);
            stream.flush();
            stream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        Log.d(TAG, "Write hook info completed");
    }

    private String findRetrofitResponseClass() {
        Method[] methods = bangumiApiResponse().getMethods();
        for (Method method : methods) {
            if ("extractResult".equals(method.getName())) {
                Class<?> responseClass = method.getParameterTypes()[0];
                return responseClass.getName();
            }
        }
        return null;
    }

    private Class<?> findFastJsonClass() {
        Class<?> clazz;
        try {
            clazz = findClass("com.alibaba.fastjson.JSON", mClassLoader);
        } catch (ClassNotFoundError e) {
            clazz = findClass("com.alibaba.fastjson.a", mClassLoader);
        }
        return clazz;
    }

    private String findColorArrayField() {
        Field[] fields = themeHelper().getDeclaredFields();
        for (Field field : fields) {
            if (field.getType() == SparseArray.class) {
                ParameterizedType genericType = (ParameterizedType) field.getGenericType();
                Type[] types = genericType.getActualTypeArguments();
                if ("int[]".equals(types[0].toString())) {
                    return field.getName();
                }
            }
        }
        return null;
    }

    private String findThemeListClickClass() {
        Class<?> themeStoreActivityClass = findClass("tv.danmaku.bili.ui.theme.ThemeStoreActivity", mClassLoader);
        for (Class<?> innerClass : themeStoreActivityClass.getDeclaredClasses()) {
            for (Class<?> interfaceClass : innerClass.getInterfaces()) {
                if (interfaceClass == View.OnClickListener.class) {
                    return innerClass.getName();
                }
            }
        }
        return null;
    }
}