package org.xbib.logging.util;

import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;

/**
 * Formatter used to format the stack trace of an exception.
 */
public class StackTraceFormatter {

    private static final String CAUSED_BY_CAPTION = "Caused by: ";

    private static final String SUPPRESSED_CAPTION = "Suppressed: ";

    private final Set<Throwable> seen;

    private final StringBuilder builder;

    private final int suppressedDepth;

    private int suppressedCount;

    private StackTraceFormatter(final StringBuilder builder,
                                final int suppressedDepth) {
        this.builder = builder;
        this.suppressedDepth = suppressedDepth;
        this.seen = Collections.newSetFromMap(new IdentityHashMap<>());
    }

    /**
     * Writes the stack trace into the builder.
     *
     * @param builder         the string builder ot append the stack trace to
     * @param t               the throwable to render
     * @param suppressedDepth the number of suppressed messages to include
     */
    public static void renderStackTrace(final StringBuilder builder, final Throwable t, final int suppressedDepth) {
        renderStackTrace(builder, t, false, suppressedDepth);
    }

    /**
     * Writes the stack trace into the builder.
     *
     * @param builder         the string builder ot append the stack trace to
     * @param t               the throwable to render
     * @param extended        ignored
     * @param suppressedDepth the number of suppressed messages to include
     */
    public static void renderStackTrace(final StringBuilder builder,
                                        final Throwable t,
                                        @SuppressWarnings("unused") final boolean extended,
                                        final int suppressedDepth) {
        new StackTraceFormatter(builder, suppressedDepth).renderStackTrace(t);
    }

    private void renderStackTrace(final Throwable t) {
        suppressedCount = 0;
        builder.append(": ").append(t);
        newLine();
        final StackTraceElement[] stackTrace = t.getStackTrace();
        for (StackTraceElement element : stackTrace) {
            renderTrivial("", element);
        }
        if (suppressedDepth != 0) {
            for (Throwable se : t.getSuppressed()) {
                if (suppressedDepth < 0 || suppressedDepth > suppressedCount++) {
                    renderStackTrace(stackTrace, se, SUPPRESSED_CAPTION, "\t");
                }
            }
        }
        final Throwable ourCause = t.getCause();
        if (ourCause != null) {
            renderStackTrace(stackTrace, ourCause, CAUSED_BY_CAPTION, "");
        }
    }

    private void renderStackTrace(final StackTraceElement[] parentStack,
                                  final Throwable child,
                                  final String caption,
                                  final String prefix) {
        if (seen.contains(child)) {
            builder.append(prefix)
                    .append(caption)
                    .append("[CIRCULAR REFERENCE: ")
                    .append(child)
                    .append(']');
            newLine();
        } else {
            seen.add(child);
            final StackTraceElement[] causeStack = child.getStackTrace();
            int m = causeStack.length - 1;
            int n = parentStack.length - 1;
            while (m >= 0 && n >= 0 && causeStack[m].equals(parentStack[n])) {
                m--;
                n--;
            }
            final int framesInCommon = causeStack.length - 1 - m;
            builder.append(prefix)
                    .append(caption)
                    .append(child);
            newLine();
            for (int i = 0; i <= m; i++) {
                renderTrivial(prefix, causeStack[i]);
            }
            if (framesInCommon != 0) {
                builder.append(prefix)
                        .append("\t... ")
                        .append(framesInCommon)
                        .append(" more");
                newLine();
            }
            if (suppressedDepth != 0) {
                for (Throwable se : child.getSuppressed()) {
                    if (suppressedDepth < 0 || suppressedDepth > suppressedCount++) {
                        renderStackTrace(causeStack, se, SUPPRESSED_CAPTION, prefix + "\t");
                    }
                }
            }
            Throwable ourCause = child.getCause();
            if (ourCause != null) {
                renderStackTrace(causeStack, ourCause, CAUSED_BY_CAPTION, prefix);
            }
        }
    }

    private void renderTrivial(final String prefix,
                               final StackTraceElement element) {
        builder.append(prefix)
                .append("\tat ")
                .append(element);
        newLine();
    }

    private void newLine() {
        builder.append(System.lineSeparator());
    }
}
