/*
 * 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.logging.log4j.flume.appender;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPOutputStream;
import org.apache.flume.event.SimpleEvent;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LoggingException;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.impl.ThrowableProxy;
import org.apache.logging.log4j.core.time.Instant;
import org.apache.logging.log4j.core.util.Patterns;
import org.apache.logging.log4j.core.util.UuidUtil;
import org.apache.logging.log4j.message.MapMessage;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.message.StructuredDataId;
import org.apache.logging.log4j.message.StructuredDataMessage;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.apache.logging.log4j.util.Strings;

/**
 * Class that is both a Flume and Log4j Event.
 */
public class FlumeEvent extends SimpleEvent implements LogEvent {

    static final String GUID = "guId";

    private static final String DEFAULT_MDC_PREFIX = Strings.EMPTY;

    private static final String DEFAULT_EVENT_PREFIX = Strings.EMPTY;

    private static final String EVENT_TYPE = "eventType";

    private static final String EVENT_ID = "eventId";

    private static final String TIMESTAMP = "timeStamp";

    private final LogEvent event;

    private final Map<String, String> contextMap = new HashMap<>();

    private final boolean compress;

    /**
     * Construct the FlumeEvent.
     * @param event The Log4j LogEvent.
     * @param includes A comma separated list of MDC elements to include.
     * @param excludes A comma separated list of MDC elements to exclude.
     * @param required A comma separated list of MDC elements that are required to be defined.
     * @param mdcPrefix The value to prefix to MDC keys.
     * @param eventPrefix The value to prefix to event keys.
     * @param compress If true the event body should be compressed.
     */
    public FlumeEvent(
            final LogEvent event,
            final String includes,
            final String excludes,
            final String required,
            String mdcPrefix,
            String eventPrefix,
            final boolean compress) {
        this.event = event;
        this.compress = compress;
        final Map<String, String> headers = getHeaders();
        headers.put(TIMESTAMP, Long.toString(event.getTimeMillis()));
        if (mdcPrefix == null) {
            mdcPrefix = DEFAULT_MDC_PREFIX;
        }
        if (eventPrefix == null) {
            eventPrefix = DEFAULT_EVENT_PREFIX;
        }
        final Map<String, String> mdc = event.getContextData().toMap();
        if (includes != null) {
            final String[] array = includes.split(Patterns.COMMA_SEPARATOR);
            if (array.length > 0) {
                for (String str : array) {
                    str = str.trim();
                    if (mdc.containsKey(str)) {
                        contextMap.put(str, mdc.get(str));
                    }
                }
            }
        } else if (excludes != null) {
            final String[] array = excludes.split(Patterns.COMMA_SEPARATOR);
            if (array.length > 0) {
                final List<String> list = new ArrayList<>(array.length);
                for (final String value : array) {
                    list.add(value.trim());
                }
                for (final Map.Entry<String, String> entry : mdc.entrySet()) {
                    if (!list.contains(entry.getKey())) {
                        contextMap.put(entry.getKey(), entry.getValue());
                    }
                }
            }
        } else {
            contextMap.putAll(mdc);
        }

        if (required != null) {
            final String[] array = required.split(Patterns.COMMA_SEPARATOR);
            if (array.length > 0) {
                for (String str : array) {
                    str = str.trim();
                    if (!mdc.containsKey(str)) {
                        throw new LoggingException("Required key " + str + " is missing from the MDC");
                    }
                }
            }
        }
        final String guid = UuidUtil.getTimeBasedUuid().toString();
        final Message message = event.getMessage();
        if (message instanceof MapMessage) {
            // Add the guid to the Map so that it can be included in the Layout.
            @SuppressWarnings("unchecked")
            final MapMessage<?, String> stringMapMessage = (MapMessage<?, String>) message;
            stringMapMessage.put(GUID, guid);
            if (message instanceof StructuredDataMessage) {
                addStructuredData(eventPrefix, headers, (StructuredDataMessage) message);
            }
            addMapData(eventPrefix, headers, stringMapMessage);
        } else {
            headers.put(GUID, guid);
        }

        addContextData(mdcPrefix, headers, contextMap);
    }

    protected void addStructuredData(
            final String prefix, final Map<String, String> fields, final StructuredDataMessage msg) {
        fields.put(prefix + EVENT_TYPE, msg.getType());
        final StructuredDataId id = msg.getId();
        fields.put(prefix + EVENT_ID, id.getName());
    }

    protected void addMapData(final String prefix, final Map<String, String> fields, final MapMessage<?, String> msg) {
        final Map<String, String> data = msg.getData();
        for (final Map.Entry<String, String> entry : data.entrySet()) {
            fields.put(prefix + entry.getKey(), entry.getValue());
        }
    }

