package com.atlassian.stash.internal.logback.pattern;

import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.PublicThrowableProxyUtil;
import ch.qos.logback.classic.spi.StackTraceElementProxy;
import com.atlassian.bitbucket.util.TextUtils;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

/**
 * Decorator around a {@link IThrowableProxy}
 * that filters out uninteresting classes in its stacktrace
 * (e.g. Tomcat classes, Spring AOP decorators, etc.).
 */
public class FilteredThrowableProxy implements IThrowableProxy {

    private static final Set<String> IGNORABLE_PREFIXES = ImmutableSet.<String>builder()
            .add("$Proxy")
            .add("com.atlassian.johnson.")
            .add("com.atlassian.plugin.osgi.")
            .add("com.atlassian.plugin.servlet.")
            .add("com.atlassian.plugins.rest.")
            .add("com.atlassian.prettyurls.")
            .add("com.atlassian.stash.internal.aop.")
            .add("com.atlassian.stash.internal.johnson.")
            .add("com.atlassian.stash.internal.web.filters.")
            .add("com.opensymphony.module.sitemesh.")
            .add("com.sun.jersey.")
            .add("com.sun.jmx.")
            .add("com.sun.proxy.")
            .add("java.lang.ClassLoader")
            .add("java.lang.reflect.")
            .add("java.util.concurrent.ThreadPoolExecutor$Worker")
            .add("javax.management.remote.rmi.")
            .add("javax.servlet.http.HttpServlet")
            .add("org.apache.catalina.")
            .add("org.apache.coyote.")
            .add("org.apache.felix.")
            .add("org.apache.tomcat.")
            .add("org.springframework.aop.")
            .add("org.springframework.beans.factory.")
            .add("org.springframework.osgi.")
            .add("org.springframework.security.")
            .add("org.springframework.transaction.interceptor.")
            .add("org.springframework.web.filter.")
            .add("org.springframework.web.method.support.InvocableHandlerMethod")
            .add("org.springframework.web.servlet.")
            .add("org.tuckey.")
            .add("sun.reflect.")
            .add("sun.rmi.")
            .build();

    private final IThrowableProxy throwable;
    private final FilteredThrowableProxy cause;
    private final FilteredThrowableProxy[] suppressed;
    private final StackTraceElementProxy[] stackTrace;
    private int commonFrames;

    public FilteredThrowableProxy(IThrowableProxy throwable) {
        this.throwable = throwable;
        this.stackTrace = filterElements(throwable.getStackTraceElementProxyArray());

        IThrowableProxy cause = throwable.getCause();
        if (cause == null) {
            this.cause = null;
        } else {
            this.cause = new FilteredThrowableProxy(cause);
            this.cause.commonFrames = findNumberOfCommonFrames(this.cause, this);
        }

        IThrowableProxy[] suppressed = throwable.getSuppressed();
        FilteredThrowableProxy[] proxies = new FilteredThrowableProxy[suppressed.length];
        for (int i = 0; i < suppressed.length; i++) {
            proxies[i] = new FilteredThrowableProxy(suppressed[i]);
            proxies[i].commonFrames = findNumberOfCommonFrames(proxies[i], this);
        }
        this.suppressed = proxies;
    }

    @Override
    public String getClassName() {
        return throwable.getClassName();
    }

    @Override
    public IThrowableProxy getCause() {
        return cause;
    }

    @Override
    public int getCommonFrames() {
        return commonFrames;
    }

    @Override
    public String getMessage() {
        return throwable.getMessage();
    }

    @Override
    public StackTraceElementProxy[] getStackTraceElementProxyArray() {
        return stackTrace;
    }

    @Override
    public IThrowableProxy[] getSuppressed() {
        return suppressed;
    }

    IThrowableProxy getThrowableProxy() {
        return throwable;
    }

    static class Placeholder extends StackTraceElementProxy {
        private final String placeholder;

        Placeholder(String placeholder) {
            super(new StackTraceElement("Unknown frame", "unknown", "Unknown frame", -1));
            this.placeholder = placeholder;
        }

        @Override
        public String toString() {
            return placeholder;
        }
    }

    private static int findNumberOfCommonFrames(FilteredThrowableProxy cause, FilteredThrowableProxy exception) {
        StackTraceElementProxy[] causeProxyStack = cause.getStackTraceElementProxyArray();
        StackTraceElement[] causeStack = Lists.transform(Arrays.asList(causeProxyStack),
                new Function<StackTraceElementProxy, StackTraceElement>() {
                    public StackTraceElement apply(StackTraceElementProxy proxy) {
                        return proxy.getStackTraceElement();
                    }
                }).toArray(new StackTraceElement[causeProxyStack.length]);

        return PublicThrowableProxyUtil.findNumberOfCommonFrames(causeStack, exception.getStackTraceElementProxyArray());
    }

    private static StackTraceElementProxy[] filterElements(StackTraceElementProxy[] stackTrace) {
        List<StackTraceElementProxy> filteredStackTrace = new ArrayList<>(estimateStackSize(stackTrace));

        if (stackTrace.length > 0) {
            // always add the first frame, in case it would otherwise be filtered
            filteredStackTrace.add(stackTrace[0]);

            // iterate over the remaining frames and filter any of them that matches the set of configured prefixes
            for (int i = 1; i < stackTrace.length; i++) {
                StackTraceElementProxy element = stackTrace[i];
                String clazz = element.getStackTraceElement().getClassName();
                boolean match = false;
                for (String prefix : IGNORABLE_PREFIXES) {
                    if (match |= clazz.startsWith(prefix)) {
                        break;
                    }
                }
                if (!match) {
                    filteredStackTrace.add(element);
                }
            }
        }

        int trimmed = stackTrace.length - filteredStackTrace.size();
        if (trimmed > 0) {
            filteredStackTrace.add(new Placeholder("... " + trimmed + ' ' +
                    TextUtils.pluralise("frame", "frames", trimmed) + " trimmed"));
        }

        return filteredStackTrace.toArray(new StackTraceElementProxy[filteredStackTrace.size()]);
    }

    private static int estimateStackSize(StackTraceElementProxy[] stackTrace) {
        // heuristic based on the following cases in Stash 2.9 (7a570d4):
        // - in a Spring MVC Controller (opening a project, with an exception thrown in ProjectAdminController):
        //      208 (before), 14 (after) -> ratio 0.067
        // - in a REST end point (opening a file, with an exception thrown in ContentService):
        //      269 (before), 21 (after) -> ratio 0.078
        // - in a plugin's REST end point (visualising the branch permissions, with an exception thrown in BranchPermissionServiceImpl):
        //     186 (before), 13 (after) -> ratio 0.069
        // - in a plugin's servlet (adding an SSH key, with an exception thrown in AddKeyServlet):
        //     169 (before), 13 (after) -> ratio 0.076
        return stackTrace.length / 10;
    }

}
