package me.firesun.wechat.enhancement.util;


import android.util.Log;

import net.dongliu.apk.parser.bean.DexClass;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import de.robv.android.xposed.XposedBridge;

public final class ReflectionUtil {

    private static final Map classesCache = new HashMap();

    private static boolean xposedExist;

    static {
        try {
            Class.forName("de.robv.android.xposed.XposedBridge");
            xposedExist = true;
        } catch (ClassNotFoundException e) {
            xposedExist = false;
        }
    }

    public static void log(String msg) {
        if (msg == null) {
            return;
        }

        if (xposedExist) {
            XposedBridge.log(msg);
        } else {
            Log.i("Xposed", "[WechatEnhancement] " + msg);
        }
    }

    public static Method findMethodsByExactParameters(Class clazz, Class returnType, Class... parameterTypes) {
        if (clazz == null) {
            return null;
        }

        // List<Method> list = Arrays.asList(XposedHelpers.findMethodsByExactParameters(clazz, returnType, (Class[]) Arrays.copyOf(parameterTypes, parameterTypes.length)));
        List<Method> list = Arrays.asList(findMethodsByExactParametersInner(clazz, returnType, (Class[]) Arrays.copyOf(parameterTypes, parameterTypes.length)));
        if (list.isEmpty())
            return null;
        else if (list.size() > 1) {
            log("find too many methods");
            for (int i = 0; i < list.size(); i++) {
                log("methods" + i + ": " + list.get(i));
            }
        }

        return list.get(0);
    }

    public static final String getClassName(DexClass clazz) {
        String str = clazz.getClassType().replace('/', '.');
        return str.substring(1, str.length() - 1);
    }

    public static Classes findClassesFromPackage(ClassLoader loader, List<String> classes, String packageName, int depth) {
        if (classesCache.containsKey(packageName + depth)) {
            return (Classes) classesCache.get(packageName + depth);
        }

        List<String> classNameList = new ArrayList();
        for (int i = 0; i < classes.size(); i++) {
            String clazz = classes.get(i);
            String currentPackage = clazz.substring(0, clazz.lastIndexOf("."));
            for (int j = 0; j < depth; j++) {
                int pos = currentPackage.lastIndexOf(".");
                if (pos < 0)
                    break;
                currentPackage = currentPackage.substring(0, currentPackage.lastIndexOf("."));
            }
            if (currentPackage.equals(packageName)) {
                classNameList.add(clazz);
            }
        }
        List<Class> classList = new ArrayList();
        for (int i = 0; i < classNameList.size(); i++) {
            String className = classNameList.get(i);
            Class c = findClassIfExists(className, loader);
            if (c != null) {
                classList.add(c);
            }
        }
        Classes cs = new Classes(classList);
        classesCache.put(packageName + depth, cs);
        return cs;
    }

    private static Map<String, Method> methodCache = new HashMap<>();

    private static String getParametersString(Class<?>... clazzes) {
        StringBuilder sb = new StringBuilder("(");
        boolean first = true;
        for (Class<?> clazz : clazzes) {
            if (first)
                first = false;
            else
                sb.append(",");

            if (clazz != null)
                sb.append(clazz.getCanonicalName());
            else
                sb.append("null");
        }
        sb.append(")");
        return sb.toString();
    }

    public static Method findMethodExact(Class<?> clazz, String methodName, Class<?>... parameterTypes) {
        String fullMethodName = clazz.getName() + '#' + methodName + getParametersString(parameterTypes) + "#exact";

        if (methodCache.containsKey(fullMethodName)) {
            Method method = methodCache.get(fullMethodName);
            if (method == null)
                throw new NoSuchMethodError(fullMethodName);
            return method;
        }

        try {
            Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
            method.setAccessible(true);
            methodCache.put(fullMethodName, method);
            return method;
        } catch (NoSuchMethodException e) {
            methodCache.put(fullMethodName, null);
            throw new NoSuchMethodError(fullMethodName);
        }
    }

    public static Method[] findMethodsByExactParametersInner(Class<?> clazz, Class<?> returnType, Class<?>... parameterTypes) {
        List<Method> result = new LinkedList<>();
        for (Method method : clazz.getDeclaredMethods()) {
            if (returnType != null && returnType != method.getReturnType())
                continue;

            Class<?>[] methodParameterTypes = method.getParameterTypes();
            if (parameterTypes.length != methodParameterTypes.length)
                continue;

            boolean match = true;
            for (int i = 0; i < parameterTypes.length; i++) {
                if (parameterTypes[i] != methodParameterTypes[i]) {
                    match = false;
                    break;
                }
            }

            if (!match)
                continue;

            method.setAccessible(true);
            result.add(method);
        }
        return result.toArray(new Method[result.size()]);
    }

