package net.ranides.assira.trace;

import net.ranides.assira.generic.Ref;

import java.util.AbstractList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * This implementation uses java.lang.StackWalker available in JVM11
 *
 * StackWalker has relatively good performance, but in some cases SecurityManager is faster.
 */
public class FStackWalker11 implements TraceUtils.IStackWalker {

    static final SecurityResolver RESOLVER_SECURITY = getSecurityResolver();

    @Override
    public List<StackTraceElement> getFrames() {
        return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
                .walk(s -> s.skip(2).map(StackWalker.StackFrame::toStackTraceElement).collect(Collectors.toList()));
    }

    @Override
    public List<Class<?>> getTypes() {
        if(RESOLVER_SECURITY != null) {
            return RESOLVER_SECURITY.list();
        }
        return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
                .walk(s -> s.skip(2).map(StackWalker.StackFrame::getDeclaringClass).collect(Collectors.toList()));
    }

    @Override
    public List<String> getNames() {
        return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
                .walk(s -> s.skip(2).map(StackWalker.StackFrame::getClassName).collect(Collectors.toList()));
    }

    @Override
    public Ref<StackTraceElement> getFrame(Predicate<StackTraceElement> filter) {
        Optional<StackTraceElement> out = StackWalker
            .getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
            .walk(s -> s.skip(2).map(StackWalker.StackFrame::toStackTraceElement).filter(filter).findFirst());
        return Ref.ofOptional(out);
    }

    @Override
    public Ref<Class<?>> getType(Predicate<Class<?>> filter) {
        if(RESOLVER_SECURITY != null) {
            return Ref.ofOptional(RESOLVER_SECURITY.stream().filter(filter).findFirst());
        }
        Optional<Class<?>> out = StackWalker
            .getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
            .walk(s -> s
                .skip(2)
                .filter(f -> filter.test(f.getDeclaringClass()))
                .findFirst()
                .map(StackWalker.StackFrame::getDeclaringClass));
        return Ref.ofOptional(out);
    }

    @Override
    public Ref<String> getName(Predicate<String> filter) {
        Optional<String> out = StackWalker
            .getInstance()
            .walk(s -> s
                .skip(2)
                .filter(f -> filter.test(f.getClassName()))
                .findFirst()
                .map(StackWalker.StackFrame::getClassName));
        return Ref.ofOptional(out);
    }

    @Override
    public StackTraceElement getFrame(int index) {
        return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
                .walk(s -> s.skip(index+2).findFirst().map(StackWalker.StackFrame::toStackTraceElement).get());
    }

    @Override
    public Class<?> getType(int index) {
        if(RESOLVER_SECURITY != null) {
            return RESOLVER_SECURITY.type(index);
        }
        return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
                .walk(s -> s.skip(index+2).findFirst().map(StackWalker.StackFrame::getDeclaringClass).get());
    }

    @Override
    public String getName(int index) {
        return StackWalker.getInstance()
                .walk(s -> s.skip(index+2).findFirst().map(StackWalker.StackFrame::getClassName).get());
    }

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

    private static final class SecurityResolver extends SecurityManager {

        private static final int FIRST = 3;

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

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

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

        public Stream<Class<?>> stream() {
            Class<?>[] array = super.getClassContext();
            return Arrays.stream(array, FIRST, array.length);
        }

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

    }

    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;
        }

    }

}
