/*
 * Copyright 2014-2019 the original author or authors.
 *
 * 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
 *
 *      https://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.springframework.integration.syslog;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.springframework.util.Assert;

/**
 * Parse for RFC 5424 syslog messages; when used with TCP, requires the use
 * of a {@code RFC6587SyslogDeserializer} which decodes the framing.
 *
 * @author Duncan McIntyre
 * @author Gary Russell
 * @since 1.4.1
 *
 */
public class RFC5424SyslogParser {

	protected static final char NILVALUE = '-';

	protected static final char SPACE = ' ';

	protected final boolean retainOriginal; // NOSONAR final


	/**
	 * Construct a default parser; do not retain the original message content unless there
	 * is an error.
	 */
	public RFC5424SyslogParser() {
		this(false);
	}

	/**
	 * @param retainOriginal when true, include the original message content intact in the
	 * map.
	 */
	public RFC5424SyslogParser(boolean retainOriginal) {
		this.retainOriginal = retainOriginal;
	}

	public Map<String, ?> parse(String lineArg, int octetCount, boolean shortRead) { // NOSONAR NCSS line count

		Map<String, Object> map = new LinkedHashMap<String, Object>();

		String line = lineArg;
		Reader r = new Reader(line);

		try {
			if (shortRead) {
				int n = line.length() - 1;
				while (n >= 0 && line.charAt(n) == 0x00) {
					n--;
				}
				line = line.substring(0, n);
				throw new IllegalStateException("Insufficient data; expected " + octetCount + " got " + (n + 1));
			}
			r.expect('<');
			int pri = r.readInt();
			r.expect('>');

			int version = r.readInt();
			r.expect(SPACE);

			Object timestamp = getTimestamp(r);

			String host = r.getIdentifier();
			String app = r.getIdentifier();
			String procId = r.getIdentifier();
			String msgId = r.getIdentifier();

			Object structuredData = getStructuredData(r);

			String message;
			if (r.is(SPACE)) {
				r.getc();
				message = r.rest();
			}
			else {
				message = "";
			}

			int severity = pri & 0x7;
			int facility = pri >> 3;
			map.put(SyslogHeaders.FACILITY, facility);

			map.put(SyslogHeaders.SEVERITY, severity);
			map.put(SyslogHeaders.SEVERITY_TEXT, Severity.parseInt(severity).label());

			if (timestamp != null) {
				map.put(SyslogHeaders.TIMESTAMP, timestamp);
			}

			if (host != null) {
				map.put(SyslogHeaders.HOST, host);
			}
			if (app != null) {
				map.put(SyslogHeaders.APP_NAME, app);
			}
			if (procId != null) {
				map.put(SyslogHeaders.PROCID, procId);
			}
			if (msgId != null) {
				map.put(SyslogHeaders.MSGID, msgId);
			}
			map.put(SyslogHeaders.VERSION, version);

			if (structuredData != null) {
				map.put(SyslogHeaders.STRUCTURED_DATA, structuredData);
			}

			map.put(SyslogHeaders.MESSAGE, message);
			map.put(SyslogHeaders.DECODE_ERRORS, "false");

			if (this.retainOriginal) {
				map.put(SyslogHeaders.UNDECODED, line);
			}
		}
		catch (IllegalStateException e) {
			map.put(SyslogHeaders.DECODE_ERRORS, "true");
			map.put(SyslogHeaders.ERRORS, e.getMessage());
			map.put(SyslogHeaders.UNDECODED, line);
		}
		catch (StringIndexOutOfBoundsException sob) {
			map.put(SyslogHeaders.DECODE_ERRORS, "true");
			map.put(SyslogHeaders.ERRORS, "Unexpected end of message: " + sob.getMessage());
			map.put(SyslogHeaders.UNDECODED, line);
		}
		return map;
	}

	/**
	 * Default implementation returns the date as a String (if present).
	 * @param r the reader.
	 * @return the timestamp.
	 */
	protected Object getTimestamp(Reader r) {

		int c = r.getc();

		if (c == NILVALUE) {
			return null;
		}

		if (!Character.isDigit(c)) {
			throw new IllegalStateException("Year expected @" + r.getIndex());
		}

		StringBuilder dateBuilder = new StringBuilder();
		dateBuilder.append((char) c);
		while ((c = r.getc()) != SPACE) {
			dateBuilder.append((char) c);
		}

		return dateBuilder.toString();
	}

