package com.srbenoit.log;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogRecord;

/**
 * A formatter to create a compact and well-aligned text output for log records.
 */
public class LogFormatter extends Formatter {

    /** the format string for dates used in log entries */
    private static final String DATE_FORMAT = "MM/dd HH:mm:ss.SSS ";

    /** the date of the log record */
    private final transient Date date;

    /** line termination string (\n or \r\n) */
    private final transient String crlf;

    /** formatter for dates */
    private final transient SimpleDateFormat dateFormat;

    /**
     * Constructs a new <code>LogFormatter</code>.
     */
    public LogFormatter() {

        super();

        String linefeed;

        this.dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.getDefault());
        this.date = new Date();

        linefeed = System.getProperty("line.separator");

        if (linefeed == null) {
            linefeed = "\r\n";
        }

        this.crlf = linefeed;
    }

    /**
     * Formats the given log record and returns the formatted string. The resulting formatted string
     * will include a localized and formated version of the <code>LogRecord</code>'s message field.
     * The <code>Formatter.formatMessage</code> convenience method can (optionally) be used to
     * localize and format the message field.
     *
     * @param   record  the log record to be formatted
     * @return  the formatted log record
     */
    @Override public String format(final LogRecord record) {

        StringBuilder builder;
        int lvl;

        builder = new StringBuilder(200);
        lvl = record.getLevel().intValue();

        // Make FINE/FINER behave like System.out.println, and FINEST like
        // System.out.println.
        if (lvl > Level.FINE.intValue()) {
            this.date.setTime(record.getMillis());
            builder.append(this.dateFormat.format(this.date));
        }

        if (Level.SEVERE.intValue() == lvl) {
            builder.append("S ");
        } else if (Level.WARNING.intValue() == lvl) {
            builder.append("W ");
        } else if (Level.INFO.intValue() == lvl) {
            builder.append("I ");
        } else if (Level.CONFIG.intValue() == lvl) {
            builder.append("C ");
        } else if (lvl > Level.FINE.intValue()) {
            builder.append("  ");
        }

        builder.append(formatMessage(record));

        if (lvl > Level.FINE.intValue()) {
            addSourceInfo(record, builder);
            addExceptionInfo(record, builder);
        }

        if (lvl > Level.FINEST.intValue()) {
            builder.append(this.crlf);
        }

        return builder.toString();
    }

    /**
     * Appends the source class and method information to the log output.
     *
     * @param  record   the log record
     * @param  builder  the <code>StringBuilder</code> to which to append the throwable information
     */
    private void addSourceInfo(final LogRecord record, final StringBuilder builder) {

        if (record.getSourceClassName() != null) {
            builder.append(" (");
            builder.append(record.getSourceClassName());

            if (record.getSourceMethodName() != null) {
                builder.append(' ');
                builder.append(record.getSourceMethodName());
            }

            builder.append(')');
        }
    }

    /**
     * Appends the throwable information to the log output.
     *
     * @param  record   the log record
     * @param  builder  the <code>StringBuilder</code> to which to append the throwable information
     */
    private void addExceptionInfo(final LogRecord record, final StringBuilder builder) {

        Throwable thrown;
        StackTraceElement[] stack;

        thrown = record.getThrown();

        while (thrown != null) {
            builder.append(this.crlf);
            builder.append("                     ");
            builder.append(thrown.getClass().getSimpleName());

            if (thrown.getLocalizedMessage() != null) {
                builder.append(": ");
                builder.append(thrown.getLocalizedMessage());
            }

            stack = thrown.getStackTrace();

            for (int i = 0; i < stack.length; i++) {
                builder.append(this.crlf);
                builder.append("                       ");
                builder.append(stack[i].toString());
            }

            thrown = thrown.getCause();

            if (thrown != null) {
                builder.append(this.crlf);
                builder.append("                     CAUSED BY:");
            }
        }
    }
}
