/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.trace;

import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.generic.ValueUtils;

import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;

/**
 * This implementation has better performance than default implementation,
 * but it uses "sun.reflect" which is available in JVM8
 *
 * sun.reflect is great if you want to get one frame, but it is slower than SecurityManager
 * if you want to inspect whole stacktrace
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public final class FStackWalker8 implements TraceUtils.IStackWalker {

    // Please note that class is implemented in very verbose way:
    // we don't use any functional interfaces, adapters, JDK Stream API etc.
    //
    // Every syntactic elegance will slow down us. It matters, because functions
    // are designed to be *extremaly* fast (~1 us per call) and every overhead
    // is noticeable.
    //
    // We use "package scope" because we want to test our resolvers in junit

    private static final String NSE = "StackInspector: operation not supported. Please use Sun JDK or enable SecurityManager.";

    static final SunResolver RESOLVER_SUN = getSunResolver();

    static final SecurityResolver RESOLVER_SECURITY = getSecurityResolver();

    static final ThreadResolver RESOLVER_THREAD = new ThreadResolver();

    static final ExceptionResolver RESOLVER_EXCEPT = new ExceptionResolver();

    /**
     * if you want full list, SecurityManager is faster than SunReflection
     */
    static final Resolver AUTO_LIST_RESOLVER = ValueUtils.or(
            RESOLVER_SECURITY,
            RESOLVER_SUN,
            RESOLVER_EXCEPT
    );

    /**
     * if you want first element, SunReflection is faster than SecurityManager
     */
    static final Resolver AUTO_CALLER_RESOLVER = ValueUtils.or(
            RESOLVER_SUN,
            RESOLVER_SECURITY,
            RESOLVER_EXCEPT
    );

    @Override
    public List<StackTraceElement> getFrames() {
        return new FrameList();
    }

    @Override
    public List<Class<?>> getTypes() {
        return AUTO_LIST_RESOLVER.list();
    }

    @Override
    public List<String> getNames() {
        return AUTO_LIST_RESOLVER.names();
    }

    @Override
    public Ref<StackTraceElement> getFrame(Predicate<StackTraceElement> filter) {
        return CQuery.from().list(new FrameList()).filter(filter).first();
    }

    @Override
    public Ref<Class<?>> getType(Predicate<Class<?>> filter) {
        return CQuery.from().list(AUTO_LIST_RESOLVER.list()).filter(filter).first();
    }

    @Override
    public Ref<String> getName(Predicate<String> filter) {
        return CQuery.from().list(AUTO_LIST_RESOLVER.names()).filter(filter).first();
    }

    @Override
    public StackTraceElement getFrame(int index) {
        return new FrameList().get(index);
    }

    @Override
    public Class<?> getType(int index) {
        return AUTO_CALLER_RESOLVER.type(index);
    }

    @Override
    public String getName(int index) {
        return AUTO_CALLER_RESOLVER.name(index);
    }

    @SuppressWarnings("PMD")
    static List<Class<?>> test_getTypes(Resolver resolver) {
        return test1_getTypes(resolver);
    }

    private static List<Class<?>> test1_getTypes(Resolver resolver) {
        return resolver.list();
    }

    @SuppressWarnings("PMD")
    static List<String> test_getNames(Resolver resolver) {
        return test1_getNames(resolver);
    }

    private static List<String> test1_getNames(Resolver resolver) {
        return resolver.names();
    }

    @SuppressWarnings("PMD")
    static Class<?> test_getCalleeType(Resolver resolver) {
        return test_getCalleeType1(resolver);
    }

    private static Class<?> test_getCalleeType1(Resolver resolver) {
        return resolver.type(0);
    }

    @SuppressWarnings("PMD")
    static String test_getCalleeName(Resolver resolver) {
        return test_getCalleeName1(resolver);
    }

    private static String test_getCalleeName1(Resolver resolver) {
        return resolver.name(0);
    }

    @SuppressWarnings("PMD")
    static Class<?> test_getCallerType(Resolver resolver) {
        return test_getCallerType1(resolver);
    }

    private static Class<?> test_getCallerType1(Resolver resolver) {
        return resolver.type(1);
    }

    @SuppressWarnings("PMD")
    static String test_getCallerName(Resolver resolver) {
        return test_getCallerName1(resolver);
    }

    private static String test_getCallerName1(Resolver resolver) {
        return resolver.name(1);
    }

    interface Resolver {

        Class<?> type(int index);

        String name(int index);

        List<Class<?>> list();

        List<String> names();
    }

    private static SunResolver getSunResolver() {
        try {
            return new SunResolver();
        } catch(Exception | Error cause) {
            return null;
        }
    }

    private static SecurityResolver getSecurityResolver() {
        try {
            return new SecurityResolver();
        } catch(SecurityException se) {
            return null;
        }
    }

    private static final class SecurityResolver extends SecurityManager implements Resolver {

        private static final int FIRST = 3;

        @Override
        public Class<?> type(int index) {
            return super.getClassContext()[FIRST+index];
        }

        @Override
        public String name(int index) {
            return super.getClassContext()[FIRST+index].getName();
        }

        @Override
        public List<Class<?>> list() {
            return new ClassArray(super.getClassContext(), FIRST);
        }

        @Override
        public List<String> names() {
            return new NameArray(super.getClassContext(), FIRST);
        }

    }

    private static final class SunResolver implements Resolver {

        private static final int FIRST = 3+3;

        public SunResolver() {
            $caller(0);
        }

        @Override
        public Class<?> type(int index) {
            return $caller(FIRST+index);
        }

        @Override
        public String name(int index) {
            return $caller(FIRST+index).getName();
        }

        @Override
        public List<Class<?>> list() {
            List<Class<?>> list = new ArrayList<>(64);
            int i=FIRST;
            Class<?> c;
            while(null != (c = $caller(i++))) {
                list.add(c);
            }
            return list;
        }

        @Override
        public List<String> names() {
            List<String> list = new ArrayList<>(64);
            int i=FIRST;
            Class<?> c;
            while(null != (c = $caller(i++))) {
                list.add(c.getName());
            }
            return list;
        }

    }

    private static Class<?> $caller(int index) {
        return sun.reflect.Reflection.getCallerClass(index);
    }

    private static final class ThreadResolver implements Resolver {

        private static final int FIRST = 4;

        @Override
        public List<String> names() {
            return new FrameArray(Thread.currentThread().getStackTrace(), FIRST);
        }

        @Override
        public Class<?> type(int index) {
            throw new UnsupportedOperationException(NSE);
        }

        @Override
        public String name(int index) {
            return Thread.currentThread().getStackTrace()[FIRST+index].getClassName();
        }

        @Override
        public List<Class<?>> list() {
            throw new UnsupportedOperationException(NSE);
        }

    }

    private static final class ExceptionResolver implements Resolver {

        private static final int FIRST = 3;

        @Override
        public Class<?> type(int index) {
            throw new UnsupportedOperationException(NSE);
        }

        @Override
        public String name(int index) {
            return ReflectFrame.get(new Exception(), FIRST+index).getClassName(); //NOPMD
        }

        @Override
        public List<String> names() {
            return new FrameArray(new Exception().getStackTrace(), FIRST); //NOPMD
        }

        @Override
        public List<Class<?>> list() {
            throw new UnsupportedOperationException(NSE);
        }

    }

    private static final class ClassArray extends AbstractList<Class<?>> {

        private final Class<?>[] array;
        private final int offset;
        private final int length;

        @SuppressWarnings("PMD.ArrayIsStoredDirectly")
        public ClassArray(Class<?>[] array, int offset, int length) {
            this.array = array;
            this.offset = offset;
            this.length = length;
        }

        public ClassArray(Class<?>[] array, int offset) {
            this(array, offset, array.length-offset);
        }

        @Override
        public Class<?> get(int index) {
            return array[offset+index];
        }

        @Override
        public int size() {
            return length;
        }

    }

    private static final class NameArray extends AbstractList<String> {

        private final Class<?>[] array;
        private final int offset;
        private final int length;

        @SuppressWarnings("PMD.ArrayIsStoredDirectly")
        public NameArray(Class<?>[] array, int offset, int length) {
            this.array = array;
            this.offset = offset;
            this.length = length;
        }

        public NameArray(Class<?>[] array, int offset) {
            this(array, offset, array.length-offset);
        }

        @Override
        public String get(int index) {
            return array[offset+index].getName();
        }

        @Override
        public int size() {
            return length;
        }

    }

    private static final class FrameArray extends AbstractList<String> {

        private final StackTraceElement[] array;
        private final int offset;
        private final int length;

        @SuppressWarnings("PMD.ArrayIsStoredDirectly")
        public FrameArray(StackTraceElement[] array, int offset, int length) {
            this.array = array;
            this.offset = offset;
            this.length = length;
        }

        public FrameArray(StackTraceElement[] array, int offset) {
            this(array, offset, array.length-offset);
        }

        @Override
        public String get(int index) {
            return array[offset+index].getClassName();
        }

        @Override
        public int size() {
            return length;
        }

    }

    private static final class FrameList extends AbstractList<StackTraceElement> {

        private static final int FIRST = 3;

        private final Throwable cause;

        private final int size;

        private StackTraceElement[] array;

        public FrameList() {
            this.cause = new Throwable(); // NOPMD
            this.size = ReflectFrame.depth(cause)-FIRST;
            this.array = null;
        }

        @Override
        public StackTraceElement get(int index) {
            if(null == array) {
                array = new StackTraceElement[ReflectFrame.depth(cause)];
            }
            if(null == array[index]) {
                array[index] = ReflectFrame.get(cause, FIRST+index);
            }
            return array[index];
        }

        @Override
        public int size() {
            return size;
        }

    }

    private static final class ReflectFrame { // NOPMD - lazy init idiom

        public static final java.lang.reflect.Method GET;

        public static final java.lang.reflect.Method DEPTH;

        static {
            try {
                GET = Throwable.class.getDeclaredMethod("getStackTraceElement", int.class);
                GET.setAccessible(true);

                DEPTH = Throwable.class.getDeclaredMethod("getStackTraceDepth");
                DEPTH.setAccessible(true);
            } catch(ReflectiveOperationException ex) {
                throw new UnsupportedOperationException(NSE, ex);
            }
        }

        public static int depth(Throwable that) {
            try {
                return (int)DEPTH.invoke(that);
            } catch(ReflectiveOperationException ex) {
                throw new UnsupportedOperationException(NSE, ex);
            }
        }

        public static StackTraceElement get(Throwable that, int index) {
            try {
                return (StackTraceElement)GET.invoke(that, index);
            } catch(ReflectiveOperationException ex) {
                throw new UnsupportedOperationException(NSE, ex);
            }
        }

    }

}
