package org.xbib.logging;

import java.text.MessageFormat;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.logging.LogRecord;
import org.xbib.logging.os.HostName;
import org.xbib.logging.os.Process;
import org.xbib.logging.util.FastCopyHashMap;
import org.xbib.logging.util.StackWalkerUtil;

/**
 * An extended log record, which includes additional information including MDC/NDC and correct
 * caller location (even in the presence of a logging facade).
 */
@SuppressWarnings("serial")
public class ExtLogRecord extends LogRecord {

    /**
     * The format style to use.
     */
    public enum FormatStyle {

        /**
         * Format the message using the {@link MessageFormat} parameter style.
         */
        MESSAGE_FORMAT,
        /**
         * Format the message using the {@link java.util.Formatter} (also known as {@code printf()}) parameter style.
         */
        PRINTF,
        /**
         * Do not format the message; parameters are ignored.
         */
        NO_FORMAT,
    }

    /**
     * Construct a new instance. Grabs the current NDC immediately. MDC is deferred.
     *
     * @param level           a logging level value
     * @param msg             the raw non-localized logging message (may be null)
     * @param loggerClassName the name of the logger class
     */
    public ExtLogRecord(final java.util.logging.Level level, final String msg, final String loggerClassName) {
        this(level, msg, FormatStyle.MESSAGE_FORMAT, loggerClassName);
    }

    /**
     * Construct a new instance. Grabs the current NDC immediately. MDC is deferred.
     *
     * @param level           a logging level value
     * @param msg             the raw non-localized logging message (may be null)
     * @param formatStyle     the parameter format style to use
     * @param loggerClassName the name of the logger class
     */
    public ExtLogRecord(final java.util.logging.Level level, final String msg, final FormatStyle formatStyle,
                        final String loggerClassName) {
        super(level, msg);
        this.formatStyle = formatStyle == null ? FormatStyle.MESSAGE_FORMAT : formatStyle;
        this.loggerClassName = loggerClassName;
        ndc = NDC.get();
        threadName = Thread.currentThread().getName();
        longThreadID = Thread.currentThread().threadId();
        hostName = HostName.getQualifiedHostName();
        processName = Process.getProcessName();
        processId = ProcessHandle.current().pid();
    }

    /**
     * Make a copy of a log record.
     *
     * @param original the original
     */
    public ExtLogRecord(final ExtLogRecord original) {
        super(original.getLevel(), original.getMessage());
        // LogRecord fields
        setLoggerName(original.getLoggerName());
        setInstant(original.getInstant());
        setParameters(original.getParameters());
        setResourceBundle(original.getResourceBundle());
        setResourceBundleName(original.getResourceBundleName());
        setSequenceNumber(original.getSequenceNumber());
        setThrown(original.getThrown());
        if (!original.calculateCaller) {
            setSourceClassName(original.getSourceClassName());
            setSourceMethodName(original.getSourceMethodName());
            sourceFileName = original.sourceFileName;
            sourceLineNumber = original.sourceLineNumber;
            sourceModuleName = original.sourceModuleName;
            sourceModuleVersion = original.sourceModuleVersion;
        }
        setLongThreadID(original.getLongThreadID());
        formatStyle = original.formatStyle;
        marker = original.marker;
        mdcCopy = original.mdcCopy;
        ndc = original.ndc;
        loggerClassName = original.loggerClassName;
        threadName = original.threadName;
        hostName = original.hostName;
        processName = original.processName;
        processId = original.processId;
    }

    /**
     * Wrap a JDK log record. If the target record is already an {@code ExtLogRecord}, it is simply returned. Otherwise
     * a wrapper record is created and returned.
     *
     * @param rec the original record
     * @return the wrapped record
     */
    public static ExtLogRecord wrap(LogRecord rec) {
        if (rec == null) {
            return null;
        } else if (rec instanceof ExtLogRecord) {
            return (ExtLogRecord) rec;
        } else {
            return new WrappedExtLogRecord(rec);
        }
    }

