package net.ranides.assira.reflection;

import lombok.experimental.UtilityClass;

import net.ranides.assira.annotations.Meta;
import net.ranides.assira.collection.iterators.ForwardSpliterator;
import net.ranides.assira.reflection.util.MethodUtils;
import net.ranides.assira.trace.ExceptionUtils;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.function.Consumer;
import java.util.function.IntFunction;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
 * Non-portable class for accessing JVM constant pool.
 *
 * We use {@code jdk.internal.reflect.ConstantPool.ConstantPool}
 * retrieved by native method {@code Class#getConstantPool()}
 * Retrieved object is converted to interface-compatible form using AdapterFactory.
 */
@UtilityClass
public class CPInspector {

    /**
     * Returns constant pool for specified class
     * It returns well-defined interface, hiding the fact, that different JVM versions can return different objects.
     *
     * @param type class to inspect
     * @return constant pool accessor
     */
    @Meta.UnstableAPI
    public static ConstantAdapter11 getConstantPool(Class<?> type) {
        return AdapterFactory.getSystem().cast(Li.ADAPTER, MethodUtils.$invoke(Li.GET_CONSTANT_POOL, type));
    }

    /**
     * Inspects constant pool to return all methods referenced inside class
     *
     * @param clazz clazz
     * @return methods
     */
    @Meta.UnstableAPI
    public static Stream<Method> getMethods(Class<?> clazz) {
        ConstantAdapter cp = getConstantPool(clazz);
        return StreamSupport.stream(new CPSpliterator<>(cp.getSize(), cp::getMethodAt), false)
            .filter(m -> m instanceof Method)
            .map(m -> (Method)m);
    }

    /**
     * Inspects constant pool to return all constructors of given class
     *
     * @param clazz clazz
     * @param <T> clazz
     * @return constructors
     */
    @SuppressWarnings("unchecked")
    @Meta.UnstableAPI
    public static <T> Stream<Constructor<T>> getConstructors(Class<T> clazz) {
        ConstantAdapter cp = getConstantPool(clazz);
        return StreamSupport.stream(new CPSpliterator<>(cp.getSize(), cp::getMethodAt), false)
            .filter(m -> m instanceof Constructor<?>)
            .map(m -> (Constructor<T>)m);
    }

    /**
     * Inspects constant pool to return all fields referenced in given class
     *
     * @param clazz clazz
     * @return fields
     */
    @Meta.UnstableAPI
    public static Stream<Field> getFields(Class<?> clazz) {
        ConstantAdapter cp = getConstantPool(clazz);
        return StreamSupport.stream(new CPSpliterator<>(cp.getSize(), cp::getFieldAt), false);
    }

    /**
     * Inspects constant pool to return all types referenced in given class
     *
     * @param clazz clazz
     * @return types
     */
    @Meta.UnstableAPI
    public static Stream<Class<?>> getClasses(Class<?> clazz) {
        ConstantAdapter cp = getConstantPool(clazz);
        return StreamSupport.stream(new CPSpliterator<>(cp.getSize(), cp::getClassAt), false);
    }

    /**
     * Inspects constant pool to return all string constants referenced in given class
     *
     * @param clazz clazz
     * @return string constants
     */
    @Meta.UnstableAPI
    public static Stream<String> getStrings(Class<?> clazz) {
        ConstantAdapter cp = getConstantPool(clazz);
        return StreamSupport.stream(new CPSpliterator<>(cp.getSize(), cp::getUTF8At), false);
    }

    /**
     * ConstanPool interface defined in JDK8.
     * See: jdk.internal.reflect.ConstantPool
     */
    @SuppressWarnings("MissingJavadoc")
    public interface ConstantAdapter {

        int getSize();

        Class<?> getClassAt(int var1);

        Class<?> getClassAtIfLoaded(int var1);

        Member getMethodAt(int var1);

        Member getMethodAtIfLoaded(int var1);

        Field getFieldAt(int var1);

        Field getFieldAtIfLoaded(int var1);

        String[] getMemberRefInfoAt(int var1);

        int getIntAt(int var1);

        long getLongAt(int var1);

        float getFloatAt(int var1);

        double getDoubleAt(int var1);

        String getStringAt(int var1);

        String getUTF8At(int var1);

    }

    /**
     * ConstanPool interface defined in JDK11.
     * See: jdk.internal.reflect.ConstantPool
     */
    @SuppressWarnings("MissingJavadoc")
    public interface ConstantAdapter11 extends ConstantAdapter {
        int getClassRefIndexAt(int index);

        int getNameAndTypeRefIndexAt(int index);

        String[] getNameAndTypeRefInfoAt(int index);

        //public ConstantPool.Tag11 getTagAt(int index);

    }

    private static final class Li {

        public static final IClass<ConstantAdapter11> ADAPTER = IClass.typeinfo(ConstantAdapter11.class);

        public static final Method GET_CONSTANT_POOL;

        static {
            try {
                GET_CONSTANT_POOL = Class.class.getDeclaredMethod("getConstantPool");
                GET_CONSTANT_POOL.setAccessible(true);
            } catch(ReflectiveOperationException cause) {
                throw ExceptionUtils.rethrow(cause);
            }
        }

    }

    private static final class CPSpliterator<T> extends ForwardSpliterator<T> {

        private final IntFunction<T> function;
        private final int max;
        private int index;


        public CPSpliterator(int size, IntFunction<T> function) {
            this.function = function;
            this.max = size;
            this.index = 0;
        }

        @Override
        public boolean tryAdvance(Consumer<? super T> action) {
            while(index<max) {
                try {
                    action.accept(function.apply(index++));
                    return true;
                } catch(IllegalArgumentException $0) { // NOPMD
                    // do nothing
                }
            }
            return false;
        }

    }
}
