/*
 * This file is part of the logback-journal project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.gnieh.logback;

import java.io.ByteArrayOutputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.StackTraceElementProxy;
import ch.qos.logback.core.AppenderBase;
import ch.qos.logback.core.encoder.Encoder;

/**
 * An appender that send the events to systemd journal
 *
 * @author Lucas Satabin
 *
 */
public class SystemdJournalAppender extends AppenderBase<ILoggingEvent> {

	boolean logLocation = true;

	boolean logSourceLocation = false;

	boolean logException = true;

	boolean logStackTrace = false;

	boolean logThreadName = true;

	boolean logLoggerName = false;

	boolean logMdc = false;

	String mdcKeyPrefix = "";

	String syslogIdentifier = "";

	Encoder<ILoggingEvent> encoder = null;

	@Override
	protected void append(ILoggingEvent event) {
		try {
			// get the message id if any
			Map<String, String> mdc = event.getMDCPropertyMap();

			List<Object> messages = new ArrayList<>();

			// the formatted human readable message
			if (encoder == null)
				messages.add(event.getFormattedMessage());
			else {
				String message = new String(encoder.encode(event));
				messages.add(message);
			}

			// the log level
			messages.add("PRIORITY=%i");
			messages.add(levelToInt(event.getLevel()));

			if (hasException(event)) {
				StackTraceElementProxy[] stack = event.getThrowableProxy().getStackTraceElementProxyArray();
				if (stack.length > 0) {

					// the location information if any is available and it is
					// enabled
					if (logLocation) {
						StackTraceElement elt = stack[0].getStackTraceElement();
						appendLocation(messages, elt);
					}

					// if one wants to log the exception name and message, just
					// do it
					if (logException) {
						messages.add("EXN_NAME=%s");
						messages.add(event.getThrowableProxy().getClassName());
						messages.add("EXN_MESSAGE=%s");
						messages.add(event.getThrowableProxy().getMessage());
					}

					// if one wants to log the exception stack trace, just do it
					if (logStackTrace) {
						messages.add("EXN_STACKTRACE=%s");
						StringWriter stacktrace = new StringWriter();
						for(StackTraceElementProxy st : stack) {
							stacktrace.write(st.getSTEAsString());
							stacktrace.write('\n');
						}
						messages.add(stacktrace.toString());
					}
				}
			}

			// log thread name if enabled
			if (logThreadName) {
				messages.add("THREAD_NAME=%s");
				messages.add(event.getThreadName());
			}

			// add a message id field if any is defined for this logging event
			if (mdc.containsKey(SystemdJournal.MESSAGE_ID)) {
				messages.add("MESSAGE_ID=%s");
				messages.add(mdc.get(SystemdJournal.MESSAGE_ID));
			}

			// override the syslog identifier string if set
			if (!syslogIdentifier.isEmpty()) {
				messages.add("SYSLOG_IDENTIFIER=%s");
				messages.add(syslogIdentifier);
			}

			if (logLoggerName) {
				messages.add("LOGGER_NAME=%s");
				messages.add(event.getLoggerName());
			}

			if (logMdc) {
				String normalizedKeyPrefix = normalizeKey(mdcKeyPrefix);
				for (Map.Entry<String, String> entry : mdc.entrySet()) {
					String key = entry.getKey();
					if (key != null && !key.equals(SystemdJournal.MESSAGE_ID)) {
						messages.add(normalizedKeyPrefix + normalizeKey(key) + "=%s");
						messages.add(entry.getValue());
					}
				}
			}

			if (logSourceLocation && !hasException(event)) {
				StackTraceElement[] callerData = event.getCallerData();
				if (callerData != null && callerData.length >= 1) {
					appendLocation(messages, callerData[0]);
				}
			}

			// the vararg list is null terminated
			messages.add(null);

			SystemdJournalLibrary journald = SystemdJournalLibrary.INSTANCE;

			journald.sd_journal_send("MESSAGE=%s", messages.toArray());
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private boolean hasException(ILoggingEvent event) {
		return event.getThrowableProxy() != null;
	}

	private void appendLocation(List<Object> messages, StackTraceElement stackTraceElement) {
		messages.add("CODE_FILE=%s");
		messages.add(stackTraceElement.getFileName());
		messages.add("CODE_LINE=%i");
		messages.add(stackTraceElement.getLineNumber());
		messages.add("CODE_FUNC=%s.%s");
		messages.add(stackTraceElement.getClassName());
		messages.add(stackTraceElement.getMethodName());
	}

	private int levelToInt(Level l) {
		switch (l.toInt()) {
		case Level.TRACE_INT:
		case Level.DEBUG_INT:
			return 7;
		case Level.INFO_INT:
			return 6;
		case Level.WARN_INT:
			return 4;
		case Level.ERROR_INT:
			return 3;
		default:
			throw new IllegalArgumentException("Unknown level value: " + l);
		}
	}

	private static String normalizeKey(String key) {
		return key.toUpperCase().replaceAll("[^_A-Z0-9]", "_");
	}

	public boolean isLogLocation() {
		return logLocation;
	}

	public void setLogLocation(boolean logLocation) {
		this.logLocation = logLocation;
	}

	public boolean isLogThreadName() {
		return logThreadName;
	}

	public void setLogThreadName(boolean logThreadName) {
		this.logThreadName = logThreadName;
	}

	public boolean isLogException() {
		return logException;
	}

	public void setLogException(boolean logException) {
		this.logException = logException;
	}

	public boolean isLogStackTrace() {
		return logStackTrace;
	}

	public void setLogStackTrace(boolean logStackTrace) {
		this.logStackTrace = logStackTrace;
	}

	public String getSyslogIdentifier() {
		return syslogIdentifier;
	}

	public void setSyslogIdentifier(String syslogIdentifier) {
		this.syslogIdentifier = syslogIdentifier;
	}

	public Encoder<ILoggingEvent> getEncoder() {
		return encoder;
	}

	public void setEncoder(Encoder<ILoggingEvent> encoder) {
		this.encoder = encoder;
	}

	public void setLogMdc(boolean logMdc) {
		this.logMdc = logMdc;
	}

	public boolean isLogMdc() {
		return logMdc;
	}

	public void setMdcKeyPrefix(String mdcKeyPrefix) {
		this.mdcKeyPrefix = mdcKeyPrefix;
	}

	public String getMdcKeyPrefix() {
		return mdcKeyPrefix;
	}

	public void setLogLoggerName(boolean logLoggerName) {
		this.logLoggerName = logLoggerName;
	}

	public boolean isLogLoggerName() {
		return logLoggerName;
	}

	public void setLogSourceLocation(boolean logSourceLocation) {
		this.logSourceLocation = logSourceLocation;
	}

	public boolean isLogSourceLocation() {
		return logSourceLocation;
	}
}