/*
 * 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.event;

import static java.util.Collections.emptyList;
import static java.util.Collections.singletonMap;

import org.apache.log4j.Category;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LocationInfo;
import org.apache.log4j.spi.LoggingEvent;
import org.apache.log4j.spi.RootLogger;
import org.apache.log4j.spi.ThrowableInformation;

import java.io.File;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

/**
 * Extension of {@link LoggingEvent} which allows to add custom secure logger specific properties.
 *
 * @author aakimov
 */
public final class SecureLoggingEvent extends LoggingEvent {
  private static final long serialVersionUID = 2288876706425291513L;

  private static final Logger ROOT_LOGGER = new RootLogger(Level.DEBUG);

  private static final Field MDC_COPY;

  static {
    try {
      MDC_COPY = LoggingEvent.class.getDeclaredField("mdcCopy");
    } catch (NoSuchFieldException e) {
      throw new IllegalStateException("Field 'mdcCopy' does not exist.", e);
    }
    MDC_COPY.setAccessible(true);
  }

  private final SecureMessage secureMessage;
  private final Hashtable<?, ?> mdcCopy;

  private SecureLoggingEvent(String fqnOfCategoryClass, Category logger, long timeStamp,
      Level level, SecureMessage secureMessage, String threadName, ThrowableInformation throwable,
      String ndc, LocationInfo info, Map<?, ?> properties) {
    super(fqnOfCategoryClass, logger, timeStamp, level, null, threadName, throwable, ndc, info,
        properties);
    this.secureMessage = secureMessage;
    this.mdcCopy = extractMdcCopy(this);
    // initialize location information now
    getLocationInformation();
  }

  /**
   * Returns a new checkpoint instance.
   *
   * @return a new checkpoint instance.
   */
  public static SecureLoggingEvent newCheckpointInstance() {
    return new SecureLoggingEvent(SecureLoggingEvent.class.getName(), ROOT_LOGGER,
        System.currentTimeMillis(), Level.DEBUG, SecureMessage.getCheckpointInstance(),
        Thread.currentThread().getName(), null, null, null, null);
  }

  /**
   * Creates a new instance from a given logging event created by client.
   *
   * @param event the event
   * @return a new instance.
   */
  public static SecureLoggingEvent newClientInstance(LoggingEvent event) {
    return new SecureLoggingEvent(event.getFQNOfLoggerClass(), event.getLogger(),
        event.getTimeStamp(), event.getLevel(),
        SecureMessage.newClientInstance(event.getRenderedMessage()), event.getThreadName(),
        event.getThrowableInformation(), event.getNDC(), event.getLocationInformation(),
        event.getProperties());
  }

  /**
   * Creates a new file instance for given caller and log file.
   * 
   * @param file the log file
   *
   * @return a new instance.
   */
  public static SecureLoggingEvent newFileInstance(File file) {
    return new SecureLoggingEvent(SecureLoggingEvent.class.getName(), ROOT_LOGGER,
        System.currentTimeMillis(), Level.DEBUG, SecureMessage.newFileInstance(file),
        Thread.currentThread().getName(), null, null, null, null);
  }

  /**
   * Creates a new HMac instance for a given caller
   *
   * @return a new instance.
   */
  public static SecureLoggingEvent newHMacInstance() {
    return new SecureLoggingEvent(SecureLoggingEvent.class.getName(), ROOT_LOGGER,
        System.currentTimeMillis(), Level.DEBUG, SecureMessage.getHMacInstance(),
        Thread.currentThread().getName(), null, null, null, null);
  }

  private static Hashtable<?, ?> extractMdcCopy(SecureLoggingEvent event) {
    try {
      return (Hashtable<?, ?>) MDC_COPY.get(event);
    } catch (IllegalAccessException e) {
      throw new IllegalStateException("Failed to extract MDC copy.", e);
    }
  }

  /**
   * Adds given properties to the secure message.
   *
   * @param properties the properties
   * @return a new instance.
   */
  public SecureLoggingEvent addProperties(Map<String, String> properties) {
    return new SecureLoggingEvent(getFQNOfLoggerClass(), getLogger(), getTimeStamp(), getLevel(),
        secureMessage.addProperties(properties), getThreadName(), getThrowableInformation(),
        getNDC(), getLocationInformation(), mdcCopy);
  }

  /**
   * Adds given property to the secure message.
   *
   * @param key the key
   * @param value the value
   * @return a new instance.
   */
  public SecureLoggingEvent addProperty(String key, String value) {
    return addProperties(singletonMap(key, value));
  }

  @Override
  public Object getMDC(String key) {
    return mdcCopy != null ? mdcCopy.get(key) : null;
  }

  @Override
  public String getRenderedMessage() {
    return secureMessage.render();
  }

  /**
   * Returns the secure message.
   *
   * @return the secure message.
   */
  public SecureMessage getSecureMessage() {
    return secureMessage;
  }

  /**
   * Returns a list of {@link SecureLoggingEvent} representing the throwable information.
   *
   * @return a list of {@link SecureLoggingEvent}
   */
  public List<SecureLoggingEvent> getThrowableEventRep() {
    String[] strings = getThrowableStrRep();
    if (strings == null) {
      return emptyList();
    }
    List<SecureLoggingEvent> events = new ArrayList<>(strings.length);
    for (String string : strings) {
      events.add(new SecureLoggingEvent(fqnOfCategoryClass, getLogger(), getTimeStamp(), getLevel(),
          SecureMessage.newRenderedInstance(string), getThreadName(), null, getNDC(),
          getLocationInformation(), mdcCopy));
    }
    return events;
  }

  /**
   * Returns whether the event is a checkpoint.
   *
   * @return the event is a checkpoint.
   */
  public boolean isCheckpoint() {
    return secureMessage.isCheckpoint();
  }

  /**
   * Removes the secure message properties.
   *
   * @return a new instance.
   */
  public SecureLoggingEvent removeProperties() {
    return new SecureLoggingEvent(getFQNOfLoggerClass(), getLogger(), getTimeStamp(), getLevel(),
        secureMessage.removeProperties(), getThreadName(), getThrowableInformation(), getNDC(),
        getLocationInformation(), mdcCopy);
  }

  /**
   * Removes a given property from the secure message.
   *
   * @param key the key
   * @return a new instance.
   */
  @Override
  public SecureLoggingEvent removeProperty(String key) {
    return new SecureLoggingEvent(getFQNOfLoggerClass(), getLogger(), getTimeStamp(), getLevel(),
        secureMessage.removeProperty(key), getThreadName(), getThrowableInformation(), getNDC(),
        getLocationInformation(), mdcCopy);
  }
}