	private Object getStructuredData(Reader r) {
		if (r.is(NILVALUE)) {
			r.getc();
			return null;
		}
		return parseStructuredDataElements(r);
	}

	/**
	 * Default implementation returns a list of structured data elements with
	 * no internal parsing.
	 * @param r the reader.
	 * @return the structured data.
	 */
	protected Object parseStructuredDataElements(Reader r) {
		List<String> fragments = new ArrayList<String>();
		while (r.is('[')) {
			r.mark();
			r.skipTo(']');
			fragments.add(r.getMarkedSegment());
		}
		return fragments;
	}

	protected static class Reader {

		private final String line;

		public int idx;

		private int mark;

		public Reader(String l) {
			this.line = l;
		}

		public int getIndex() {
			return this.idx;
		}

		public void mark() {
			this.mark = this.idx;
		}

		public String getMarkedSegment() {
			Assert.state(this.mark <= this.idx, "mark is greater than this.idx");
			return this.line.substring(this.mark, this.idx);
		}

		public int current() {
			return this.line.charAt(this.idx);
		}

		public int prev() {
			return this.line.charAt(this.idx - 1);
		}

		public int getc() {
			return this.line.charAt(this.idx++);
		}

		public int peek() {
			return this.line.charAt(this.idx + 1);
		}

		public void ungetc() {
			this.idx--;
		}

		public int getInt() {
			int c = getc();
			if (!Character.isDigit(c)) {
				ungetc();
				return -1;
			}

			return c - '0';
		}

		/**
		 * Read characters building an int until a non-digit is found
		 * @return int
		 */
		public int readInt() {
			int val = 0;
			while (isDigit()) {
				val = (val * 10) + getInt();
			}
			return val;
		}

		public double readFraction() {
			int val = 0;
			int order = 0;
			while (isDigit()) {
				val = (val * 10) + getInt();
				order *= 10;
			}
			return (double) val / order;

		}

		public boolean is(char c) {
			return this.line.charAt(this.idx) == c;
		}

		public boolean was(char c) {
			return this.line.charAt(this.idx - 1) == c;
		}

		public boolean isDigit() {
			return Character.isDigit(this.line.charAt(this.idx));
		}

		public void expect(char c) {
			if (this.line.charAt(this.idx++) != c) {
				throw new IllegalStateException("Expected '" + c + "' @" + this.idx);
			}
		}

		public void skipTo(char searchChar) {
			while (!is(searchChar) || was('\\')) {
				getc();
			}
			getc();
		}

		public String rest() {
			return this.line.substring(this.idx);
		}

		public String getIdentifier() {
			StringBuilder sb = new StringBuilder();
			int c;
			while (true) {
				c = getc();
				if (c >= 33 && c <= 127) {
					sb.append((char) c);
				}
				else {
					break;
				}
			}
			return sb.toString();
		}

	}

	protected enum Severity {

		DEBUG(7, "DEBUG"),

		INFO(6, "INFO"),

		NOTICE(5, "NOTICE"),

		WARN(4, "WARN"),

		ERROR(3, "ERRORS"),

		CRITICAL(2, "CRITICAL"),

		ALERT(1, "ALERT"),

		EMERGENCY(0, "EMERGENCY"),

		UNDEFINED(-1, "UNDEFINED");

		private final int level;

		private final String label;

		Severity(int level, String label) {
			this.level = level;
			this.label = label;
		}

		public int level() {
			return this.level;
		}

		public String label() {
			return this.label;
		}

		public static Severity parseInt(int syslogSeverity) {
			if (syslogSeverity == 7) {
				return DEBUG;
			}
			if (syslogSeverity == 6) {
				return INFO;
			}
			if (syslogSeverity == 5) {
				return NOTICE;
			}
			if (syslogSeverity == 4) {
				return WARN;
			}
			if (syslogSeverity == 3) {
				return ERROR;
			}
			if (syslogSeverity == 2) {
				return CRITICAL;
			}
			if (syslogSeverity == 1) {
				return ALERT;
			}
			if (syslogSeverity == 0) {
				return EMERGENCY;
			}
			return UNDEFINED;
		}

	}


}