    protected void addContextData(
            final String prefix, final Map<String, String> fields, final Map<String, String> context) {
        final Map<String, String> map = new HashMap<>();
        for (final Map.Entry<String, String> entry : context.entrySet()) {
            if (entry.getKey() != null && entry.getValue() != null) {
                fields.put(prefix + entry.getKey(), entry.getValue());
                map.put(prefix + entry.getKey(), entry.getValue());
            }
        }
        context.clear();
        context.putAll(map);
    }

    @Override
    public LogEvent toImmutable() {
        return toMemento();
    }

    /**
     * Set the body in the event.
     * @param body The body to add to the event.
     */
    @Override
    public void setBody(final byte[] body) {
        if (body == null || body.length == 0) {
            super.setBody(new byte[0]);
            return;
        }
        if (compress) {
            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
            try (final GZIPOutputStream os = new GZIPOutputStream(baos)) {
                os.write(body);
            } catch (final IOException ioe) {
                throw new LoggingException("Unable to compress message", ioe);
            }
            super.setBody(baos.toByteArray());
        } else {
            super.setBody(body);
        }
    }

    /**
     * Get the Frequently Qualified Class Name.
     * @return the FQCN String.
     */
    @Override
    public String getLoggerFqcn() {
        return event.getLoggerFqcn();
    }

    /**
     * Returns the logging Level.
     * @return the Level.
     */
    @Override
    public Level getLevel() {
        return event.getLevel();
    }

    /**
     * Returns the logger name.
     * @return the logger name.
     */
    @Override
    public String getLoggerName() {
        return event.getLoggerName();
    }

    /**
     * Returns the StackTraceElement for the caller of the logging API.
     * @return the StackTraceElement of the caller.
     */
    @Override
    public StackTraceElement getSource() {
        return event.getSource();
    }

    /**
     * Returns the Message.
     * @return the Message.
     */
    @Override
    public Message getMessage() {
        return event.getMessage();
    }

    /**
     * Returns the Marker.
     * @return the Marker.
     */
    @Override
    public Marker getMarker() {
        return event.getMarker();
    }

    /**
     * Returns the ID of the Thread.
     * @return the ID of the Thread.
     */
    @Override
    public long getThreadId() {
        return event.getThreadId();
    }

    /**
     * Returns the priority of the Thread.
     * @return the priority of the Thread.
     */
    @Override
    public int getThreadPriority() {
        return event.getThreadPriority();
    }

    /**
     * Returns the name of the Thread.
     * @return the name of the Thread.
     */
    @Override
    public String getThreadName() {
        return event.getThreadName();
    }

    /**
     * Returns the event timestamp.
     * @return the event timestamp.
     */
    @Override
    public long getTimeMillis() {
        return event.getTimeMillis();
    }

    /**
     * {@inheritDoc}
     * @since 2.11.0
     */
    @Override
    public Instant getInstant() {
        return event.getInstant();
    }

    /**
     * Returns the value of the running Java Virtual Machine's high-resolution time source when this event was created,
     * or a dummy value if it is known that this value will not be used downstream.
     * @return the event nanosecond timestamp.
     */
    @Override
    public long getNanoTime() {
        return event.getNanoTime();
    }

    /**
     * Returns the Throwable associated with the event, if any.
     * @return the Throwable.
     */
    @Override
    public Throwable getThrown() {
        return event.getThrown();
    }

    /**
     * Returns the Throwable associated with the event, if any.
     * @return the Throwable.
     */
    @Override
    public ThrowableProxy getThrownProxy() {
        return event.getThrownProxy();
    }

    /**
     * Returns the context data of the {@code LogEvent} that this {@code FlumeEvent} was constructed with.
     * @return the context data of the {@code LogEvent} that this {@code FlumeEvent} was constructed with.
     */
    @Override
    public ReadOnlyStringMap getContextData() {
        return event.getContextData();
    }

    /**
     * Returns a copy of the context stack.
     * @return a copy of the context stack.
     */
    @Override
    public ThreadContext.ContextStack getContextStack() {
        return event.getContextStack();
    }

    @Override
    public boolean isIncludeLocation() {
        return event.isIncludeLocation();
    }

    @Override
    public void setIncludeLocation(final boolean includeLocation) {
        event.setIncludeLocation(includeLocation);
    }

    @Override
    public boolean isEndOfBatch() {
        return event.isEndOfBatch();
    }

    @Override
    public void setEndOfBatch(final boolean endOfBatch) {
        event.setEndOfBatch(endOfBatch);
    }
}
