/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2008 Sun Microsystems, Inc.
 *      Portions Copyright 2014-2015 ForgeRock AS.
 */
package org.opends.server.loggers;

import static org.opends.server.util.ServerConstants.EOL;

/**
 * A DebugStackTraceFormatter converts an exception's stack trace into a String
 * appropriate for tracing, optionally performing filtering of stack frames.
 */
class DebugStackTraceFormatter
{
  /**
   * The stack depth value to indicate the entire stack should be printed.
   */
  public static final int COMPLETE_STACK = Integer.MAX_VALUE;
  /**
   * A nested frame filter that removes debug and trailing no OpenDS frames.
   */
  public static final FrameFilter SMART_FRAME_FILTER = new SmartFrameFilter();

  /**
   * A FrameFilter provides stack frame filtering used during formatting.
   */
  interface FrameFilter
  {

    /**
     * Filters out all undesired stack frames from the given Throwable's stack
     * trace.
     *
     * @param frames
     *          the frames to filter
     * @return an array of StackTraceElements to be used in formatting.
     */
    StackTraceElement[] getFilteredStackTrace(StackTraceElement[] frames);
  }

  /**
   * A basic FrameFilter that filters out frames from the debug logging and non
   * OpenDS classes.
   */
  private static class SmartFrameFilter implements FrameFilter
  {

    private boolean isFrameForPackage(StackTraceElement frame,
        String packageName)
    {
      boolean isContained = false;

      if (frame != null)
      {
        String className = frame.getClassName();
        isContained = className != null && className.startsWith(packageName);
      }
      return isContained;
    }

    /**
     * Return the stack trace of an exception with debug and trailing non OpenDS
     * frames filtered out.
     *
     * @param frames
     *          the frames to filter
     * @return the filtered stack trace.
     */
    public StackTraceElement[] getFilteredStackTrace(StackTraceElement[] frames)
    {
      StackTraceElement[] trimmedStack = null;
      if (frames != null && frames.length > 0)
      {
        int firstFrame = 0;

        // Skip leading frames debug logging classes
        while (firstFrame < frames.length
            && DebugTracer.isLoggingStackTraceElement(frames[firstFrame]))
        {
          firstFrame++;
        }

        // Skip trailing frames not in OpenDS classes
        int lastFrame = frames.length - 1;
        while (lastFrame > firstFrame
            && !isFrameForPackage(frames[lastFrame], "org.opends"))
        {
          lastFrame--;
        }

        trimmedStack = new StackTraceElement[lastFrame - firstFrame + 1];
        for (int i = firstFrame; i <= lastFrame; i++)
        {
          trimmedStack[i - firstFrame] = frames[i];
        }
      }

      return trimmedStack;
    }
  }

  /**
   * Generate a String representation of the entire stack trace of the given
   * Throwable.
   *
   * @param t
   *          - the Throwable for which to generate the stack trace.
   * @return the stack trace.
   */
  public static String formatStackTrace(Throwable t)
  {
    return formatStackTrace(t, COMPLETE_STACK, true);
  }

  /**
   * Generate a String representation of the possibly filtered stack trace of
   * the given Throwable.
   *
   * @param t
   *          - the Throwable for which to generate the stack trace.
   * @param maxDepth
   *          - the maximum number of stack frames to include in the trace.
   * @param includeCause
   *          - also include the stack trace for the cause Throwable.
   * @return the stack trace.
   */
  static String formatStackTrace(Throwable t, int maxDepth, boolean includeCause)
  {
    StringBuilder buffer = new StringBuilder();

    StackTraceElement[] trace = t.getStackTrace();
    int frameLimit = Math.min(maxDepth, trace.length);
    for (int i = 0; i < frameLimit; i++)
    {
      buffer.append("  at ");
      buffer.append(trace[i]);
      buffer.append(EOL);
    }
    if (frameLimit < trace.length)
    {
      buffer.append("  ... ");
      buffer.append(trace.length - frameLimit);
      buffer.append(" more");
      buffer.append(EOL);
    }

    if (includeCause)
    {
      Throwable ourCause = t.getCause();
      if (ourCause != null)
      {
        formatStackTraceForCause(ourCause, maxDepth, buffer, trace);
      }
    }

    return buffer.toString();
  }

  private static void formatStackTraceForCause(Throwable t, int maxDepth,
      StringBuilder buffer, StackTraceElement[] causedTrace)
  {
    StackTraceElement[] trace = t.getStackTrace();
    int framesToSkip = Math.max(trace.length - maxDepth, 0);

    // Compute number of frames in common between this and caused
    int m = trace.length - 1 - framesToSkip;
    int n = causedTrace.length - 1 - framesToSkip;
    while (m >= 0 && n >= 0 && trace[m].equals(causedTrace[n]))
    {
      m--;
      n--;
    }
    framesToSkip = trace.length - 1 - m;

    buffer.append("Caused by: ");
    buffer.append(t);
    buffer.append(EOL);
    for (int i = 0; i <= m; i++)
    {
      buffer.append("  at ");
      buffer.append(trace[i]);
      buffer.append(EOL);
    }
    if (framesToSkip != 0)
    {
      buffer.append("  ... ");
      buffer.append(framesToSkip);
      buffer.append(" more");
      buffer.append(EOL);
    }

    // Recurse if we have a cause
    Throwable ourCause = t.getCause();
    if (ourCause != null)
    {
      formatStackTraceForCause(ourCause, maxDepth, buffer, trace);
    }
  }

  /**
   * Generate a String representation of the possibly filtered stack trace from
   * the current position in execution.
   *
   * @param stackTrace
   *          - The stack trace elements to format.
   * @param maxDepth
   *          - the maximum number of stack frames to include in the trace.
   * @return the stack trace.
   */
  static String formatStackTrace(StackTraceElement[] stackTrace, int maxDepth)
  {
    StringBuilder buffer = new StringBuilder();

    if (stackTrace != null)
    {
      int frameLimit = Math.min(maxDepth, stackTrace.length);
      if (frameLimit > 0)
      {

        for (int i = 0; i < frameLimit; i++)
        {
          buffer.append("  ");
          buffer.append(stackTrace[i]);
          buffer.append(EOL);
        }

        if (frameLimit < stackTrace.length)
        {
          buffer.append("  ...(");
          buffer.append(stackTrace.length - frameLimit);
          buffer.append(" more)");
          buffer.append(EOL);
        }
      }
    }

    return buffer.toString();
  }
}
