/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.nifi.syslog.parsers;

import org.apache.nifi.syslog.events.SyslogEvent;

import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Parses a Syslog message from a ByteBuffer into a SyslogEvent instance.
 *
 * The Syslog regular expressions below were adapted from the Apache Flume project.
 */
public class SyslogParser {

    public static final String SYSLOG_MSG_RFC5424_0 =
            "(?:\\<(\\d{1,3})\\>)" + // priority
                    "(?:(\\d)?\\s?)" + // version
                    /* yyyy-MM-dd'T'HH:mm:ss.SZ or yyyy-MM-dd'T'HH:mm:ss.S+hh:mm or - (null stamp) */
                    "(?:" +
                    "(\\d{4}[-]\\d{2}[-]\\d{2}[T]\\d{2}[:]\\d{2}[:]\\d{2}" +
                    "(?:\\.\\d{1,6})?(?:[+-]\\d{2}[:]\\d{2}|Z)?)|-)" + // stamp
                    "\\s" + // separator
                    "(?:([\\w][\\w\\d\\.@\\-]*)|-)" + // host name or - (null)
                    "\\s" + // separator
                    "(.*)$"; // body

    public static final String SYSLOG_MSG_RFC3164_0 =
            "(?:\\<(\\d{1,3})\\>)" +
                    "(?:(\\d)?\\s?)" + // version
                    // stamp MMM d HH:mm:ss, single digit date has two spaces
                    "([A-Z][a-z][a-z]\\s{1,2}\\d{1,2}\\s\\d{2}[:]\\d{2}[:]\\d{2})" +
                    "\\s" + // separator
                    "([\\w][\\w\\d(\\.|\\:)@-]*)" + // host
                    "\\s(.*)$";  // body

    public static final Collection<Pattern> MESSAGE_PATTERNS;
    static {
        List<Pattern> patterns = new ArrayList<>();
        patterns.add(Pattern.compile(SYSLOG_MSG_RFC5424_0));
        patterns.add(Pattern.compile(SYSLOG_MSG_RFC3164_0));
        MESSAGE_PATTERNS = Collections.unmodifiableList(patterns);
    }

    // capture group positions from the above message patterns
    public static final int SYSLOG_PRIORITY_POS = 1;
    public static final int SYSLOG_VERSION_POS = 2;
    public static final int SYSLOG_TIMESTAMP_POS = 3;
    public static final int SYSLOG_HOSTNAME_POS = 4;
    public static final int SYSLOG_BODY_POS = 5;

    private Charset charset;

    public SyslogParser() {
        this(StandardCharsets.UTF_8);
    }

    public SyslogParser(final Charset charset) {
        this.charset = charset;
    }

    /**
     *  Parses a SyslogEvent from a byte buffer.
     *
     * @param buffer a byte buffer containing a syslog message
     * @return a SyslogEvent parsed from the byte array
     */
    public SyslogEvent parseEvent(final ByteBuffer buffer) {
        return parseEvent(buffer, null);
    }

    /**
     *  Parses a SyslogEvent from a byte buffer.
     *
     * @param buffer a byte buffer containing a syslog message
     * @param sender the hostname of the syslog server that sent the message
     * @return a SyslogEvent parsed from the byte array
     */
    public SyslogEvent parseEvent(final ByteBuffer buffer, final String sender) {
        if (buffer == null) {
            return null;
        }
        if (buffer.position() != 0) {
            buffer.flip();
        }
        byte[] bytes = new byte[buffer.limit()];
        buffer.get(bytes, 0, buffer.limit());
        return parseEvent(bytes, sender);
    }

    /**
     * Parses a SyslogEvent from a byte array.
     *
     * @param bytes a byte array containing a syslog message
     * @param sender the hostname of the syslog server that sent the message
     * @return a SyslogEvent parsed from the byte array
     */
    public SyslogEvent parseEvent(final byte[] bytes, final String sender) {
        if (bytes == null || bytes.length == 0) {
            return null;
        }

        // remove trailing new line before parsing
        int length = bytes.length;
        if (bytes[length - 1] == '\n') {
            length = length - 1;
        }

        final String message = new String(bytes, 0, length, charset);

        final SyslogEvent.Builder builder = new SyslogEvent.Builder()
                .valid(false).fullMessage(message).rawMessage(bytes).sender(sender);

        for (Pattern pattern : MESSAGE_PATTERNS) {
            final Matcher matcher = pattern.matcher(message);
            if (!matcher.matches()) {
                continue;
            }

            final MatchResult res = matcher.toMatchResult();
            for (int grp = 1; grp <= res.groupCount(); grp++) {
                String value = res.group(grp);
                if (grp == SYSLOG_TIMESTAMP_POS) {
                    builder.timestamp(value);
                } else if (grp == SYSLOG_HOSTNAME_POS) {
                    builder.hostname(value);
                } else if (grp == SYSLOG_PRIORITY_POS) {
                    int pri = Integer.parseInt(value);
                    int sev = pri % 8;
                    int facility = pri / 8;
                    builder.priority(value);
                    builder.severity(String.valueOf(sev));
                    builder.facility(String.valueOf(facility));
                } else if (grp == SYSLOG_VERSION_POS) {
                    builder.version(value);
                } else if (grp == SYSLOG_BODY_POS) {
                    builder.msgBody(value);
                }
            }

            builder.valid(true);
            break;
        }

        // either invalid w/original msg, or fully parsed event
        return builder.build();
    }

    public String getCharsetName() {
        return charset == null ? StandardCharsets.UTF_8.name() : charset.name();
    }
}
