/*
 * 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 2013-2015 ForgeRock AS
 */
package org.opends.server.loggers;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.opends.server.admin.server.ConfigurationAddListener;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.server.ConfigurationDeleteListener;
import org.opends.server.admin.std.server.DebugTargetCfg;
import org.opends.server.admin.std.server.FileBasedDebugLogPublisherCfg;
import org.opends.server.api.DirectoryThread;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ServerContext;
import org.opends.server.types.DN;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.FilePermission;
import org.opends.server.types.InitializationException;
import org.opends.server.util.TimeThread;

import static org.opends.messages.ConfigMessages.*;
import static org.opends.server.util.StaticUtils.*;

/**
 * The debug log publisher implementation that writes debug messages to files
 * on disk. It also maintains the rotation and retention polices of the log
 * files.
 */
public class TextDebugLogPublisher
    extends DebugLogPublisher<FileBasedDebugLogPublisherCfg>
    implements ConfigurationChangeListener<FileBasedDebugLogPublisherCfg>,
               ConfigurationAddListener<DebugTargetCfg>,
               ConfigurationDeleteListener<DebugTargetCfg>
{
  private static long globalSequenceNumber;
  private TextWriter writer;
  private FileBasedDebugLogPublisherCfg currentConfig;

  /**
   * Returns an instance of the text debug log publisher that will print all
   * messages to the provided writer, based on the provided debug targets.
   *
   * @param debugTargets
   *          The targets defining which and how debug events are logged.
   * @param writer
   *          The text writer where the message will be written to.
   * @return The instance of the text error log publisher that will print all
   *         messages to standard out. May be {@code null} if no debug target is
   *         valid.
   */
  static TextDebugLogPublisher getStartupTextDebugPublisher(List<String> debugTargets, TextWriter writer)
  {
    TextDebugLogPublisher startupPublisher = null;
    for (String value : debugTargets)
    {
      int settingsStart = value.indexOf(":");

      //See if the scope and settings exists
      if (settingsStart > 0)
      {
        String scope = value.substring(0, settingsStart);
        TraceSettings settings = TraceSettings.parseTraceSettings(value.substring(settingsStart + 1));
        if (settings != null)
        {
          if (startupPublisher == null) {
            startupPublisher = new TextDebugLogPublisher();
            startupPublisher.writer = writer;
          }
          startupPublisher.addTraceSettings(scope, settings);
        }
      }
    }
    return startupPublisher;
  }

  @Override
  public boolean isConfigurationAcceptable(
      FileBasedDebugLogPublisherCfg config, List<LocalizableMessage> unacceptableReasons)
  {
    return isConfigurationChangeAcceptable(config, unacceptableReasons);
  }

  @Override
  public void initializeLogPublisher(FileBasedDebugLogPublisherCfg config, ServerContext serverContext)
      throws ConfigException, InitializationException
  {
    File logFile = getFileForPath(config.getLogFile(), serverContext);
    FileNamingPolicy fnPolicy = new TimeStampNaming(logFile);

    try
    {
      FilePermission perm = FilePermission.decodeUNIXMode(config.getLogFilePermissions());
      LogPublisherErrorHandler errorHandler = new LogPublisherErrorHandler(config.dn());
      boolean writerAutoFlush = config.isAutoFlush() && !config.isAsynchronous();

      MultifileTextWriter writer = new MultifileTextWriter("Multifile Text Writer for " + config.dn(),
                                  config.getTimeInterval(),
                                  fnPolicy,
                                  perm,
                                  errorHandler,
                                  "UTF-8",
                                  writerAutoFlush,
                                  config.isAppend(),
                                  (int)config.getBufferSize());

      // Validate retention and rotation policies.
      for(DN dn : config.getRotationPolicyDNs())
      {
        writer.addRotationPolicy(DirectoryServer.getRotationPolicy(dn));
      }
      for(DN dn: config.getRetentionPolicyDNs())
      {
        writer.addRetentionPolicy(DirectoryServer.getRetentionPolicy(dn));
      }

      if(config.isAsynchronous())
      {
        this.writer = newAsyncWriter(writer, config);
      }
      else
      {
        this.writer = writer;
      }
    }
    catch(DirectoryException e)
    {
      throw new InitializationException(
          ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(config.dn(), e), e);
    }
    catch(IOException e)
    {
      throw new InitializationException(
          ERR_CONFIG_LOGGING_CANNOT_OPEN_FILE.get(logFile, config.dn(), e), e);
    }

    config.addDebugTargetAddListener(this);
    config.addDebugTargetDeleteListener(this);

    addTraceSettings(null, getDefaultSettings(config));

    for(String name : config.listDebugTargets())
    {
      final DebugTargetCfg targetCfg = config.getDebugTarget(name);
      addTraceSettings(targetCfg.getDebugScope(), new TraceSettings(targetCfg));
    }

    currentConfig = config;

    config.addFileBasedDebugChangeListener(this);
  }

  @Override
  public boolean isConfigurationChangeAcceptable(
      FileBasedDebugLogPublisherCfg config, List<LocalizableMessage> unacceptableReasons)
  {
    // Make sure the permission is valid.
    try
    {
      FilePermission filePerm = FilePermission.decodeUNIXMode(config.getLogFilePermissions());
      if (!filePerm.isOwnerWritable())
      {
        LocalizableMessage message = ERR_CONFIG_LOGGING_INSANE_MODE.get(config.getLogFilePermissions());
        unacceptableReasons.add(message);
        return false;
      }
    }
    catch (DirectoryException e)
    {
      unacceptableReasons.add(ERR_CONFIG_LOGGING_MODE_INVALID.get(config.getLogFilePermissions(), e));
      return false;
    }

    return true;
  }

  @Override
  public ConfigChangeResult applyConfigurationChange(FileBasedDebugLogPublisherCfg config)
  {
    final ConfigChangeResult ccr = new ConfigChangeResult();

    addTraceSettings(null, getDefaultSettings(config));
    DebugLogger.updateTracerSettings();

    try
    {
      // Determine the writer we are using. If we were writing asynchronously,
      // we need to modify the underlying writer.
      TextWriter currentWriter;
      if(writer instanceof AsynchronousTextWriter)
      {
        currentWriter = ((AsynchronousTextWriter)writer).getWrappedWriter();
      }
      else
      {
        currentWriter = writer;
      }

      if(currentWriter instanceof MultifileTextWriter)
      {
        MultifileTextWriter mfWriter = (MultifileTextWriter)writer;
        configure(mfWriter, config);

        if (config.isAsynchronous())
        {
          if (writer instanceof AsynchronousTextWriter)
          {
            if (hasAsyncConfigChanged(config))
            {
              // reinstantiate
              final AsynchronousTextWriter previousWriter = (AsynchronousTextWriter) writer;
              writer = newAsyncWriter(mfWriter, config);
              previousWriter.shutdown(false);
            }
          }
          else
          {
            // turn async text writer on
            writer = newAsyncWriter(mfWriter, config);
          }
        }
        else
        {
          if (writer instanceof AsynchronousTextWriter)
          {
            // asynchronous is being turned off, remove async text writers.
            final AsynchronousTextWriter previousWriter = (AsynchronousTextWriter) writer;
            writer = mfWriter;
            previousWriter.shutdown(false);
          }
        }

        if(currentConfig.isAsynchronous() && config.isAsynchronous() &&
            currentConfig.getQueueSize() != config.getQueueSize())
        {
          ccr.setAdminActionRequired(true);
        }

        currentConfig = config;
      }
    }
    catch(Exception e)
    {
      ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
      ccr.addMessage(ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(
          config.dn(), stackTraceToSingleLineString(e)));
    }

    return ccr;
  }

  private AsynchronousTextWriter newAsyncWriter(MultifileTextWriter writer, FileBasedDebugLogPublisherCfg config)
  {
    String name = "Asynchronous Text Writer for " + config.dn();
    return new AsynchronousTextWriter(name, config.getQueueSize(), config.isAutoFlush(), writer);
  }

  private void configure(MultifileTextWriter mfWriter, FileBasedDebugLogPublisherCfg config) throws DirectoryException
  {
    FilePermission perm = FilePermission.decodeUNIXMode(config.getLogFilePermissions());
    boolean writerAutoFlush = config.isAutoFlush() && !config.isAsynchronous();

    File logFile = getLogFile(config);
    FileNamingPolicy fnPolicy = new TimeStampNaming(logFile);

    mfWriter.setNamingPolicy(fnPolicy);
    mfWriter.setFilePermissions(perm);
    mfWriter.setAppend(config.isAppend());
    mfWriter.setAutoFlush(writerAutoFlush);
    mfWriter.setBufferSize((int)config.getBufferSize());
    mfWriter.setInterval(config.getTimeInterval());

    mfWriter.removeAllRetentionPolicies();
    mfWriter.removeAllRotationPolicies();
    for(DN dn : config.getRotationPolicyDNs())
    {
      mfWriter.addRotationPolicy(DirectoryServer.getRotationPolicy(dn));
    }
    for(DN dn: config.getRetentionPolicyDNs())
    {
      mfWriter.addRetentionPolicy(DirectoryServer.getRetentionPolicy(dn));
    }
  }

  private File getLogFile(FileBasedDebugLogPublisherCfg config)
  {
    return getFileForPath(config.getLogFile());
  }

  private boolean hasAsyncConfigChanged(FileBasedDebugLogPublisherCfg newConfig)
  {
    return !currentConfig.dn().equals(newConfig.dn())
        && currentConfig.isAutoFlush() != newConfig.isAutoFlush()
        && currentConfig.getQueueSize() != newConfig.getQueueSize();
  }

  private TraceSettings getDefaultSettings(FileBasedDebugLogPublisherCfg config)
  {
    return new TraceSettings(
        TraceSettings.Level.getLevel(true, config.isDefaultDebugExceptionsOnly()),
        config.isDefaultOmitMethodEntryArguments(),
        config.isDefaultOmitMethodReturnValue(),
        config.getDefaultThrowableStackFrames(),
        config.isDefaultIncludeThrowableCause());
  }

  @Override
  public boolean isConfigurationAddAcceptable(DebugTargetCfg config,
                                              List<LocalizableMessage> unacceptableReasons)
  {
    return !hasTraceSettings(config.getDebugScope());
  }

  @Override
  public boolean isConfigurationDeleteAcceptable(DebugTargetCfg config,
                                              List<LocalizableMessage> unacceptableReasons)
  {
    // A delete should always be acceptable.
    return true;
  }

  @Override
  public ConfigChangeResult applyConfigurationAdd(DebugTargetCfg config)
  {
    addTraceSettings(config.getDebugScope(), new TraceSettings(config));

    DebugLogger.updateTracerSettings();

    return new ConfigChangeResult();
  }

  @Override
  public ConfigChangeResult applyConfigurationDelete(DebugTargetCfg config)
  {
    removeTraceSettings(config.getDebugScope());

    DebugLogger.updateTracerSettings();

    return new ConfigChangeResult();
  }

  @Override
  public void trace(TraceSettings settings, String signature,
      String sourceLocation, String msg, StackTraceElement[] stackTrace)
  {
    String stack = null;
    if (stackTrace != null)
    {
      stack = DebugStackTraceFormatter.formatStackTrace(stackTrace,
          settings.getStackDepth());
    }
    publish(signature, sourceLocation, msg, stack);
  }

  @Override
  public void traceException(TraceSettings settings, String signature,
      String sourceLocation, String msg, Throwable ex,
      StackTraceElement[] stackTrace)
  {
    String message = DebugMessageFormatter.format("%s caught={%s}", new Object[] { msg, ex });

    String stack = null;
    if (stackTrace != null)
    {
      stack = DebugStackTraceFormatter.formatStackTrace(ex, settings.getStackDepth(),
          settings.isIncludeCause());
    }
    publish(signature, sourceLocation, message, stack);
  }

  @Override
  public void close()
  {
    writer.shutdown();

    if(currentConfig != null)
    {
      currentConfig.removeFileBasedDebugChangeListener(this);
    }
  }

  /**
   * Publishes a record, optionally performing some "special" work:
   * - injecting a stack trace into the message
   * - format the message with argument values
   */
  private void publish(String signature, String sourceLocation, String msg,
                       String stack)
  {
    Thread thread = Thread.currentThread();

    StringBuilder buf = new StringBuilder();
    // Emit the timestamp.
    buf.append("[");
    buf.append(TimeThread.getLocalTime());
    buf.append("] ");

    // Emit the seq num
    buf.append(globalSequenceNumber++);
    buf.append(" ");

    // Emit the debug level.
    buf.append("trace ");

    // Emit thread info.
    buf.append("thread={");
    buf.append(thread.getName());
    buf.append("(");
    buf.append(thread.getId());
    buf.append(")} ");

    if(thread instanceof DirectoryThread)
    {
      buf.append("threadDetail={");
      for (Map.Entry<String, String> entry :
        ((DirectoryThread) thread).getDebugProperties().entrySet())
      {
        buf.append(entry.getKey());
        buf.append("=");
        buf.append(entry.getValue());
        buf.append(" ");
      }
      buf.append("} ");
    }

    // Emit method info.
    buf.append("method={");
    buf.append(signature);
    buf.append("(");
    buf.append(sourceLocation);
    buf.append(")} ");

    // Emit message.
    buf.append(msg);

    // Emit Stack Trace.
    if(stack != null)
    {
      buf.append("\nStack Trace:\n");
      buf.append(stack);
    }

    writer.writeRecord(buf.toString());
  }

  @Override
  public DN getDN()
  {
    if(currentConfig != null)
    {
      return currentConfig.dn();
    }
    return null;
  }
}
