/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt'
 * which is part of this source code package
 */

package com.scytl.slogger;

import static java.util.Collections.emptyList;

import com.scytl.slogger.event.SecureLoggingEvent;
import com.scytl.slogger.event.SecurePatternLayout;
import com.scytl.slogger.policy.LogFileNamePolicies;
import com.scytl.slogger.policy.LogFileNamePolicy;
import com.scytl.slogger.policy.LogFileNamePolicyException;
import com.scytl.slogger.processor.EventProcessorException;
import com.scytl.slogger.processor.LoggingEventProcessor;

import org.apache.log4j.FileAppender;
import org.apache.log4j.Layout;
import org.apache.log4j.spi.ErrorCode;
import org.apache.log4j.spi.LoggingEvent;

import java.io.File;
import java.lang.reflect.Constructor;
import java.util.List;

/**
 * Abstract base class that implements a log4j file appender. It is based in the use of
 * LoggingEventProcessors to process any log received by the appender.
 */
public abstract class SecureFileAppender extends FileAppender {

  private static final String SECURE_APPENDER_ERROR = "Secure Appender Error";

  private LoggingEventProcessor loggingEventProcessor;

  private String pattern;

  private String verifyPattern;

  private long triggeringTime;

  private RollingTimer rollingTimer;

  private String logFileNamePolicyName;

  private LogFileNamePolicy logFileNamePolicy;

  protected File lastLogFile;

  /**
   * Blank constructor, pretended only to create the appender from a logj4.xml descriptor. Please
   * don't use it directly.
   */
  public SecureFileAppender() {}

  @Override
  public void activateOptions() {

    int errors = 0;

    if (fileName == null && getLogFileNamePolicy() == null) {
      errors++;
      logError("File option or file policy not set for appender");
    } else {
      if (logFileNamePolicyName == null) {
        logFileNamePolicyName = LogFileNamePolicies.DEFAULT.name();
      }

      try {
        Class<? extends LogFileNamePolicy> policyClass =
            LogFileNamePolicies.valueOf(logFileNamePolicyName).getPolicy();
        Constructor<? extends LogFileNamePolicy> constructor =
            policyClass.getConstructor(File.class);
        constructor.setAccessible(true);

        logFileNamePolicy = constructor.newInstance(new File(fileName));

        newLogFile();
      } catch (Exception e) {
        errors++;
        logError("Error getting new log file name", e);
      }

    }

    rollingTimer = new RollingTimer(triggeringTime);
    if (pattern == null) {
      errors++;
      logError("Pattern option not set for appender");
    }

    if (verifyPattern == null) {
      errors++;
      logError("Verify Pattern option not set for appender");
    }

    if (errors == 0) {
      super.activateOptions();
    }
  }

  @Override
  public synchronized void append(final LoggingEvent event) {
    super.append(event);
  }

  @Override
  public synchronized void close() {
    if (!closed) {
      try {
        List<SecureLoggingEvent> events = emptyList();
        if (loggingEventProcessor != null) {
          events = loggingEventProcessor.stopProcessor((SecurePatternLayout) getLayout());
        }

        for (SecureLoggingEvent loggingEvent : events) {
          super.subAppend(loggingEvent);
        }
      } catch (Exception e) {
        logError(SECURE_APPENDER_ERROR, e);
      } finally {
        super.close();
      }
    }
  }

  /**
   * Forces close.
   */
  public synchronized void forceClose() {
    try {
      loggingEventProcessor.killProcessor();
    } catch (EventProcessorException e) {
      logError("Error killing the processors", e);
    } finally {
      super.close();
    }
  }

  /**
   * Returns the log file name policy.
   *
   * @return the log file name policy.
   */
  public LogFileNamePolicy getLogFileNamePolicy() {
    return logFileNamePolicy;
  }

  /**
   * Returns the log file name policy name.
   *
   * @return the log file name policy name.
   */
  public String getLogFileNamePolicyName() {
    return logFileNamePolicyName;
  }

  /**
   * Returns the logging event processor.
   *
   * @return the logging event processor.
   */
  public LoggingEventProcessor getLoggingEventProcessor() {
    return loggingEventProcessor;
  }

  public String getPattern() {
    return pattern;
  }

  /**
   * Returns the triggering time.
   *
   * @return the triggering time.
   */
  public long getTriggeringTime() {
    return triggeringTime;
  }

  /**
   * Returns the verify pattern.
   *
   * @return the verify pattern.
   */
  public String getVerifyPattern() {
    return verifyPattern;
  }

  @Override
  public void setLayout(final Layout layout) {
    logError("Layout option can not be set for appender");
  }

