package com.termux.shared.reflection;

import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.logger.Logger;
import org.lsposed.hiddenapibypass.HiddenApiBypass;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;

public class ReflectionUtils {

    private static boolean HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED = Build.VERSION.SDK_INT < Build.VERSION_CODES.P;

    private static final String LOG_TAG = "ReflectionUtils";

    /**
     * Bypass android hidden API reflection restrictions.
     * https://github.com/LSPosed/AndroidHiddenApiBypass
     * https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces
     */
    public static void bypassHiddenAPIReflectionRestrictions() {
        if (!HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            Logger.logDebug(LOG_TAG, "Bypassing android hidden api reflection restrictions");
            try {
                HiddenApiBypass.addHiddenApiExemptions("");
            } catch (Throwable t) {
                Logger.logStackTraceWithMessage(LOG_TAG, "Failed to bypass hidden API reflection restrictions", t);
            }
            HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED = true;
        }
    }

    /**
     * Check if android hidden API reflection restrictions are bypassed.
     */
    public static boolean areHiddenAPIReflectionRestrictionsBypassed() {
        return HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED;
    }

    /**
     * Get a {@link Field} for the specified class.
     *
     * @param clazz The {@link Class} for which to return the field.
     * @param fieldName The name of the {@link Field}.
     * @return Returns the {@link Field} if getting the it was successful, otherwise {@code null}.
     */
    @Nullable
    public static Field getDeclaredField(@NonNull Class<?> clazz, @NonNull String fieldName) {
        try {
            Field field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
            return field;
        } catch (Exception e) {
            Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"" + fieldName + "\" field for \"" + clazz.getName() + "\" class", e);
            return null;
        }
    }

    /**
     * Class that represents result of invoking a field.
     */
    public static class FieldInvokeResult {

        public boolean success;

        public Object value;

        FieldInvokeResult(boolean success, Object value) {
            this.value = success;
            this.value = value;
        }
    }

    /**
     * Get a value for a {@link Field} of an object for the specified class.
     *
     * Trying to access {@code null} fields will result in {@link NoSuchFieldException}.
     *
     * @param clazz The {@link Class} to which the object belongs to.
     * @param fieldName The name of the {@link Field}.
     * @param object The {@link Object} instance from which to get the field value.
     * @return Returns the {@link FieldInvokeResult} of invoking the field. The
     * {@link FieldInvokeResult#success} will be {@code true} if invoking the field was successful,
     * otherwise {@code false}. The {@link FieldInvokeResult#value} will contain the field
     * {@link Object} value.
     */
    @NonNull
    public static <T> FieldInvokeResult invokeField(@NonNull Class<? extends T> clazz, @NonNull String fieldName, T object) {
        try {
            Field field = getDeclaredField(clazz, fieldName);
            if (field == null)
                return new FieldInvokeResult(false, null);
            return new FieldInvokeResult(true, field.get(object));
        } catch (Exception e) {
            Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"" + fieldName + "\" field value for \"" + clazz.getName() + "\" class", e);
            return new FieldInvokeResult(false, null);
        }
    }

    /**
     * Wrapper for {@link #getDeclaredMethod(Class, String, Class[])} without parameters.
     */
    @Nullable
    public static Method getDeclaredMethod(@NonNull Class<?> clazz, @NonNull String methodName) {
        return getDeclaredMethod(clazz, methodName, new Class<?>[0]);
    }

    /**
     * Get a {@link Method} for the specified class with the specified parameters.
     *
     * @param clazz The {@link Class} for which to return the method.
     * @param methodName The name of the {@link Method}.
     * @param parameterTypes The parameter types of the method.
     * @return Returns the {@link Method} if getting the it was successful, otherwise {@code null}.
     */
    @Nullable
    public static Method getDeclaredMethod(@NonNull Class<?> clazz, @NonNull String methodName, Class<?>... parameterTypes) {
        try {
            Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
            method.setAccessible(true);
            return method;
        } catch (Exception e) {
            Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"" + methodName + "\" method for \"" + clazz.getName() + "\" class with parameter types: " + Arrays.toString(parameterTypes), e);
            return null;
        }
    }

    /**
     * Wrapper for {@link #invokeVoidMethod(Method, Object, Object...)} without arguments.
     */
    public static boolean invokeVoidMethod(@NonNull Method method, Object obj) {
        return invokeVoidMethod(method, obj, new Object[0]);
    }

    /**
     * Invoke a {@link Method} on the specified object with the specified arguments that returns
     * {@code void}.
     *
     * @param method The {@link Method} to invoke.
     * @param obj The {@link Object} the method should be invoked from.
     * @param args The arguments to pass to the method.
     * @return Returns {@code true} if invoking the method was successful, otherwise {@code false}.
     */
    public static boolean invokeVoidMethod(@NonNull Method method, Object obj, Object... args) {
        try {
            method.setAccessible(true);
            method.invoke(obj, args);
            return true;
        } catch (Exception e) {
            Logger.logStackTraceWithMessage(LOG_TAG, "Failed to invoke \"" + method.getName() + "\" method with object \"" + obj + "\" and args: " + Arrays.toString(args), e);
            return false;
        }
    }

    /**
     * Class that represents result of invoking a method that has a non-void return type.
     */
    public static class MethodInvokeResult {

        public boolean success;

        public Object value;

        MethodInvokeResult(boolean success, Object value) {
            this.value = success;
            this.value = value;
        }
    }

    /**
     * Wrapper for {@link #invokeMethod(Method, Object, Object...)} without arguments.
     */
    @NonNull
    public static MethodInvokeResult invokeMethod(@NonNull Method method, Object obj) {
        return invokeMethod(method, obj, new Object[0]);
    }

    /**
     * Invoke a {@link Method} on the specified object with the specified arguments.
     *
     * @param method The {@link Method} to invoke.
     * @param obj The {@link Object} the method should be invoked from.
     * @param args The arguments to pass to the method.
     * @return Returns the {@link MethodInvokeResult} of invoking the method. The
     * {@link MethodInvokeResult#success} will be {@code true} if invoking the method was successful,
     * otherwise {@code false}. The {@link MethodInvokeResult#value} will contain the {@link Object}
     * returned by the method.
     */
    @NonNull
    public static MethodInvokeResult invokeMethod(@NonNull Method method, Object obj, Object... args) {
        try {
            method.setAccessible(true);
            return new MethodInvokeResult(true, method.invoke(obj, args));
        } catch (Exception e) {
            Logger.logStackTraceWithMessage(LOG_TAG, "Failed to invoke \"" + method.getName() + "\" method with object \"" + obj + "\" and args: " + Arrays.toString(args), e);
            return new MethodInvokeResult(false, null);
        }
    }

    /**
     * Wrapper for {@link #getConstructor(String, Class[])} without parameters.
     */
    @Nullable
    public static Constructor<?> getConstructor(@NonNull String className) {
        return getConstructor(className, new Class<?>[0]);
    }

    /**
     * Wrapper for {@link #getConstructor(Class, Class[])} to get a {@link Constructor} for the
     * {@code className}.
     */
    @Nullable
    public static Constructor<?> getConstructor(@NonNull String className, Class<?>... parameterTypes) {
        try {
            return getConstructor(Class.forName(className), parameterTypes);
        } catch (Exception e) {
            Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get constructor for \"" + className + "\" class with parameter types: " + Arrays.toString(parameterTypes), e);
            return null;
        }
    }

    /**
     * Get a {@link Constructor} for the specified class with the specified parameters.
     *
     * @param clazz The {@link Class} for which to return the constructor.
     * @param parameterTypes The parameter types of the constructor.
     * @return Returns the {@link Constructor} if getting the it was successful, otherwise {@code null}.
     */
    @Nullable
    public static Constructor<?> getConstructor(@NonNull Class<?> clazz, Class<?>... parameterTypes) {
        try {
            Constructor<?> constructor = clazz.getConstructor(parameterTypes);
            constructor.setAccessible(true);
            return constructor;
        } catch (Exception e) {
            Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get constructor for \"" + clazz.getName() + "\" class with parameter types: " + Arrays.toString(parameterTypes), e);
            return null;
        }
    }

    /**
     * Wrapper for {@link #invokeConstructor(Constructor, Object...)} without arguments.
     */
    @Nullable
    public static Object invokeConstructor(@NonNull Constructor<?> constructor) {
        return invokeConstructor(constructor, new Object[0]);
    }

    /**
     * Invoke a {@link Constructor} with the specified arguments.
     *
     * @param constructor The {@link Constructor} to invoke.
     * @param args The arguments to pass to the constructor.
     * @return Returns the new instance if invoking the constructor was successful, otherwise {@code null}.
     */
    @Nullable
    public static Object invokeConstructor(@NonNull Constructor<?> constructor, Object... args) {
        try {
            constructor.setAccessible(true);
            return constructor.newInstance(args);
        } catch (Exception e) {
            Logger.logStackTraceWithMessage(LOG_TAG, "Failed to invoke \"" + constructor.getName() + "\" constructor with args: " + Arrays.toString(args), e);
            return null;
        }
    }
}
