/**
 * Logback: the reliable, generic, fast and flexible logging framework.
 * 
 * Copyright (C) 1999-2006, QOS.ch
 * 
 * This library is free software, you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation.
 */

package ch.qos.logback.classic.spi;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Map;

import org.slf4j.MDC;
import org.slf4j.Marker;
import org.slf4j.helpers.MessageFormatter;
import org.slf4j.impl.LogbackMDCAdapter;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;

/**
 * The internal representation of logging events. When an affirmative decision
 * is made to log then a <code>LoggingEvent</code> instance is created. This
 * instance is passed around to the different Logback components.
 * 
 * <p> Writers of Logback components such as appenders should be aware of that
 * some of the LoggingEvent fields are initialized lazily. Therefore, an
 * appender wishing to output data to be later correctly read by a receiver,
 * must initialize "lazy" fields prior to writing them out. See the
 * {@link #prepareForDeferredProcessing()} method for the exact list. </p>
 * 
 * @author Ceki G&uuml;lc&uuml;
 * @author S&eacute;bastien Pennec
 */
public class LoggingEvent implements Serializable {

  private static final long serialVersionUID = 3075964498087694229L;

  private static final int NULL_ARGUMENT_ARRAY = -1;
  private static final String NULL_ARGUMENT_ARRAY_ELEMENT = "NULL_ARGUMENT_ARRAY_ELEMENT";

  /**
   * 
   */
  private static long startTime = System.currentTimeMillis();

  /**
   * Fully qualified name of the calling Logger class. This field does not
   * survive serialization.
   * 
   * <p> Note that the getCallerInformation() method relies on this fact.
   */
  transient String fqnOfLoggerClass;

  /**
   * The name of thread in which this logging event was generated.
   */
  private String threadName;

  /**
   * Level of logging event.
   * 
   * <p> This field should not be accessed directly. You shoud use the {@link
   * #getLevel} method instead. </p>
   * 
   */
  private transient Level level;

  private String message;

  // we gain significant space at serialization time by marking
  // formattedMessage as transient and constructing it lazily in
  // getFormmatedMessage()
  private transient String formattedMessage;

  private transient Object[] argumentArray;

  private ThrowableProxy throwableProxy;

  private CallerData[] callerDataArray;
  private LoggerRemoteView loggerRemoteView;

  private Marker marker;

  private Map<String, String> mdcPropertyMap;

  /**
   * The number of milliseconds elapsed from 1/1/1970 until logging event was
   * created.
   */
  private long timeStamp;

  public LoggingEvent() {
  }

  public LoggingEvent(String fqcn, Logger logger, Level level, String message,
      Throwable throwable, Object[] argArray) {
    this.fqnOfLoggerClass = fqcn;
    this.loggerRemoteView = logger.getLoggerRemoteView();
    this.level = level;
    this.message = message;

    if (throwable != null) {
      this.throwableProxy = new ThrowableProxy(throwable);
    }

    // bug 85 (we previously failed to set this.argumentArray)
    this.argumentArray = argArray;

    timeStamp = System.currentTimeMillis();

    // the case is ugly but under the circumstances acceptable
    LogbackMDCAdapter logbackMDCAdapter = (LogbackMDCAdapter) MDC
        .getMDCAdapter();
    mdcPropertyMap = logbackMDCAdapter.getPropertyMap();
  }

  public void setArgumentArray(Object[] argArray) {
    if (this.argumentArray != null) {
      throw new IllegalStateException("argArray has been already set");
    }
    this.argumentArray = argArray;
  }

  public Object[] getArgumentArray() {
    return this.argumentArray;
  }

  public Level getLevel() {
    return level;
  }

  public String getThreadName() {
    if (threadName == null) {
      threadName = (Thread.currentThread()).getName();
    }
    return threadName;
  }

  /**
   * @param threadName
   *                The threadName to set.
   * @throws IllegalStateException
   *                 If threadName has been already set.
   */
  public void setThreadName(String threadName) throws IllegalStateException {
    if (this.threadName != null) {
      throw new IllegalStateException("threadName has been already set");
    }
    this.threadName = threadName;
  }

  /**
   * Returns the throwable information contained within this event. May be
   * <code>null</code> if there is no such information.
   */
  public ThrowableProxy getThrowableProxy() {
    return throwableProxy;
  }

