/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 flex.messaging.util;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;

/**
 *
 */
public class ExceptionUtil {
    /**
     * List of no-arg methods that are known to return a wrapped throwable.
     **/
    public static String[] unwrapMethods = {"getRootCause", "getTargetException",
            "getTargetError", "getException",
            "getCausedByException", "getLinkedException"};

    /**
     * Get the wrapped Exception object from the Throwable object.
     *
     * @param t the Throwable object
     * @return Throwable the wrapped exception object if any
     */
    public static Throwable wrappedException(Throwable t) {
        // Handle these statically since they are core to Java
        return (t instanceof InvocationTargetException) ?
                ((InvocationTargetException) t).getTargetException() : getRootCauseWithReflection(t);
    }

    /**
     * Get to the base exception (if any).
     *
     * @param t the Throwable object
     * @return the base Exception object
     */
    public static Throwable baseException(Throwable t) {
        Throwable wrapped = wrappedException(t);
        return wrapped != null ? baseException(wrapped) : t;
    }

    /**
     * Return the stack trace in a String.
     *
     * @param t the Throwable object
     * @return String the String presentation of the Throwable object
     */
    public static String toString(Throwable t) {
        StringWriter strWrt = new StringWriter();
        t.printStackTrace(new PrintWriter(strWrt));

        return strWrt.toString();
    }

    /**
     * Return the stack trace up to the first line that starts with prefix.
     *
     * <p>Example: ExceptionUtil.getStackTraceUpTo(exception, "jrunx.");</p>
     *
     * @param t      the Throwable object
     * @param prefix the prefix message that we are looking for
     * @return String the String of stack trace lines till the prefix message is located
     */
    public static String getStackTraceUpTo(Throwable t, String prefix) {
        StringTokenizer tokens = new StringTokenizer(toString(t), "\n\r");
        StringBuffer trace = new StringBuffer();
        boolean done = false;

        String lookingFor = "at " + prefix;
        while (!done && tokens.hasMoreElements()) {
            String token = tokens.nextToken();
            if (token.indexOf(lookingFor) == -1)
                trace.append(token);
            else
                done = true;
            trace.append(StringUtils.NEWLINE);
        }

        return trace.toString();
    }

    /**
     * return the top n lines of this stack trace.
     *
     * <p>Example: ExceptionUtil.getStackTraceLines(exception, 10);</p>
     *
     * @param t        the Throwable object
     * @param numLines number of lines we should trace down
     * @return String the String of stack trace lines
     */
    public static String getStackTraceLines(Throwable t, int numLines) {
        StringTokenizer tokens = new StringTokenizer(toString(t), "\n\r");

        StringBuffer trace = new StringBuffer();

        for (int i = 0; i < numLines; i++) {
            String token = tokens.nextToken();
            trace.append(token);
            trace.append(StringUtils.NEWLINE);
        }

        return trace.toString();
    }

    /**
     * Return the "nth" method call from the stack trace of "t", where 0 is
     * the top.
     *
     * @param t   the Throwable object
     * @param nth the line number of the message should we skip
     * @return String the callAt String
     */
    public static String getCallAt(Throwable t, int nth) {
        StringTokenizer tokens = new StringTokenizer(toString(t), "\n\r");
        try {
            // Skip the first line - the exception message
            for (int i = 0; i <= nth; ++i)
                tokens.nextToken();

            // get the method name from the next token
            String token = tokens.nextToken();
            int index1 = token.indexOf(' ');
            int index2 = token.indexOf('(');
            StringBuffer call = new StringBuffer();
            call.append(token.substring(index1 < 0 ? 0 : index1 + 1, index2 < 0 ? call.length() : index2));

            int index3 = token.indexOf(':', index2 < 0 ? 0 : index2);
            if (index3 >= 0) {
                int index4 = token.indexOf(')', index3);
                call.append(token.substring(index3, index4 < 0 ? token.length() : index4));
            }
            return call.toString();
        } catch (NoSuchElementException e) {
        }

        return "unknown";
    }


    /**
     * Utility method for converting an exception into a string. This
     * method unwinds all wrapped exceptions
     *
     * @param t The throwable exception
     * @return The printable exception
     */
    public static String exceptionToString(Throwable t) {
        StringWriter sw = new StringWriter();
        PrintWriter out = new PrintWriter(sw);

        //print out the exception stack.
        printExceptionStack(t, out, 0);
        return sw.toString();
    }

    /**
     * Utility method for converting an exception and all chained root causes into a
     * string. Unlike <code>exceptionToString(Throwable)</code> which prints the chain
     * from most nested root cause down to the top-level exception, this method prints
     * from the top-level exception down to the most nested root cause.
     *
     * @param t The throwable exception.
     * @return The printable exception.
     */
    public static String exceptionFollowedByRootCausesToString(Throwable t) {
        StringBuffer output = new StringBuffer();
        Throwable root = t;
        while (root != null) {
            output.append((root == t) ? ((root instanceof Exception) ? "  Exception: " : "  Error: ") : "  Root cause: ");
            output.append(ExceptionUtil.toString(root));
            // Do not recurse if the root cause has already been printed; this will have happened if the root cause has
            // been assigned to the current Throwable via initCause() or as a constructor argument.
            Throwable cause = root.getCause();
            root = ExceptionUtil.wrappedException(root);
            if (cause == root)
                break;
        }
        return output.toString();
    }

    /**
     * Recursively prints out a stack of wrapped exceptions.
     *
     * @param th    the Throwable object
     * @param out   the output writer to print to
     * @param depth the number of levels the stack should be printed
     */
    protected static void printExceptionStack(Throwable th, PrintWriter out, int depth) {
        //only print the stack depth if the depth is greater than 0
        boolean printStackDepth = depth > 0;

        Throwable wrappedException = ExceptionUtil.wrappedException(th);
        if (wrappedException != null) {
            printStackDepth = true;
            printExceptionStack(wrappedException, out, depth + 1);
        }

        if (printStackDepth) {
            out.write("[" + depth + "]");
        }

        th.printStackTrace(out);
    }

    private static Throwable getRootCauseWithReflection(Throwable t) {
        for (int i = 0; i < unwrapMethods.length; i++) {
            Method m = null;

            try {
                m = t.getClass().getMethod(unwrapMethods[i], (Class[]) null);
                return (Throwable) m.invoke(t, (Object[]) null);
            } catch (Exception nsme) {
                // ignore
            }
        }

        return null;
    }
}
