/*
 * Helma License Notice
 *
 * The contents of this file are subject to the Helma License
 * Version 2.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://adele.helma.org/download/helma/license.txt
 *
 * Copyright 1998-2003 Helma Software. All Rights Reserved.
 *
 * $RCSfile$
 * $Author$
 * $Revision$
 * $Date$
 */

package helma.util;

import org.apache.commons.logging.Log;

import java.io.*;
import java.text.*;
import java.util.*;

/**
 * A simple logger that writes to a PrintStream such as System.out.
 */
public class Logger implements Log {

    // buffer for log items; create a synchronized list for log entries since
    // different threads may attempt to modify the list at the same time
    List entries = Collections.synchronizedList(new LinkedList());

    // Writer for log output
    PrintWriter writer;

    // the canonical name for this logger
    String canonicalName;

    // fields for date rendering and caching
    static DateFormat dformat = new SimpleDateFormat("[yyyy/MM/dd HH:mm:ss] ");
    static long dateLastRendered;
    static String dateCache;

    public final static int TRACE = 1;
    public final static int DEBUG = 2;
    public final static int INFO =  3;
    public final static int WARN =  4;
    public final static int ERROR = 5;
    public final static int FATAL = 6;
    
    int logLevel = INFO;    

    // timestamp of last log message, used to close file loggers after longer
    // periods of inactivity
    long lastMessage = System.currentTimeMillis();

    // sedated log instance for jetty
    private Log sedatedLog = new SedatedLog();

    /**
     * zero argument constructor, only here for FileLogger subclass
     */
    Logger() {
        init();
    }

    /**
     * Create a logger for a PrintStream, such as System.out.
     * @param out the output stream
     */
    protected Logger(PrintStream out) {
        init();
        writer = new PrintWriter(out);
        canonicalName = out.toString();
    }

    /**
     * Get loglevel from System properties
     */
     private void init() {
        String level = System.getProperty("helma.loglevel");
        if ("trace".equalsIgnoreCase(level))
            logLevel = TRACE;
        else if ("debug".equalsIgnoreCase(level))
            logLevel = DEBUG;
        else if ("info".equalsIgnoreCase(level))
            logLevel = INFO;
        else if ("warn".equalsIgnoreCase(level))
            logLevel = WARN;
        else if ("error".equalsIgnoreCase(level))
            logLevel = ERROR;
        else if ("fatal".equalsIgnoreCase(level))
            logLevel = FATAL;
    }

    /**
     * Get the current log level.
     * @return the current log level
     */
    public int getLogLevel() {
        return logLevel;
    }

    /**
     * Set the log level for this logger.
     * @param logLevel the new log level
     */
    public void setLogLevel(int logLevel) {
        this.logLevel = logLevel;
    }

    /**
     * Return a string representation of this Logger
     */
    public String toString() {
        return new StringBuffer(getClass().getName()).append("[")
                .append(canonicalName).append(",").append(logLevel)
                .append("]").toString();
    }

    /**
     * Return an object  which identifies  this logger.
     * @return the canonical name of this logger
     */
    public String getCanonicalName() {
        return canonicalName;
    }

    /**
     * Append a message to the log.
     * @param level a string representing the log level
     * @param msg the log message
     * @param exception an exception, or null
     */
    protected void log(String level, Object msg, Throwable exception) {
        lastMessage = System.currentTimeMillis();
        // it's enough to render the date every second
        if ((lastMessage - 1000) > dateLastRendered) {
            renderDate();
        }
        // add a safety net so we don't grow indefinitely even if writer thread
        // has gone. the 2000 entries threshold is somewhat arbitrary. 
        if (entries.size() < 2000) {
            String message = msg == null ? "null" : msg.toString();
            Thread thread = Thread.currentThread();
            String threadId = "[" + thread.getName() + "] ";
            entries.add(new Entry(dateCache, level, message, threadId, exception));
        }
    }

    /**
     * This is called by the runner thread to perform actual output.
     */
    protected synchronized void write() {
        if (entries.isEmpty()) {
            return;
        }

        try {
            // make sure we have a valid writer
            ensureOpen();

            String color;
            int l = entries.size();

            for (int i = 0; i < l; i++) {
                Entry entry = (Entry) entries.remove(0);

                if (entry.level == "[TRACE] " || entry.level == "[DEBUG] ") {
                    color = "\u001B[0m";
                } else if (entry.level == "[INFO] ") {
                    color = "\u001B[0m";
                } else if (entry.level == "[WARN] ") {
                    color = "\u001B[0;33m";
                } else if (entry.level == "[ERROR] ") {
                    color = "\u001B[0;31m";
                } else if (entry.level == "[FATAL] ") {
                    color = "\033[1;31m";
                } else {
                    color = "\u001B[32m";
                }

                writer.print(color);
                writer.print(entry.date);
                writer.print(entry.level);
                writer.print(entry.threadId);
                writer.println(entry.message);

                if (entry.exception != null)
                    entry.exception.printStackTrace(writer);

                writer.print("\033[0m");
            }
            writer.flush();

        } catch (Exception x) {
            int size = entries.size();

            if (size > 1000) {
                // more than 1000 entries queued plus exception - something
                // is definitely wrong with this logger. Write a message to std err and
                // discard queued log entries.
                System.err.println("Error writing log file " + this + ": " + x);
                System.err.println("Discarding " + size + " log entries.");
                entries.clear();
            }
        }
    }

