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

import lombok.experimental.UtilityClass;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.reflection.*;
import net.ranides.assira.reflection.util.ServiceScanner;

import java.util.List;
import java.util.function.Predicate;

/**
 * TraceUtils is used for stack trace inspection.
 * Most importantly, it allows to check stackframe of the caller.
 * It can be used to search for specific stackframe too.
 *
 * This class tries to optimize operations as much as possible.
 *
 * Loading only frame of interest if prefferred over obtaining full list of elements.
 * Altough even returned lists try to load data lazily.
 *
 * Please note, that assira library packaged for different JVM version
 * could use different implementations.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass
public class TraceUtils {

    /**
     * Index of callee stack frame.
     * In other words: index of stack frame of current method.
     */
    public static final int CALLEE = 1;

    /**
     * Index of caller stack frame.
     * In other words: index of stack frame of method which invoked current method.
     */
    public static final int CALLER = 2;

    interface IStackWalker {

        List<StackTraceElement> getFrames();

        List<Class<?>> getTypes();

        List<String> getNames();

        Ref<StackTraceElement> getFrame(Predicate<StackTraceElement> filter);

        Ref<Class<?>> getType(Predicate<Class<?>> filter);

        Ref<String> getName(Predicate<String> filter);

        StackTraceElement getFrame(int index);

        Class<?> getType(int index);

        String getName(int index);

    }

    private static final IStackWalker WALKER = ServiceScanner
        .scanner(IStackWalker.class)
        .resolver(name -> Ref.of(IStackWalker.class.cast(Class.forName(name).newInstance())))
        .services()
        .first()
        .orElse(null);

    /**
     * Returns list with all stack frames.
     *
     * @return list
     */
    public static List<StackTraceElement> getFrames() {
        return WALKER.getFrames();
    }

    /**
     * Returns list with all classes from stack frames.
     *
     * @return list
     */
    public static List<Class<?>> getTypes() {
        return WALKER.getTypes();
    }

    /**
     * Returns list with all class names from stack frames.
     *
     * @return list
     */
    public static List<String> getNames() {
        return WALKER.getNames();
    }

    /**
     * Returns first frame matching predicate
     *
     * @param filter filter
     * @return optional
     */
    public static Ref<StackTraceElement> getFrame(Predicate<StackTraceElement> filter) {
        return WALKER.getFrame(filter);
    }

    /**
     * Returns first class matching predicate
     *
     * @param filter filter
     * @return optional
     */
    public static Ref<Class<?>> getType(Predicate<Class<?>> filter) {
        return WALKER.getType(filter);
    }

    /**
     * Returns first class name matching predicate
     *
     * @param filter filter
     * @return optional
     */
    public static Ref<String> getName(Predicate<String> filter) {
        return WALKER.getName(filter);
    }

    /**
     * Returns frame at specified index.
     * Most important indexes are CALLEE and CALLER.
     *
     * Behaviour is undefined for invalid indexes.
     *
     * @param index index
     * @return frame
     */
    public static StackTraceElement getFrame(int index) {
        return WALKER.getFrame(index);
    }

    /**
     * Returns class at specified index.
     * Most important indexes are CALLEE and CALLER.
     *
     * Behaviour is undefined for invalid indexes.
     *
     * @param index index
     * @return class
     */
    public static Class<?> getType(int index) {
        return WALKER.getType(index);
    }

    /**
     * Returns class name at specified index.
     * Most important indexes are CALLEE and CALLER.
     *
     * Behaviour is undefined for invalid indexes.
     *
     * @param index index
     * @return class name
     */
    public static String getName(int index) {
        return WALKER.getName(index);
    }

    /**
     * Returns frame of callee method.
     *
     * @return frame
     */
    public static StackTraceElement getCalleeFrame() {
        return getFrame(CALLEE);
    }

    /**
     * Returns class of callee method.
     *
     * @return class
     */
    public static Class<?> getCalleeType() {
        return getType(CALLEE);
    }

    /**
     * Returns class name of callee method.
     *
     * @return class name
     */
    public static String getCalleeName() {
        return getName(CALLEE);
    }

    /**
     * Returns frame of caller method.
     *
     * @return frame
     */
    public static StackTraceElement getCallerFrame() {
        return getFrame(CALLER);
    }

    /**
     * Returns class of caller method.
     *
     * @return class
     */
    public static Class<?> getCallerType() {
        return getType(CALLER);
    }

    /**
     * Returns class name of caller method.
     *
     * @return class name
     */
    public static String getCallerName() {
        return getName(CALLER);
    }

    /**
     * Finds first public StackFrame invoked by JUnit framework.
     * Returns none if code is not invoked from unit test
     *
     * @return optional
     */
    public static Ref<StackTraceElement> getTestFrame() {
        List<StackTraceElement> frames = TraceUtils.getFrames();
        int junit = -1;

        // find first junit frame
        for(int i=0, n=frames.size(); i<n; i++) {
            if(frames.get(i).getClassName().startsWith("org.junit")) {
                junit = i;
                break;
            }
        }

        // move back to first public frame
        for(int i=junit-1; i>0; i--) {
            StackTraceElement frame = frames.get(i);
            IClass<?> type = IClass.typeinfo(frame.getClassName());

            if(!type.isPublic()) {
                continue;
            }
            if(type.name().startsWith("java.")) {
                continue;
            }
            if(type.methods().require(IAttribute.PUBLIC).require(frame.getMethodName()).isEmpty()) {
                continue;
            }

            return Ref.of(frame);
        }
        return Ref.empty();
    }

}
