/**
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v1.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation.
 */
package ch.qos.logback.classic.spi;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.time.Instant;
import java.util.List;
import java.util.Map;

import org.slf4j.Marker;
import org.slf4j.event.KeyValuePair;
import org.slf4j.helpers.MessageFormatter;

import ch.qos.logback.classic.Level;

// http://www.riehle.org/computer-science/research/1998/ubilab-tr-1998-10-1.html
// See also the paper https://www.riehle.org/computer-science/research/1998/ubilab-tr-1998-10-1.pdf

/**
 * A read-only and serializable implementation of {@link ILoggingEvent}.
 * 
 * @author Ceki G&uuml;lc&uuml;
 * @since 0.9.16
 */
public class LoggingEventVO implements ILoggingEvent, Serializable {

    private static final long serialVersionUID = 6553722650255690312L;

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

    private String threadName;
    private String loggerName;
    private LoggerContextVO loggerContextVO;

    private transient Level level;
    private String message;

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

    private transient Object[] argumentArray;

    private ThrowableProxyVO throwableProxy;
    private StackTraceElement[] callerDataArray;
    private List<Marker> markerList;
    private List<KeyValuePair> keyValuePairList;
    private Map<String, String> mdcPropertyMap;

    private long timestamp;
    private int nanoseconds;

    private long sequenceNumber;

    public static LoggingEventVO build(ILoggingEvent le) {
        LoggingEventVO ledo = new LoggingEventVO();
        ledo.loggerName = le.getLoggerName();
        ledo.loggerContextVO = le.getLoggerContextVO();
        ledo.threadName = le.getThreadName();
        ledo.level = (le.getLevel());
        ledo.message = (le.getMessage());
        ledo.argumentArray = (le.getArgumentArray());
        ledo.markerList = le.getMarkerList();
        ledo.keyValuePairList = le.getKeyValuePairs();
        ledo.mdcPropertyMap = le.getMDCPropertyMap();
        ledo.timestamp = le.getTimeStamp();
        ledo.nanoseconds = le.getNanoseconds();
        ledo.sequenceNumber = le.getSequenceNumber();
        ledo.throwableProxy = ThrowableProxyVO.build(le.getThrowableProxy());
        // add caller data only if it is there already
        // fixes http://jira.qos.ch/browse/LBCLASSIC-145
        if (le.hasCallerData()) {
            ledo.callerDataArray = le.getCallerData();
        }
        return ledo;
    }

    public String getThreadName() {
        return threadName;
    }

    public LoggerContextVO getLoggerContextVO() {
        return loggerContextVO;
    }

    public String getLoggerName() {
        return loggerName;
    }

    public Level getLevel() {
        return level;
    }

    public String getMessage() {
        return message;
    }

    public String getFormattedMessage() {
        if (formattedMessage != null) {
            return formattedMessage;
        }

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

        return formattedMessage;
    }

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

    public IThrowableProxy getThrowableProxy() {
        return throwableProxy;
    }

    public StackTraceElement[] getCallerData() {
        return callerDataArray;
    }

    public boolean hasCallerData() {
        return callerDataArray != null;
    }

    public List<Marker> getMarkerList() {
        return markerList;
    }

    @Override
    public long getTimeStamp() { return timestamp; }

    @Override
    public int getNanoseconds() {  return nanoseconds; }

    public long getSequenceNumber() {
        return sequenceNumber;
    }

    public long getContextBirthTime() {
        return loggerContextVO.getBirthTime();
    }

    public LoggerContextVO getContextLoggerRemoteView() {
        return loggerContextVO;
    }

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

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

    @Override
    public List<KeyValuePair> getKeyValuePairs() {
        return this.keyValuePairList;
    }

    public void prepareForDeferredProcessing() {
    }

    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();

        // Prevent DOS attacks via large or negative arrays
        if (argArrayLen < NULL_ARGUMENT_ARRAY || argArrayLen > ARGUMENT_ARRAY_DESERIALIZATION_LIMIT) {
            throw new InvalidObjectException("Argument array length is invalid: " + argArrayLen);
        }

        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 int hashCode() {
        final int prime = 31;
        long millis = getTimeStamp();

        int result = 1;
        result = prime * result + ((message == null) ? 0 : message.hashCode());
        result = prime * result + ((threadName == null) ? 0 : threadName.hashCode());
        result = prime * result + (int) (millis ^ (millis >>> 32));
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        final LoggingEventVO other = (LoggingEventVO) obj;
        if (message == null) {
            if (other.message != null)
                return false;
        } else if (!message.equals(other.message))
            return false;

        if (loggerName == null) {
            if (other.loggerName != null)
                return false;
        } else if (!loggerName.equals(other.loggerName))
            return false;

        if (threadName == null) {
            if (other.threadName != null)
                return false;
        } else if (!threadName.equals(other.threadName))
            return false;
        if (getTimeStamp() != other.getTimeStamp())
            return false;

        if (markerList == null) {
            if (other.markerList != null)
                return false;
        } else if (!markerList.equals(other.markerList))
            return false;

        if (mdcPropertyMap == null) {
            if (other.mdcPropertyMap != null)
                return false;
        } else if (!mdcPropertyMap.equals(other.mdcPropertyMap))
            return false;
        return true;
    }

}