    public static final Method findMethodExactIfExists(Class clazz, String methodName, Class... parameterTypes) {
        Method method = null;
        try {
            method = findMethodExact(clazz, methodName, (Class[]) Arrays.copyOf(parameterTypes, parameterTypes.length));
        } catch (Error | Exception e) {
        }
        return method;
    }

    public static final Class findClassIfExists(String className, ClassLoader classLoader) {
        Class c = null;
        try {
            c = Class.forName(className, false, classLoader);
        } catch (Error | Exception e) {
        }
        return c;
    }

    public static final Field findFieldIfExists(Class clazz, String fieldName) {
        if (clazz == null) {
            return null;
        }
        Field field = null;
        try {
            field = clazz.getField(fieldName);
        } catch (Error | Exception e) {
        }
        return field;
    }

    public static final List<Field> findFieldsWithType(Class clazz, String typeName) {
        List<Field> list = new ArrayList<Field>();
        if (clazz == null) {
            return list;
        }
        Field[] fields = clazz.getDeclaredFields();
        if (fields != null) {
            for (int i = 0; i < fields.length; i++) {
                Field field = fields[i];
                Class fieldType = field.getType();
                if (fieldType.getName().equals(typeName)) {
                    list.add(field);
                }
            }
        }
        return list;
    }

    public static final class Classes {
        private final List<Class> classes;

        public Classes(List<Class> list) {
            this.classes = (List<Class>) list;
        }

        public final Classes filterByNoMethod(Class<?> cls, Class<?>... clsArr) {
            List arrayList = new ArrayList();
            for (Object next : this.classes) {
                if (ReflectionUtil.findMethodsByExactParameters((Class) next, cls, (Class[]) Arrays.copyOf(clsArr, clsArr.length)) == null) {
                    arrayList.add(next);
                }
            }
            return new Classes(arrayList);
        }

        public final Classes filterByMethod(Class<?> cls, Class<?>... clsArr) {
            List arrayList = new ArrayList();
            for (Object next : this.classes) {
                if (ReflectionUtil.findMethodsByExactParameters((Class) next, cls, (Class[]) Arrays.copyOf(clsArr, clsArr.length)) != null) {
                    arrayList.add(next);
                }
            }

            return new Classes(arrayList);
        }

        public final Classes filterByNoField(String fieldType) {
            List arrayList = new ArrayList();
            for (Object next : this.classes) {
                if (ReflectionUtil.findFieldsWithType((Class) next, fieldType).isEmpty()) {
                    arrayList.add(next);
                }
            }

            return new Classes(arrayList);
        }

        public final Classes filterByField(String fieldType) {
            List arrayList = new ArrayList();
            for (Object next : this.classes) {
                if (!ReflectionUtil.findFieldsWithType((Class) next, fieldType).isEmpty()) {

                    arrayList.add(next);
                }
            }

            return new Classes(arrayList);
        }

        public final Classes filterByField(String fieldName, String fieldType) {
            List arrayList = new ArrayList();
            for (Object next : this.classes) {
                Field field = ReflectionUtil.findFieldIfExists((Class) next, fieldName);

                if (field != null && field.getType().getCanonicalName().equals(fieldType)) {
                    arrayList.add(next);
                }
            }


            return new Classes(arrayList);
        }

        public final Classes filterByMethod(Class returnType, String methodName, Class... parameterTypes) {
            List arrayList = new ArrayList();
            for (Object next : this.classes) {
                Method method = ReflectionUtil.findMethodExactIfExists((Class) next, methodName, (Class[]) Arrays.copyOf(parameterTypes, parameterTypes.length));
                if (method != null && method.getReturnType().getName().equals(returnType.getName())) {
                    arrayList.add(next);
                }
            }

            return new Classes(arrayList);
        }

        public final Class<?> firstOrNull() {
            if (this.classes.isEmpty())
                return null;
            else if (this.classes.size() > 1) {
                log("find too many classes");
                for (int i = 0; i < this.classes.size(); i++) {
                    log("class" + i + ": " + this.classes.get(i));
                }
            }
            return this.classes.get(0);
        }
    }
}