    private final transient String loggerClassName;
    private transient boolean calculateCaller = true;

    private String ndc;
    private FormatStyle formatStyle;
    private FastCopyHashMap<String, Object> mdcCopy;
    private int sourceLineNumber = -1;
    private String sourceFileName;
    private String threadName;
    private String hostName;
    private String processName;
    private long processId = -1;
    private String sourceModuleName;
    private String sourceModuleVersion;
    private Object marker;
    private long longThreadID;

    /**
     * Disable caller calculation for this record. If the caller has already been calculated, leave it; otherwise
     * set the caller to {@code "unknown"}.
     */
    public void disableCallerCalculation() {
        if (calculateCaller) {
            setUnknownCaller();
        }
    }

    /**
     * Copy all fields and prepare this object to be passed to another thread. Calling this method
     * more than once has no additional effect and will not incur extra copies.
     */
    public void copyAll() {
        copyMdc();
        calculateCaller();
    }

    /**
     * Copy the MDC. Call this method before passing this log record to another thread. Calling this method
     * more than once has no additional effect and will not incur extra copies.
     */
    public void copyMdc() {
        if (mdcCopy == null) {
            mdcCopy = FastCopyHashMap.of(MDC.getMDCProvider().copyObject());
        }
    }

    /**
     * Get the value of an MDC property.
     *
     * @param key the property key
     * @return the property value
     */
    public String getMdc(String key) {
        final Map<String, Object> mdcCopy = this.mdcCopy;
        if (mdcCopy == null) {
            return MDC.get(key);
        }
        final Object value = mdcCopy.get(key);
        return value == null ? null : value.toString();
    }

    /**
     * Get a copy of all the MDC properties for this log record. If the MDC has not yet been copied, this method will copy it.
     *
     * @return a copy of the MDC map
     */
    public Map<String, String> getMdcCopy() {
        copyMdc();
        // Create a new map with string values
        final FastCopyHashMap<String, String> newMdc = new FastCopyHashMap<String, String>();
        for (Map.Entry<String, Object> entry : mdcCopy.entrySet()) {
            final String key = entry.getKey();
            final Object value = entry.getValue();
            newMdc.put(key, (value == null ? null : value.toString()));
        }
        return newMdc;
    }

    /**
     * Change an MDC value on this record. If the MDC has not yet been copied, this method will copy it.
     *
     * @param key   the key to set
     * @param value the value to set it to
     * @return the old value, if any
     */
    public String putMdc(String key, String value) {
        copyMdc();
        final Object oldValue = mdcCopy.put(key, value);
        return oldValue == null ? null : oldValue.toString();
    }

    /**
     * Remove an MDC value on this record. If the MDC has not yet been copied, this method will copy it.
     *
     * @param key the key to remove
     * @return the old value, if any
     */
    public String removeMdc(String key) {
        copyMdc();
        final Object oldValue = mdcCopy.remove(key);
        return oldValue == null ? null : oldValue.toString();
    }

    /**
     * Create a new MDC using a copy of the source map.
     *
     * @param sourceMap the source man, must not be {@code null}
     */
    public void setMdc(Map<?, ?> sourceMap) {
        final FastCopyHashMap<String, Object> newMdc = new FastCopyHashMap<String, Object>();
        for (Map.Entry<?, ?> entry : sourceMap.entrySet()) {
            final Object key = entry.getKey();
            final Object value = entry.getValue();
            if (key != null && value != null) {
                newMdc.put(key.toString(), value);
            }
        }
        mdcCopy = newMdc;
    }

    /**
     * Get the NDC for this log record.
     *
     * @return the NDC
     */
    public String getNdc() {
        return ndc;
    }

    /**
     * Change the NDC for this log record.
     *
     * @param value the new NDC value
     */
    public void setNdc(String value) {
        ndc = value;
    }

    /**
     * Get the class name of the logger which created this record.
     *
     * @return the class name
     */
    public String getLoggerClassName() {
        return loggerClassName;
    }

