/*
 * 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-2009 Sun Microsystems, Inc.
 *      Portions Copyright 2014-2015 ForgeRock AS
 */
package org.opends.server.loggers;

import static org.opends.server.loggers.TraceSettings.Level.*;

import java.util.Map;

import org.forgerock.i18n.slf4j.LocalizedLogger;

/**
 * Class for source-code tracing at the method level.
 *
 * One DebugTracer instance exists for each Java class using tracing.
 * Tracer must be registered with the DebugLogger.
 *
 * Logging is always done at a level basis, with debug log messages
 * exceeding the trace threshold being traced, others being discarded.
 */
public class DebugTracer
{
  /**
   *  We have to hardcode this value because we cannot import
   *  {@code org.opends.server.loggers.slf4j.OpenDJLoggerAdapter.class.getName()}
   *  to avoid OSGI split package issues.
   *  @see OPENDJ-2226
   */
  private static final String OPENDJ_LOGGER_ADAPTER_CLASS_NAME = "org.opends.server.loggers.slf4j.OpenDJLoggerAdapter";

  /** The class this aspect traces. */
  private String className;

  /** A class that represents a settings cache entry. */
  private class PublisherSettings
  {
    private final DebugLogPublisher<?> debugPublisher;
    private final TraceSettings classSettings;
    private final Map<String, TraceSettings> methodSettings;

    private PublisherSettings(String className, DebugLogPublisher<?> publisher)
    {
      debugPublisher = publisher;
      classSettings = publisher.getClassSettings(className);
      methodSettings = publisher.getMethodSettings(className);
    }

    @Override
    public String toString()
    {
      return getClass().getSimpleName() + "("
          + "className=" + className
          + ", classSettings=" + classSettings
          + ", methodSettings=" + methodSettings
          + ")";
    }
  }

  private PublisherSettings[] publisherSettings;

  /**
   * Construct a new DebugTracer object with cached settings obtained from
   * the provided array of publishers.
   *
   * @param className The class name to use as category for logging.
   * @param publishers The array of publishers to obtain the settings from.
   */
  DebugTracer(String className, DebugLogPublisher<?>[] publishers)
  {
    this.className = className;
    publisherSettings = toPublisherSettings(publishers);
  }

  /**
   * Log the provided message.
   *
   * @param msg
   *          message to log.
   */
  public void trace(String msg)
  {
    traceException(msg, null);
  }

  /**
   * Log the provided message and exception.
   *
   * @param msg
   *          the message
   * @param exception
   *          the exception caught. May be {@code null}.
   */
  public void traceException(String msg, Throwable exception)
  {
    StackTraceElement[] stackTrace = null;
    StackTraceElement[] filteredStackTrace = null;
    StackTraceElement callerFrame = null;
    final boolean hasException = exception != null;
    for (PublisherSettings settings : publisherSettings)
    {
      TraceSettings activeSettings = settings.classSettings;
      Map<String, TraceSettings> methodSettings = settings.methodSettings;

      if (shouldLog(activeSettings, hasException) || methodSettings != null)
      {
        if (stackTrace == null)
        {
          stackTrace = Thread.currentThread().getStackTrace();
        }
        if (callerFrame == null)
        {
          callerFrame = getCallerFrame(stackTrace);
        }

        String signature = callerFrame.getMethodName();

        // Specific method settings still could exist. Try getting
        // the settings for this method.
        if (methodSettings != null)
        {
          TraceSettings mSettings = methodSettings.get(signature);
          if (mSettings == null)
          {
            // Try looking for an undecorated method name
            int idx = signature.indexOf('(');
            if (idx != -1)
            {
              mSettings = methodSettings.get(signature.substring(0, idx));
            }
          }

          // If this method does have a specific setting
          // and it is not supposed to be logged, continue.
          if (!shouldLog(mSettings, hasException))
          {
            continue;
          }
          activeSettings = mSettings;
        }

        String sourceLocation = callerFrame.getFileName() + ":" + callerFrame.getLineNumber();

        if (filteredStackTrace == null && activeSettings.getStackDepth() > 0)
        {
          StackTraceElement[] trace = hasException ? exception.getStackTrace() : stackTrace;
          filteredStackTrace = DebugStackTraceFormatter.SMART_FRAME_FILTER.getFilteredStackTrace(trace);
        }

        if (hasException)
        {
          settings.debugPublisher.traceException(activeSettings, signature,
              sourceLocation, msg, exception, filteredStackTrace);
        }
        else
        {
          settings.debugPublisher.trace(activeSettings, signature,
              sourceLocation, msg, filteredStackTrace);
        }
      }
    }
  }

  /**
   * Gets the name of the class this tracer traces.
   *
   * @return The name of the class this tracer traces.
   */
  String getTracedClassName()
  {
    return className;
  }

  /**
   * Indicates if logging is enabled for at least one category
   * in a publisher.
   *
   * @return {@code true} if logging is enabled, false otherwise.
   */
  public boolean enabled()
  {
    for (PublisherSettings settings : publisherSettings)
    {
      if (shouldLog(settings.classSettings) || settings.methodSettings != null)
      {
        return true;
      }
    }
    return false;
  }

  /**
   * Update the cached settings of the tracer with the settings from the
   * provided publishers.
   *
   * @param publishers The array of publishers to obtain the settings from.
   */
  void updateSettings(DebugLogPublisher<?>[] publishers)
  {
    publisherSettings = toPublisherSettings(publishers);
  }

  private PublisherSettings[] toPublisherSettings(DebugLogPublisher<?>[] publishers)
  {
    // Get the settings from all publishers.
    PublisherSettings[] newSettings = new PublisherSettings[publishers.length];
    for(int i = 0; i < publishers.length; i++)
    {
      newSettings[i] = new PublisherSettings(className, publishers[i]);
    }
    return newSettings;
  }

  /**
   * Return the caller stack frame.
   *
   * @param stackTrace
   *          The stack trace frames of the caller.
   * @return the caller stack frame or null if none is found on the stack trace.
   */
  private StackTraceElement getCallerFrame(StackTraceElement[] stackTrace)
  {
    if (stackTrace != null && stackTrace.length > 0)
    {
      // Skip all logging related classes
      for (StackTraceElement aStackTrace : stackTrace)
      {
        if(!isLoggingStackTraceElement(aStackTrace))
        {
          return aStackTrace;
        }
      }
    }

    return null;
  }

  /**
   * Checks if element belongs to a class responsible for logging
   * (includes the Thread class that may be used to get the stack trace).
   *
   * @param trace
   *            the trace element to check.
   * @return {@code true} if element corresponds to logging
   */
  static boolean isLoggingStackTraceElement(StackTraceElement trace)
  {
    String name = trace.getClassName();
    return name.startsWith(Thread.class.getName())
        || name.startsWith(DebugTracer.class.getName())
        || name.startsWith(OPENDJ_LOGGER_ADAPTER_CLASS_NAME)
        || name.startsWith(LocalizedLogger.class.getName());
  }

  /** Indicates if there is something to log. */
  private boolean shouldLog(TraceSettings settings, boolean hasException)
  {
    return settings != null
        && (settings.getLevel() == ALL
          || (hasException && settings.getLevel() == EXCEPTIONS_ONLY));
  }

  /** Indicates if there is something to log. */
  private boolean shouldLog(TraceSettings settings)
  {
    return settings.getLevel() != DISABLED;
  }
}