  /**
   * Set this event's throwable information.
   */
  public void setThrowableProxy(ThrowableProxy tp) {
    if (throwableProxy != null) {
      throw new IllegalStateException("ThrowableProxy has been already set.");
    } else {
      throwableProxy = tp;
    }
  }

  /**
   * This method should be called prior to serializing an event. It should also
   * be called when using asynchronous logging.
   * 
   * <p> Note that due to performance concerns, this method does NOT extract
   * caller data. It is the responsability of the calller to extract caller
   * information.
   */
  public void prepareForDeferredProcessing() {
    this.getThreadName();
  }

  public LoggerRemoteView getLoggerRemoteView() {
    return loggerRemoteView;
  }

  public void setLoggerRemoteView(LoggerRemoteView loggerRemoteView) {
    this.loggerRemoteView = loggerRemoteView;
  }

  public String getMessage() {
    return message;
  }

  public void setMessage(String message) {
    if (this.message != null) {
      throw new IllegalStateException(
          "The message for this event has been set already.");
    }
    this.message = message;
  }

  public long getTimeStamp() {
    return timeStamp;
  }

  public void setTimeStamp(long timeStamp) {
    this.timeStamp = timeStamp;
  }

  public void setLevel(Level level) {
    if (this.level != null) {
      throw new IllegalStateException(
          "The level has been already set for this event.");
    }
    this.level = level;
  }

  /**
   * The time at which this class was loaded into memory, expressed in
   * millisecond elapsed since the epoch (1.1.1970).
   * 
   * @return The time as measured when this class was loaded into memory.
   */
  public static final long getStartTime() {
    return startTime;
  }

  /**
   * Get the caller information for this logging event. If caller information is
   * null at the time of its invocation, this method extracts location
   * information. The collected information is cached for future use.
   * 
   * <p> Note that after serialization it is impossible to correctly extract
   * caller information. </p>
   */
  public CallerData[] getCallerData() {
    // we rely on the fact that fqnOfLoggerClass does not survive
    // serialization
    if (callerDataArray == null && fqnOfLoggerClass != null) {
      callerDataArray = CallerData.extract(new Throwable(), fqnOfLoggerClass);
    }
    return callerDataArray;
  }

  public void setCallerInformation(CallerData[] callerDataArray) {
    this.callerDataArray = callerDataArray;
  }

  public Marker getMarker() {
    return marker;
  }

  public void setMarker(Marker marker) {
    if (this.marker != null) {
      throw new IllegalStateException(
          "The marker has been already set for this event.");
    }
    this.marker = marker;
  }

  // computer formatted lazy as suggested in
  // http://jira.qos.ch/browse/LBCLASSIC-47
  public String getFormattedMessage() {
    if (formattedMessage != null) {
      return formattedMessage;
    }

    if (argumentArray != null) {
      formattedMessage = MessageFormatter.arrayFormat(message, argumentArray);
    } else {
      formattedMessage = message;
    }

    return formattedMessage;
  }

  public Map<String, String> getMDCPropertyMap() {
    return mdcPropertyMap;
  }

  private void writeObject(ObjectOutputStream out) throws IOException {
    out.defaultWriteObject();
    out.writeInt(level.levelInt);
    if (argumentArray != null) {
      int len = argumentArray.length;
      out.writeInt(len);
      for (int i = 0; i < argumentArray.length; i++) {
        if (argumentArray[i] != null) {
          out.writeObject(argumentArray[i].toString());
        } else {
          out.writeObject(NULL_ARGUMENT_ARRAY_ELEMENT);
        }
      }
    } else {
      out.writeInt(NULL_ARGUMENT_ARRAY);
    }

  }

  private void readObject(ObjectInputStream in) throws IOException,
      ClassNotFoundException {
    in.defaultReadObject();
    int levelInt = in.readInt();
    level = Level.toLevel(levelInt);

    int argArrayLen = in.readInt();
    if (argArrayLen != NULL_ARGUMENT_ARRAY) {
      argumentArray = new String[argArrayLen];
      for (int i = 0; i < argArrayLen; i++) {
        Object val = in.readObject();
        if (!NULL_ARGUMENT_ARRAY_ELEMENT.equals(val)) {
          argumentArray[i] = val;
        }
      }
    }
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append('[');
    sb.append(level).append("] ");
    sb.append(getFormattedMessage());
    return sb.toString();
  }
}
