/*
 * 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 java.util.List;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.server.DebugTargetCfg;

/** This class encapsulates the trace settings in effect at a given tracing scope. */
public class TraceSettings implements
    ConfigurationChangeListener<DebugTargetCfg>
{
  /** A TraceSettings object representing a fully disabled trace state. */
  public static final TraceSettings DISABLED = new TraceSettings(Level.DISABLED);

  private static final String STACK_DUMP_KEYWORD = "stack";
  private static final String INCLUDE_CAUSE_KEYWORD = "cause";
  private static final String SUPPRESS_ARG_KEYWORD = "noargs";
  private static final String SUPPRESS_RETVAL_KEYWORD = "noretval";
  private static final String ENABLED_KEYWORD = "enabled";
  private static final String EXCEPTIONS_ONLY_KEYWORD = "exceptionsonly";

  /** Represents the level of trace. */
  enum Level
  {
    /** Log nothing. */
    DISABLED,
    /** Log only exceptions. */
    EXCEPTIONS_ONLY,
    /** Log everything. */
    ALL;

    /**
     * Returns the level corresponding to provided options.
     *
     * @param isEnabled
     *          Indicates if tracer is enabled.
     * @param isDebugExceptionsOnly
     *          Indicates if tracer should log only exceptions.
     * @return the level corresponding to options
     */
    static Level getLevel(boolean isEnabled, boolean isDebugExceptionsOnly)
    {
      if (isEnabled)
      {
        if (isDebugExceptionsOnly)
        {
          return Level.EXCEPTIONS_ONLY;
        }
        return Level.ALL;
      }
      return Level.DISABLED;
    }
  }

  /** The level of this setting. */
  private Level level;
  /** Indicates if method arguments should be logged. */
  private boolean noArgs;
  /** Indicates if method return values should be logged. */
  private boolean noRetVal;
  /** The level of stack frames to include. */
  private int stackDepth;
  /** Indicates if the cause exception is included in exception messages. */
  private boolean includeCause;

  /** Construct new trace settings with default values. */
  public TraceSettings()
  {
    this(Level.ALL, false, false, 0, false);
  }

  /**
   * Construct new trace settings at provided level.
   *
   * @param level
   *          Level for this settings.
   */
  private TraceSettings(Level level)
  {
    this(level, false, false, 0, false);
  }

  /**
   * Construct new trace settings at the specified level. Optionally turn off
   * arguments, return value in entry and exit messages, and specifying the
   * depth of stack traces and whether to include the cause of exceptions.
   *
   * @param level
   *          the level for this setting.
   * @param noArgs
   *          whether to include arguments in the log messages.
   * @param noRetVal
   *          whether to include return values in the log messages.
   * @param stackDepth
   *          the stack depth to display in log messages.
   * @param includeCause
   *          whether to include the cause of exceptions.
   */
  TraceSettings(Level level, boolean noArgs,
      boolean noRetVal, int stackDepth, boolean includeCause)
  {
    this.level = level;
    this.noArgs = noArgs;
    this.noRetVal = noRetVal;
    this.stackDepth = stackDepth;
    this.includeCause = includeCause;
  }

  /**
   * Construct a new trace settings from the provided configuration.
   *
   * @param config
   *          The debug target configuration that contains the information to
   *          use to initialize this trace setting.
   */
  TraceSettings(DebugTargetCfg config)
  {
    this.level = Level.getLevel(config.isEnabled(), config.isDebugExceptionsOnly());
    this.noArgs = config.isOmitMethodEntryArguments();
    this.noRetVal = config.isOmitMethodReturnValue();
    this.stackDepth = config.getThrowableStackFrames();
    this.includeCause = config.isIncludeThrowableCause();

    config.addChangeListener(this);
  }

  @Override
  public boolean isConfigurationChangeAcceptable(DebugTargetCfg config,
      List<LocalizableMessage> unacceptableReasons)
  {
    // This should always be acceptable. We are assuming that the scope for this
    // trace setting is the same since it is part of the DN.
    return true;
  }

  @Override
  public ConfigChangeResult applyConfigurationChange(DebugTargetCfg config)
  {
    final ConfigChangeResult ccr = new ConfigChangeResult();
    // We can assume that the target scope did not change since it is the
    // naming attribute. Changing it would result in a modify DN.

    this.level = Level.getLevel(config.isEnabled(), config.isDebugExceptionsOnly());
    this.noArgs = config.isOmitMethodEntryArguments();
    this.noRetVal = config.isOmitMethodReturnValue();
    this.stackDepth = config.getThrowableStackFrames();
    this.includeCause = config.isIncludeThrowableCause();

    return ccr;
  }

  /**
   * Parse trace settings from the string representation.
   *
   * @param value
   *          the trace settings string to be parsed.
   * @return the trace settings parsed from the string.
   */
  protected static TraceSettings parseTraceSettings(String value)
  {
    TraceSettings settings = null;
    if (value != null)
    {
      boolean enabled = false;
      boolean exceptionsOnly = false;
      boolean noArgs = false;
      boolean noRetVal = false;
      int stackDepth = 0;
      boolean includeCause = false;

      String[] keywords = value.split(",");

      for (String keyword : keywords)
      {
        //See if stack dump keyword is included
        if (keyword.startsWith(STACK_DUMP_KEYWORD))
        {
          //See if a stack depth is included
          if (keyword.length() == STACK_DUMP_KEYWORD.length())
          {
            stackDepth = DebugStackTraceFormatter.COMPLETE_STACK;
          }
          else
          {
            int depthStart = keyword.indexOf("=", STACK_DUMP_KEYWORD.length());
            if (depthStart == STACK_DUMP_KEYWORD.length())
            {
              try
              {
                stackDepth = Integer.valueOf(keyword.substring(depthStart + 1));
              }
              catch (NumberFormatException nfe)
              { // TODO: i18n
                System.err.println("The keyword " + STACK_DUMP_KEYWORD
                    + " contains an invalid depth value. The complete stack "
                    + "will be included.");
              }
            }
          }
        }
        //See if to include cause in exception messages.
        else if (INCLUDE_CAUSE_KEYWORD.equals(keyword))
        {
          includeCause = true;
        }
        // See if to suppress method arguments.
        else if (SUPPRESS_ARG_KEYWORD.equals(keyword))
        {
          noArgs = true;
        }
        // See if to suppress return values.
        else if (SUPPRESS_RETVAL_KEYWORD.equals(keyword))
        {
          noRetVal = true;
        }
        else if (ENABLED_KEYWORD.equals(keyword))
        {
          enabled = true;
        }
        else if (EXCEPTIONS_ONLY_KEYWORD.equals(keyword))
        {
          exceptionsOnly = true;
        }
      }
      settings =
          new TraceSettings(Level.getLevel(enabled, exceptionsOnly),
              noArgs, noRetVal, stackDepth, includeCause);
    }

    return settings;
  }

  /**
   * Get the level of this setting.
   *
   * @return the level of this setting.
   */
  public Level getLevel()
  {
    return level;
  }

  /**
   * Get whether method arguments should be logged.
   *
   * @return if method arguments should be logged.
   */
  public boolean isNoArgs()
  {
    return noArgs;
  }

  /**
   * Get whether method return values should be logged.
   *
   * @return if method return values should be logged.
   */
  public boolean isNoRetVal()
  {
    return noRetVal;
  }

  /**
   * Get the level of stack frames to include.
   *
   * @return the level of stack frames to include.
   */
  public int getStackDepth()
  {
    return stackDepth;
  }

  /**
   * Get whether the cause exception is included in exception messages.
   *
   * @return if the cause exception is included in exception messages.
   */
  public boolean isIncludeCause()
  {
    return includeCause;
  }

  @Override
  public String toString()
  {
    return getClass().getSimpleName() + "("
        + "level=" + level
        + ", logMethodArguments=" + !noArgs
        + ", logMethodReturnValue=" + !noRetVal
        + ", logCauseException=" + includeCause
        + ", stackDepth=" + stackDepth
        + ")";
  }
}
