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

import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
import net.ranides.assira.generic.ValueUtils;

/**
 * 
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public final class TraceUtils {
    
    // 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 Resolver RESOLVER_SR = getSunResolver();
    
    static final Resolver RESOLVER_SM = getSMResolver();
    
    static final Resolver RESOLVER_TS = new ThreadResolver();

    static final Resolver RESOLVER_ES = new FrameResolver();

    /**
     * if you want full list, SecurityManager is faster than SunReflection 
     */
    static final Resolver AUTO_LIST_RESOLVER = ValueUtils.or(
        RESOLVER_SM, 
        RESOLVER_SR, 
        RESOLVER_ES
    );
    
    /**
     * if you want first element, SunReflection is faster than SecurityManager
     */
    static final Resolver AUTO_CALLER_RESOLVER = ValueUtils.or(
        RESOLVER_SR, 
        RESOLVER_SM, 
        RESOLVER_ES
    );

    private TraceUtils() { 
        /* utility class */ 
    }
    
    public static List<StackTraceElement> getFrames() {
        return new FrameList();
    }
    
    public static List<Class<?>> getTypes() {
        return AUTO_LIST_RESOLVER.list();
    }
    
    public static List<String> getNames() {
        return AUTO_LIST_RESOLVER.names();
    }
    
    public static StackTraceElement getFrame(int index) {
        return new FrameList().get(index);
    }
    
    public static Class<?> getType(int index) {
        return AUTO_CALLER_RESOLVER.type(index);
    }
    
    public static String getName(int index) {
        return AUTO_CALLER_RESOLVER.name(index);
    }
    
    public static StackTraceElement getCalleeFrame() {
        return getFrame(1);
    }
    
    public static Class<?> getCalleeType() {
        return getType(1);
    }
    
    public static String getCalleeName() {
        return getName(1);
    }
    
    public static StackTraceElement getCallerFrame() {
        return getFrame(2);
    }
    
    public static Class<?> getCallerType() {
        return getType(2);
    }
    
    public static String getCallerName() {
        return getName(2);
    }
    
    @SuppressWarnings("PMD")
    static List<Class<?>> test_getTypes(Resolver resolver) {
        return resolver.list();
    }
    
    @SuppressWarnings("PMD")
    static List<String> test_getNames(Resolver resolver) {
        return resolver.names();
    }
    
	@SuppressWarnings("PMD")
    static Class<?> test_getCalleeType(Resolver resolver) {
        return resolver.type(0);
    }
    
	@SuppressWarnings("PMD")
    static String test_getCalleeName(Resolver resolver) {
        return resolver.name(0);
    }
    
    @SuppressWarnings("PMD")
    static Class<?> test_getCallerType(Resolver resolver) {
        return resolver.type(1);
    }
    
	@SuppressWarnings("PMD")
    static String test_getCallerName(Resolver resolver) {
        return resolver.name(1);
    }

    interface Resolver {
        
        Class<?> type(int index);
        
        String name(int index);
        
        List<Class<?>> list();
        
        List<String> names();
    }
    
    private static Resolver getSunResolver() {
        try {
            return new SunResolver();
        } catch(Exception | Error cause) {
            return null;
        }
    }
    
    private static Resolver getSMResolver() {
        try {
            return new SMResolver();
        } catch(SecurityException se) {
            return null;
        }
    }
    
    private static final class SMResolver extends SecurityManager implements Resolver {
        
        private static final int FIRST = 2;

        @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+2;

        @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 = 3;
        
        @Override
        public List<String> names() {
            return new FrameArray(Thread.currentThread().getStackTrace(), 3);
        }

        @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 FrameResolver implements Resolver { 
        
        private static final int FIRST = 2;

        @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 = 2;
        
        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);
            }
        }
        
    }
    
}