    /**
     * Get the format style for the record.
     *
     * @return the format style
     */
    public FormatStyle getFormatStyle() {
        return formatStyle;
    }

    /**
     * Find the first stack frame below the call to the logger, and populate the log record with that information.
     */
    private void calculateCaller() {
        if (!calculateCaller) {
            return;
        }
        calculateCaller = false;
        StackWalkerUtil.calculateCaller(this);
    }

    public void setUnknownCaller() {
        setSourceClassName(null);
        setSourceMethodName(null);
        setSourceLineNumber(-1);
        setSourceFileName(null);
        setSourceModuleName(null);
        setSourceModuleVersion(null);
    }

    /**
     * Get the source line number for this log record.
     * <p/>
     * Note that this line number is not verified and may be spoofed. This information may either have been
     * provided as part of the logging call, or it may have been inferred automatically by the logging framework. In the
     * latter case, the information may only be approximate and may in fact describe an earlier call on the stack frame.
     * May be -1 if no information could be obtained.
     *
     * @return the source line number
     */
    public int getSourceLineNumber() {
        calculateCaller();
        return sourceLineNumber;
    }

    /**
     * Set the source line number for this log record.
     *
     * @param sourceLineNumber the source line number
     */
    public void setSourceLineNumber(final int sourceLineNumber) {
        calculateCaller = false;
        this.sourceLineNumber = sourceLineNumber;
    }

    /**
     * Get the source file name for this log record.
     * <p/>
     * Note that this file name is not verified and may be spoofed. This information may either have been
     * provided as part of the logging call, or it may have been inferred automatically by the logging framework. In the
     * latter case, the information may only be approximate and may in fact describe an earlier call on the stack frame.
     * May be {@code null} if no information could be obtained.
     *
     * @return the source file name
     */
    public String getSourceFileName() {
        calculateCaller();
        return sourceFileName;
    }