    /**
     * This is called by the runner thread to to make sure we have an open writer.
     */
    protected void ensureOpen() {
        // nothing to do for console logger
    }

    // Pre-render the date to use for log messages. Called about once a second.
    protected static synchronized void renderDate() {
        Date date = new Date();
        dateCache = dformat.format(date);
        dateLastRendered = date.getTime();
    }

    // methods to implement org.apache.commons.logging.Log interface

    public boolean isTraceEnabled() {
        return logLevel <= TRACE;
    }

    public boolean isDebugEnabled() {
        return logLevel <= DEBUG;
    }

    public boolean isInfoEnabled() {
        return logLevel <= INFO;
    }

    public boolean isWarnEnabled() {
        return logLevel <= WARN;
    }

    public boolean isErrorEnabled() {
        return logLevel <= ERROR;
    }

    public boolean isFatalEnabled() {
        return logLevel <= FATAL;
    }

    public void trace(Object parm1) {
        if (logLevel <= TRACE)
            log("[TRACE] ", parm1, null);
    }

    public void trace(Object parm1, Throwable parm2) {
        if (logLevel <= TRACE)
            log("[TRACE] ", parm1, parm2);
    }

    public void debug(Object parm1) {
        if (logLevel <= DEBUG)
            log("[DEBUG] ", parm1, null);
    }

    public void debug(Object parm1, Throwable parm2) {
        if (logLevel <= DEBUG)
            log("[DEBUG] ", parm1, parm2);
    }

    public void info(Object parm1) {
        if (logLevel <= INFO)
            log("[INFO] ", parm1, null);
    }

    public void info(Object parm1, Throwable parm2) {
        if (logLevel <= INFO)
            log("[INFO] ", parm1, parm2);
    }

    public void warn(Object parm1) {
        if (logLevel <= WARN)
            log("[WARN] ", parm1, null);
    }

    public void warn(Object parm1, Throwable parm2) {
        if (logLevel <= WARN)
            log("[WARN] ", parm1, parm2);
    }

    public void error(Object parm1) {
        if (logLevel <= ERROR)
            log("[ERROR] ", parm1, null);
    }

    public void error(Object parm1, Throwable parm2) {
        if (logLevel <= ERROR)
            log("[ERROR] ", parm1, parm2);
    }

    public void fatal(Object parm1) {
        if (logLevel <= FATAL)
            log("[FATAL] ", parm1, null);
    }

    public void fatal(Object parm1, Throwable parm2) {
        if (logLevel <= FATAL)
            log("[FATAL] ", parm1, parm2);
    }

    // utility method to get the stack trace from a Throwable as string
    public static String getStackTrace(Throwable t) {
        StringWriter stringWriter = new StringWriter();
        PrintWriter writer = new PrintWriter(stringWriter);
        t.printStackTrace(writer);
        writer.close();
        return stringWriter.toString();
    }

    class Entry {
        final String date, level, message, threadId;
        final Throwable exception;

        Entry(String date, String level, String message, String threadId, Throwable exception) {
            this.date = date;
            this.level = level;
            this.message = message;
            this.threadId = threadId;
            this.exception = exception;
        }
    }

    /**
     * return a "quiet" version of this log that routes debug() output to trace()
     * @return a possibly less verbose version of this log.
     */
    protected Log getSedatedLog() {
        return sedatedLog;
    }

    /*
     * A inner class that "calms down" logging output by routing debug() output
     * to trace(). This is useful for software like Jetty, which has really
     * verbose output at DEBUG level (dumps whole HTTP request and response headers).
     * You can enable that output by setting the log level to TRACE.
     */
    class SedatedLog implements Log {

        public void debug(Object o) {
            // Route debug() to trace()
            Logger.this.trace(o);
        }

        public void debug(Object o, Throwable t) {
            // Route debug() to trace()
            Logger.this.trace(o, t);
        }

        public void error(Object o) {
            Logger.this.error(o);
        }

        public void error(Object o, Throwable t) {
            Logger.this.error(o, t);
        }

        public void fatal(Object o) {
            Logger.this.fatal(o);
        }

        public void fatal(Object o, Throwable t) {
            Logger.this.fatal(o, t);
        }

        public void info(Object o) {
            Logger.this.info(o);
        }

        public void info(Object o, Throwable t) {
            Logger.this.info(o, t);
        }

        public void trace(Object o) {
            // swallow trace()
        }

        public void trace(Object o, Throwable t) {
            // swallow trace()
        }

        public void warn(Object o) {
            Logger.this.warn(o);
        }

        public void warn(Object o, Throwable t) {
            Logger.this.warn(o, t);
        }

        public boolean isDebugEnabled() {
            // Route debug() to trace()
            return Logger.this.isTraceEnabled();
        }

        public boolean isErrorEnabled() {
            return Logger.this.isErrorEnabled();
        }

        public boolean isFatalEnabled() {
            return Logger.this.isFatalEnabled();
        }

        public boolean isInfoEnabled() {
            return Logger.this.isInfoEnabled();
        }

        public boolean isTraceEnabled() {
            // swallow trace()
            return false;
        }

        public boolean isWarnEnabled() {
            return Logger.this.isWarnEnabled();
        }

    }

}
