package org.infinispan.commons.logging.log4j;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender;
import org.apache.logging.log4j.core.appender.FileManager;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.apache.logging.log4j.core.net.Advertiser;
import org.apache.logging.log4j.core.util.Booleans;
import org.apache.logging.log4j.core.util.Integers;

/**
 * CompressedFile Appender.
 */
@Plugin(name = "CompressedFile", category = "Core", elementType = "appender", printObject = true)
public final class CompressedFileAppender extends AbstractOutputStreamAppender<FileManager> {
   private static final int DEFAULT_BUFFER_SIZE = 8192;
   private final String fileName;
   private final Advertiser advertiser;
   private Object advertisement;

   private CompressedFileAppender(final String name, final Layout<? extends Serializable> layout, final Filter filter,
                                  final FileManager manager,
                                  final String filename, final boolean ignoreExceptions, final boolean immediateFlush,
                                  final Advertiser advertiser) {
      super(name, layout, filter, ignoreExceptions, immediateFlush, manager);
      if (advertiser != null) {
         final Map<String, String> configuration = new HashMap<>(layout.getContentFormat());
         configuration.putAll(manager.getContentFormat());
         configuration.put("contentType", layout.getContentType());
         configuration.put("name", name);
         advertisement = advertiser.advertise(configuration);
      }
      this.fileName = filename;
      this.advertiser = advertiser;
   }

   @Override
   public boolean stop(long timeout, TimeUnit timeUnit, boolean changeLifeCycleState) {
      super.stop(timeout, timeUnit, false);
      if (advertiser != null) {
         advertiser.unadvertise(advertisement);
      }
      if (changeLifeCycleState) {
         setStopped();
      }
      return true;
   }

   /**
    * Returns the file name this appender is associated with.
    *
    * @return The File name.
    */
   public String getFileName() {
      return this.fileName;
   }

   /**
    * Create a File Appender.
    *
    * @param fileName       The name and path of the file.
    * @param append         "True" if the file should be appended to, "false" if it should be overwritten.
    *                       The default is "true".
    * @param locking        "True" if the file should be locked. The default is "false".
    * @param name           The name of the Appender.
    * @param immediateFlush "true" if the contents should be flushed on every write, "false" otherwise. The default
    *                       is "true".
    * @param ignore         If {@code "true"} (default) exceptions encountered when appending events are logged;
    *                       otherwise
    *                       they are propagated to the caller.
    * @param bufferedIo     "true" if I/O should be buffered, "false" otherwise. The default is "true".
    * @param bufferSizeStr  buffer size for buffered IO (default is 8192).
    * @param layout         The layout to use to format the event. If no layout is provided the default PatternLayout
    *                       will be used.
    * @param filter         The filter, if any, to use.
    * @param advertise      "true" if the appender configuration should be advertised, "false" otherwise.
    * @param advertiseUri   The advertised URI which can be used to retrieve the file contents.
    * @param config         The Configuration
    * @return The FileAppender.
    */
   @PluginFactory
   public static CompressedFileAppender createAppender(
      // @formatter:off
      @PluginAttribute("fileName") final String fileName,
      @PluginAttribute("append") final String append,
      @PluginAttribute("locking") final String locking,
      @PluginAttribute("name") final String name,
      @PluginAttribute("immediateFlush") final String immediateFlush,
      @PluginAttribute("ignoreExceptions") final String ignore,
      @PluginAttribute("bufferedIo") final String bufferedIo,
      @PluginAttribute("bufferSize") final String bufferSizeStr,
      @PluginElement("Layout") Layout<? extends Serializable> layout,
      @PluginElement("Filter") final Filter filter,
      @PluginAttribute("advertise") final String advertise,
      @PluginAttribute("advertiseUri") final String advertiseUri,
      @PluginAttribute(value = "compressionLevel", defaultInt = 3) final int compressionLevel,
      @PluginConfiguration final Configuration config) {
      // @formatter:on
      final boolean isAppend = Booleans.parseBoolean(append, true);
      final boolean isLocking = Boolean.parseBoolean(locking);
      boolean isBuffered = Booleans.parseBoolean(bufferedIo, true);
      final boolean isAdvertise = Boolean.parseBoolean(advertise);
      if (isLocking && isBuffered) {
         if (bufferedIo != null) {
            LOGGER.warn("Locking and buffering are mutually exclusive. No buffering will occur for " + fileName);
         }
         isBuffered = false;
      }
      final int bufferSize = Integers.parseInt(bufferSizeStr, DEFAULT_BUFFER_SIZE);
      if (!isBuffered && bufferSize > 0) {
         LOGGER.warn("The bufferSize is set to {} but bufferedIO is not true: {}", bufferSize, bufferedIo);
      }
      final boolean isFlush = Booleans.parseBoolean(immediateFlush, true);
      final boolean ignoreExceptions = Booleans.parseBoolean(ignore, true);

      if (name == null) {
         LOGGER.error("No name provided for CompressedFileAppender");
         return null;
      }

      if (fileName == null) {
         LOGGER.error("No filename provided for CompressedFileAppender with name {}", name);
         return null;
      }
      if (!fileName.endsWith(".gz")) {
         LOGGER.error("Filename {} does not end in .gz for CompressedFileAppender with name {}", fileName, name);
         return null;
      }
      if (layout == null) {
         layout = PatternLayout.createDefaultLayout();
      }

      if (compressionLevel < 1 || 9 < compressionLevel) {
         LOGGER.error("compressionLevel {} is not between 1..9 for CompressedFileAppender with name {}",
                      compressionLevel, name);
         return null;
      }

      final CompressedFileManager manager = CompressedFileManager.getFileManager(fileName, isAppend, isLocking,
                                                                                 isBuffered, advertiseUri,
                                                                                 layout, bufferSize, compressionLevel);
      if (manager == null) {
         return null;
      }

      return new CompressedFileAppender(name, layout, filter, manager, fileName, ignoreExceptions, isFlush,
                                        isAdvertise ? config.getAdvertiser() : null);
   }
}