    /**
     * Set the source file name for this log record.
     *
     * @param sourceFileName the source file name
     */
    public void setSourceFileName(final String sourceFileName) {
        calculateCaller = false;
        this.sourceFileName = sourceFileName;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getSourceClassName() {
        calculateCaller();
        return super.getSourceClassName();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setSourceClassName(final String sourceClassName) {
        calculateCaller = false;
        super.setSourceClassName(sourceClassName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getSourceMethodName() {
        calculateCaller();
        return super.getSourceMethodName();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setSourceMethodName(final String sourceMethodName) {
        calculateCaller = false;
        super.setSourceMethodName(sourceMethodName);
    }

    /**
     * Get the name of the module that initiated the logging request, if known.
     *
     * @return the name of the module that initiated the logging request
     */
    public String getSourceModuleName() {
        calculateCaller();
        return sourceModuleName;
    }

    /**
     * Set the source module name of this record.
     *
     * @param sourceModuleName the source module name
     */
    public void setSourceModuleName(final String sourceModuleName) {
        calculateCaller = false;
        this.sourceModuleName = sourceModuleName;
    }

    /**
     * Get the version of the module that initiated the logging request, if known.
     *
     * @return the version of the module that initiated the logging request
     */
    public String getSourceModuleVersion() {
        calculateCaller();
        return sourceModuleVersion;
    }

    /**
     * Set the source module version of this record.
     *
     * @param sourceModuleVersion the source module version
     */
    public void setSourceModuleVersion(final String sourceModuleVersion) {
        calculateCaller = false;
        this.sourceModuleVersion = sourceModuleVersion;
    }

    /**
     * Get the fully formatted log record, with resources resolved and parameters applied.
     *
     * @return the formatted log record
     * @deprecated The formatter should normally be used to format the message contents.
     */
    @Deprecated
    public String getFormattedMessage() {
        final ResourceBundle bundle = getResourceBundle();
        String msg = getMessage();
        if (msg == null) {
            return null;
        }
        if (bundle != null) {
            try {
                msg = bundle.getString(msg);
            } catch (MissingResourceException ex) {
                // ignore
            }
        }
        final Object[] parameters = getParameters();
        if (parameters == null || parameters.length == 0) {
            return msg;
        }
        return switch (formatStyle) {
            case PRINTF -> String.format(msg, parameters);
            case MESSAGE_FORMAT -> msg.indexOf('{') >= 0 ? MessageFormat.format(msg, parameters) : msg;
            default -> msg;
        };
    }

    /**
     * Get the resource key, if any. If the log message is not localized, then the key is {@code null}.
     *
     * @return the resource key
     */
    public String getResourceKey() {
        final String msg = getMessage();
        if (msg == null)
            return null;
        if (getResourceBundleName() == null && getResourceBundle() == null)
            return null;
        return msg;
    }

    /**
     * Get the thread name of this logging event.
     *
     * @return the thread name
     */
    public String getThreadName() {
        return threadName;
    }

    /**
     * Set the thread name of this logging event.
     *
     * @param threadName the thread name
     */
    public void setThreadName(final String threadName) {
        this.threadName = threadName;
    }

    /**
     * Get the host name of the record, if known.
     *
     * @return the host name of the record, if known
     */
    public String getHostName() {
        return hostName;
    }

    /**
     * Set the host name of the record.
     *
     * @param hostName the host name of the record
     */
    public void setHostName(final String hostName) {
        this.hostName = hostName;
    }

    /**
     * Get the process name of the record, if known.
     *
     * @return the process name of the record, if known
     */
    public String getProcessName() {
        return processName;
    }

    /**
     * Set the process name of the record.
     *
     * @param processName the process name of the record
     */
    public void setProcessName(final String processName) {
        this.processName = processName;
    }

    /**
     * Get the process ID of the record, if known.
     *
     * @return the process ID of the record, or -1 if not known
     */
    public long getProcessId() {
        return processId;
    }

    /**
     * Set the process ID of the record.
     *
     * @param processId the process ID of the record
     */
    public void setProcessId(final long processId) {
        this.processId = processId;
    }

    /**
     * Set the raw message. Any cached formatted message is discarded. The parameter format is set to be
     * {@link MessageFormat}-style.
     *
     * @param message the new raw message
     */
    @Override
    public void setMessage(final String message) {
        setMessage(message, FormatStyle.MESSAGE_FORMAT);
    }

    /**
     * Set the raw message. Any cached formatted message is discarded. The parameter format is set according to the
     * given argument.
     *
     * @param message     the new raw message
     * @param formatStyle the format style to use
     */
    public void setMessage(final String message, final FormatStyle formatStyle) {
        this.formatStyle = formatStyle == null ? FormatStyle.MESSAGE_FORMAT : formatStyle;
        super.setMessage(message);
    }

    /**
     * Set the parameters to the log message. Any cached formatted message is discarded.
     *
     * @param parameters the log message parameters. (may be null)
     */
    @Override
    public void setParameters(final Object[] parameters) {
        super.setParameters(parameters);
    }

    /**
     * Set the localization resource bundle. Any cached formatted message is discarded.
     *
     * @param bundle localization bundle (may be null)
     */
    @Override
    public void setResourceBundle(final ResourceBundle bundle) {
        super.setResourceBundle(bundle);
    }

    /**
     * Set the localization resource bundle name. Any cached formatted message is discarded.
     *
     * @param name localization bundle name (may be null)
     */
    @Override
    public void setResourceBundleName(final String name) {
        super.setResourceBundleName(name);
    }

    /**
     * Set the marker for this event. Markers are used mostly by SLF4J and Log4j.
     */
    public void setMarker(Object marker) {
        this.marker = marker;
    }

    public Object getMarker() {
        return marker;
    }

    @Deprecated
    @SuppressWarnings("deprecation")
    @Override
    public void setThreadID(final int threadID) {
        super.setThreadID(threadID);
        this.longThreadID = threadID;
    }

    @Override
    public long getLongThreadID() {
        return longThreadID;
    }

    @Override
    public ExtLogRecord setLongThreadID(final long id) {
        this.longThreadID = id;
        return this;
    }
}