  /**
   * Sets the log file name policy name.
   *
   * @param logFileNamePolicyName the log file name policy name.
   */
  public void setLogFileNamePolicyName(final String logFileNamePolicyName) {
    this.logFileNamePolicyName = logFileNamePolicyName;
  }

  /**
   * Sets the logging event processor.
   *
   * @param loggingEventProcessor the logging event processor.
   */
  public void setLoggingEventProcessor(final LoggingEventProcessor loggingEventProcessor) {
    this.loggingEventProcessor = loggingEventProcessor;
  }

  /**
   * Sets the pattern.
   *
   * @param pattern The pattern to set.
   */
  public void setPattern(final String pattern) {
    this.pattern = pattern;
    super.setLayout(new SecurePatternLayout(pattern));
  }

  /**
   * Sets the triggering time.
   *
   * @param the triggering time.
   */
  public void setTriggeringTime(final long triggeringTime) {
    this.triggeringTime = triggeringTime;
  }

  /**
   * Sets the verification pattern.
   *
   * @param verifyPattern the verification pattern..
   */
  public void setVerifyPattern(final String verifyPattern) {
    this.verifyPattern = verifyPattern;
  }

  /**
   * Appends the header to the log file.
   *
   * @throws EventProcessorException failed to append the header
   */
  protected void appendHeader() throws EventProcessorException {
    appendFirstLogs();
    rollingTimer.startTimer();
  }

  /**
   * Logs the error.
   *
   * @param description The description of the error.
   */
  protected void logError(final String description) {
    getErrorHandler().error(getErrorDescription(description));
  }

  /**
   * Logs the error.
   *
   * @param description The description of the error.
   * @param error The Exception thrown.
   */
  protected void logError(final String description, final Exception error) {
    getErrorHandler().error(getErrorDescription(description), error, ErrorCode.GENERIC_FAILURE);
  }

  @Override
  protected void subAppend(final LoggingEvent logEvent) {
    SecureLoggingEvent event = SecureLoggingEvent.newClientInstance(logEvent);
    try {
      checkRollingTimer(event);
      processEvent(event);
    } catch (EventProcessorException e) {
      logError(SECURE_APPENDER_ERROR, e);
    } catch (LogFileNamePolicyException e) {
      logError(SECURE_APPENDER_ERROR, e);
    }
  }

  @Override
  protected boolean checkEntryConditions() {
    if (loggingEventProcessor == null) {
      errorHandler.error("No logging event processor set for the appender named [" + name + "].");
      return false;
    }
    return super.checkEntryConditions();
  }

  private void appendFirstLogs() throws EventProcessorException {
    List<SecureLoggingEvent> loggingEventList =
        loggingEventProcessor.startProcessor((SecurePatternLayout) getLayout());
    for (SecureLoggingEvent loggingEvent : loggingEventList) {
      super.subAppend(loggingEvent);
    }
  }

  private void appendThrowableInfo(final SecureLoggingEvent event) {
    if (super.layout.ignoresThrowable()) {
      event.getThrowableEventRep().forEach(this::subAppend);
    }
  }

  private void checkRollingTimer(final SecureLoggingEvent event)
      throws EventProcessorException, LogFileNamePolicyException {
    if (rollingTimer.isTriggeringEvent() && !event.isCheckpoint()) {
      List<SecureLoggingEvent> stopEvents =
          loggingEventProcessor.stopProcessor((SecurePatternLayout) getLayout());
      for (SecureLoggingEvent loggingEvent : stopEvents) {
        subAppendImpl(loggingEvent);
      }

      newLogFile();
      super.activateOptions();
      appendHeader();
    }
  }

  private String getErrorDescription(final String description) {
    StringBuffer sb = new StringBuffer(description);
    sb.append(" [");
    sb.append(name);
    sb.append("].");
    return sb.toString();
  }

  private void newLogFile() throws LogFileNamePolicyException {
    closeWriter();
    getLogFileNamePolicy().closeCurrentFile();
    setAppend(true);
    setFile(getLogFileNamePolicy().nextLogFile().getAbsolutePath());
    lastLogFile = getLogFileNamePolicy().lastLogFile();
  }

  private void processEvent(final SecureLoggingEvent event) throws EventProcessorException {
    List<SecureLoggingEvent> loggingEventList =
        loggingEventProcessor.processEvent(event, (SecurePatternLayout) getLayout());
    for (SecureLoggingEvent loggingEvent : loggingEventList) {
      subAppendImpl(loggingEvent);
    }
  }

  private void subAppendImpl(final SecureLoggingEvent event) {

    super.subAppend(event);
    appendThrowableInfo(event);
  }
}
