/**
 * Healenium-web Copyright (C) 2019 EPAM
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *        http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.epam.healenium.utils;

import com.epam.healenium.handlers.proxy.BaseHandler;
import com.google.common.collect.Iterables;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import one.util.streamex.StreamEx;
import org.apache.commons.lang3.StringUtils;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Slf4j
@UtilityClass
public class StackUtils {

    /**
     *
     * @return
     */
    public boolean isAnnotationPresent(Class<? extends Annotation> aClass){
        StackTraceElement[] trace = Thread.currentThread().getStackTrace();
        return findAnnotatedInTrace(trace, aClass).isPresent();
    }

    public Optional<StackTraceElement> findOriginCaller(){
        StackTraceElement[] trace = Thread.currentThread().getStackTrace();
        return findOriginCaller(trace);
    }

    public Optional<StackTraceElement> findOriginCaller(StackTraceElement[] elements){
        List<StackTraceElement> elementList = normalize(elements);
        String callerName = getCallerPackageName(elementList);
        if(StringUtils.isBlank(callerName)) return Optional.empty();
        return elementList.stream()
                .filter(it -> it.getClassName().startsWith(callerName))
                .findFirst();
    }

    /**
     *
     * @param elements
     * @param targetClass
     * @return
     */
    public Optional<StackTraceElement> getElementByClass(StackTraceElement[] elements, String targetClass) {
        return Arrays.stream(elements)
                .filter(redundantPackages())
                .filter(element -> {
                    String className = element.getClassName();
                    String simpleClassName = className.substring(className.lastIndexOf('.') + 1);
                    return simpleClassName.equals(targetClass);
                })
                .findFirst();
    }

    /**
     * Lookup for method annotated as restorable in the stack trace. The first occurrence will be taken
     *
     * @param elements that represent invocation stack trace
     * @return
     */
    private Optional<StackTraceElement> findAnnotatedInTrace(StackTraceElement[] elements, Class<? extends Annotation> clazz) {
        return Arrays.stream(elements)
                .filter(redundantPackages())
                .filter(it -> {
                    try {
                        Class<?> aClass = Class.forName(it.getClassName());
                        String methodName = it.getMethodName();
                        return Arrays.stream(aClass.getMethods())
                                .filter(m -> m.getName().equals(methodName))
                                .anyMatch(m -> {
                                    for (Annotation annotation : m.getDeclaredAnnotations()) {
                                        if (clazz.isInstance(annotation)) {
                                            log.debug("Found at ={},{}", it.getClassName(), methodName);
                                            return true;
                                        }
                                    }
                                    return false;
                                });
                    } catch (ClassNotFoundException ex) {
                        log.warn("Failed to check class: {}", it.getClassName());
                        return false;
                    }
                })
                .findFirst();
    }

    /**
     *
     * @return
     */
    private Predicate<StackTraceElement> redundantPackages() {
        return value -> {
            Stream<String> skippingPackageStream = Stream.of("java.base","sun.reflect", "java.lang", "org.gradle", "org.junit", "java.util", "com.sun", "com.google","jdk.internal","org.openqa");
            return skippingPackageStream.noneMatch(s -> value.getClassName().startsWith(s));
        };
    }

    private List<StackTraceElement> normalize(StackTraceElement[] traceElements){
        List<StackTraceElement> elementList = Arrays.stream(traceElements)
                .filter(redundantPackages())
                .collect(Collectors.toList());
        Collections.reverse(elementList);
        elementList = StreamEx.of(elementList)
                .takeWhile(it-> !it.getClassName().equals(BaseHandler.class.getName()))
                .toList();
        return elementList.subList(0, elementList.size() -1);
    }

    private String getCallerPackageName(List<StackTraceElement> traceElements){
        String result = "";
        try{
            StackTraceElement element = Iterables.getLast(traceElements);
            String className = element.getClassName();
            int dotPos = lastDotPosition(className);
            result = element.getClassName().substring(0, Math.max(dotPos,0));
        } catch (Exception ex){
            log.warn("Failed to find caller package name", ex);
        }
        return result;
    }

    private int lastDotPosition(String input){
        int dot1 = input.indexOf(".");
        int dot2 = input.indexOf(".", dot1 + 1);
        return Math.max(dot1, dot2);
    }
}