/*
 * NIST HL7 Web Service
 * Logger.java Oct 23, 2007
 *
 * This code was produced by the National Institute of Standards and
 * Technology (NIST). See the "nist.disclaimer" file given in the distribution
 * for information on the use and redistribution of this software.
 */

package gov.nist.hl7.ws;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * This class provide a very simple static Logger. It can output to any
 * OutputStream. It provides the following output levels:
 * <pre>
 *   FATAL, ERROR, WARNING, INFO, DEBUG, VERBOSE
 * </pre>
 *
 * <h2>Output</h2>
 * <p>
 * It uses two different output streams. FATAL, ERROR and WARNING go to 'Err'
 * whereas INFO, DEBUG and VERBOSE go to 'Out'. Err and Out default to
 * System.err and System.out. If you want to change that, you can just call:
 * </p>
 * <pre>
 *  Logger.setErr(OutputStream err);
 *  Logger.setOut(OutputStream out);
 * </pre>
 *
 * <h2>Log level and Trace</h2>
 * <p>
 * There are two properties that you can set on the command line to modify the
 * output:
 * </p>
 * <pre>
 *  -Dlog.level=[0..5] : up to which level do you want to output (default: INFO=3)
 *  -Dlog.trace=true   : activate trace mode
 * </pre>
 *
 * <p>
 * The trace mode will output a line showing where the call was made before each
 * message. Logging levels FATAL, ERROR and WARNING always output this
 * information, no matter the state of the trace mode (the idea behind it being
 * that if an error occurs, you'd like to know where it happens).
 * </p>
 *
 * <h2>Other notes</h2>
 *
 * <p>
 * Using the default, the LOGGER should work fairly well. If it doesn't fit you,
 * feel free to modify it. The code is pretty simple. But if you just want to
 * keep it simple, you can just start to use it anywhere like this:
 * </p>
 * <pre>
 *  // Simple message
 *  Logger.info("some text");
 *
 *  // Multiline message, v1
 *  Logger.warn("some text" + Logger.NL + "some other text");
 *
 *  // Multiline message, v2
 *  Logger.debug("some text");
 *  Logger.debug("some other text");
 * </pre>
 *
 * <p>
 * You should note that if you want to output a multiline message, the result is
 * the one you expect:
 * </p>
 * <pre>
 *     [info] 12:45  line 1
 *     [info] 12:45  line 2
 *
 *  and not:
 *
 *     [info] 12:45  line 1
 *     line 2
 * </pre>
 *
 * <p>
 * Finally, you'll see that you can log exception directly for log levels FATAL,
 * ERROR and WARN, nut not for levels below that. Again, the idea behind that is
 * that if there is an exception, it's either an error and you want to know
 * about it as such, or it's expected and then you want to catch it and maybe
 * show something more meaningful to your user.
 * </p>
 *
 * <h2>Ideas for future versions</h2>
 *
 * <ul>
 *  <li>More flexibility/variables for the message formatting</li>
 *  <li>Allows several outputs instead of only one (like output to the console
 *      and a file)</li>
 *  <li>Simplify setting a file as output (just provide the filename)</li>
 *  <li>Configure the size of the show trace (right now there is only one level,
 *      but you might want to see more).<li>
 * <ul>
 *
 * @author <a href="mailto:julien.lancien@nist.gov">Julien Lancien</a>
 * @version 0.1
 */
public final class Logger {

    /**
     * New line character for your platform
     */
    public static final String NL = System.getProperty("line.separator");

    // This instance is used as a way to initialize all the class variables. When
    // it's created, the constructor deals with all the configuration required.
    private static final Logger LOGGER = new Logger();
    private static Writer out;
    private static Writer err;

    // Logging Levels
    private static int logLevel;
    private static boolean logTrace;
    private static final String[] LEVELNAMES = new String[] {
        "fatal", "error", "warn", "info", "debug", "verbose"
    };
    private static final String[] LEVELSPC = new String[] {
        "  ", "  ", "   ", "   ", "  ", ""
    };
    private static final int FATAL   = 0;
    private static final int ERROR   = 1;
    private static final int WARN    = 2;
    private static final int INFO    = 3;
    private static final int DEBUG   = 4;
    private static final int VERBOSE = 5;

    // Formatting
    private static DateFormat dateFormat;
    private static String logFormat;

    /////////////////////////////////////////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////

    private Logger() {

        SimpleDateFormat sdf = new SimpleDateFormat("yyMMddHHmmssZ");
        String name =  "logAxis2_" + sdf.format(new Date()) + ".log";
        File f = new File("../logs/HL7WS_logs/" + name);
        FileOutputStream fos;
        try {
            fos = new FileOutputStream(f);
            out = new Writer(fos);
            err = new Writer(fos);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        // Getting the log level
        String dl = System.getProperty("log.level");
        if (!(null == dl || "" == dl)) {
            try {
                logLevel = (new Integer(dl)).intValue();
            } catch (Exception e) {
                logLevel = 3;
                System.err.println("  [Logger] The provided log level is invalid: '" + dl + "'");
                System.err.println("  [Logger] Using the default value: INFO");
            }
        } else {
            logLevel = 3;
        }
        // Checking log level
        if (FATAL > logLevel || VERBOSE < logLevel) {
            System.err.println("[Logger] Property 'log.level' is invalid. Must be in the range [0..5]");
            //System.exit(2);
        }
        // Setting trace
        String trace = System.getProperty("log.trace");
        logTrace = (null == trace) ? false : (new Boolean(trace)).booleanValue();
        // Default Formats
        setDateFormat(null);
        setLogFormat(null);
    }

    /////////////////////////////////////////////////////////////////////////////
    // Formatting
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Sets the date format. The syntax is the one of
     * {@link java.text.SimpleDateFormat SimpleDateFormat}.
     * Default is "HH:mm:ss".
     */
    public static synchronized void setDateFormat(String s) {
        if (null == s) {
            dateFormat = new SimpleDateFormat("HH:mm:ss");
        } else {
            dateFormat = new SimpleDateFormat(s);
        }
    }

    /**
     * <p>
     * Sets the output format. The syntax used is the one of
     * {@link java.text.MessageFormat MessageFormat}.
     * Available parameters are:
     * <ul>
     *  <li>{0} : Date (see setDateFormat)</li>
     *  <li>{1} : Message</li>
     *  <li>{2} : Log level (info, warn, etc..)</li>
     *  <li>{3} : Log level spaces (this is used to left/right align the log level
     *       information. For example, <code>{3}[{2}]</code> will show:<pre>
     *           [info] some text
     *          [debug] some text
     *       </pre>Whereas [{2}] would output:<pre>
     *          [info] some text
     *          [debug] some text
     *       </pre>
     *  </li>
     *  <li> {4} : Location where the call was made</li>
     * </ul>
     * </p>
     * <p>
     * Default format is: "<code> {3}[{2}] {0}    {1}</code>", which looks like
     * this:<pre>
     *    [info] 12:46:25  your text here
     *
     *   [debug] 12:46:29  >> package.someClass.someMethod(someFile.java:143)
     *   [debug] 12:46:29  your text here
     * </pre>
     * </p>
     */
    public static synchronized void setLogFormat(String s) {
        logFormat = (null == s) ? " {3}[{2}] {0}    {1}" : s;
    }

    private static String getLocation() {
        Throwable t = new Throwable();
        return (t.getStackTrace())[4].toString();
    }

    /////////////////////////////////////////////////////////////////////////////
    // Log Methods
    /////////////////////////////////////////////////////////////////////////////

    private static String format(String fmt, int level, String msg) {
        String date = dateFormat.format(new Date());
        // If we have a multiline string, we format each line
        String[] all = msg.split(NL);
        String result = "";
        for (int i = 0; i < all.length; i++) {
            result += MessageFormat.format(fmt, new Object[] {
                    date, all[i], LEVELNAMES[level], LEVELSPC[level], getLocation()
            });
            result += (i == all.length - 1) ? "" : NL;
        }
        return result;
    }

    private static synchronized void log(int level, String s) {
        if (level <= logLevel) {
            // Getting date
            String msg = format(logFormat, level, s);
            String trc = format(" {3}[{2}] {0}    >> {4}", level, "");
            // Getting the right writer
            Writer w = (level <= WARN) ? err : out;
            // Getting location if needed
            if (logTrace) {
                w.print(trc);
                w.print(msg);
                w.print("");
            } else if (level <= ERROR) {
                w.print(trc);
                w.print(msg);
            } else {
                w.print(msg);
            }
        }
    }

    private static synchronized void log(int level, Exception e) {
        if (level <= logLevel) {
            err.print(e);
            if (logTrace) {
                err.print("");
            }
        }
    }

    public static void fatal(String s) {
        log(FATAL, s);
    }

    public static void fatal(Exception e) {
        log(FATAL, e);
    }

    public static void error(String s) {
        log(ERROR, s);
    }

    public static void error(Exception e) {
        log(ERROR, e);
    }

    public static void warn(String s) {
        log(WARN, s);
    }

    public static void warn(Exception e) {
        log(WARN, e);
    }

    public static void info(String s) {
        log(INFO, s);
    }

    public static void debug(String s) {
        log(DEBUG, s);
    }

    public static void debug(Exception e) {
        log(DEBUG, e);
    }

    public static void verbose(String s) {
        log(VERBOSE, s);
    }

    public static void verbose(Exception e) {
        log(VERBOSE, e);
    }

    /////////////////////////////////////////////////////////////////////////////
    // Setting writers
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Sets the destination for log levels INFO, DEBUG, VERBOSE
     */
    public static synchronized void setOut(OutputStream p) {
        Logger.out = LOGGER.new Writer(p);
    }

    /**
     * Sets the destination for log levels FATAL, ERROR, WARNING
     */
    public static synchronized void setErr(OutputStream p) {
        Logger.err = LOGGER.new Writer(p);
    }

    /////////////////////////////////////////////////////////////////////////////
    // Output Writers
    /////////////////////////////////////////////////////////////////////////////
    private class Writer {

        private PrintStream ps;

        public Writer(OutputStream stream) {
            ps = new PrintStream(stream);
        }

        public void print(String s) {
            ps.println(s);
        }

        public void print(Exception e) {
            e.printStackTrace(ps);
        }
    }
}

