/*
 * Copyright (C) 2006 Steve Ratcliffe
 * 
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 * 
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 * 
 * 
 * Author: Steve Ratcliffe
 * Create date: 30-Dec-2006
 */
package uk.me.parabola.log;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.LogManager;

/**
 * Simple logging class.  By default it is disabled.  You have to set it up
 * using (currently) a system property.  On the other hand it uses varargs
 * to make easier logging without having to do string concatenation in the
 * regular code.
 *
 * @author Steve Ratcliffe
 */
public class Logger {
	private final java.util.logging.Logger log;
	private final boolean addPrefix;
	public static final Logger defaultLogger = new Logger(java.util.logging.Logger.GLOBAL_LOGGER_NAME, false);

	private static final ThreadLocal<String> threadTags = new ThreadLocal<>();

	static {
		initLogging();
	}

	private Logger(String name, boolean addPrefix) {
		this.log = java.util.logging.Logger.getLogger(name);
		this.addPrefix = addPrefix;
	}

	/**
	 * Get a logger by its name.
	 *
	 * @param name The name of the logger.  Uses the same conventions as in
	 * java.util.logging.Logger as this is just a thin wrapper around that
	 * class.
	 * @return The logger.
	 */
	public static Logger getLogger(String name) {
		return new Logger(name, true);
	}

	/**
	 * Convenience class to get a logger using a class name as the name.
	 * @param aClass The class - its name will be used to retrieve the
	 * logger.
	 * @return The logger.
	 */
	public static Logger getLogger(Class<?> aClass) {
		String name = aClass.getName();
		return getLogger(name);
	}

	public static void resetLogging(String filename) {
		initLoggingFromFile(filename);
	}

	private static void initLogging() {
		Properties props = System.getProperties();

		String logconf = props.getProperty("log.config");
		if (logconf != null) {
			initLoggingFromFile(logconf);
		}
		else {
			staticSetup();
		}
		if (!defaultLogger.isLoggable(Level.WARNING))
			defaultLogger.log.setLevel(Level.WARNING);
	}

	private static void initLoggingFromFile(String logconf) {
		try {
			InputStream in = new FileInputStream(logconf);
			LogManager lm = LogManager.getLogManager();
			lm.reset();
			lm.readConfiguration(in);
		} catch (FileNotFoundException e) {
			System.err.println("Failed to open logging config file " + logconf);
			staticSetup();
		} catch (IOException e) {
			staticSetup();
		}
	}

	/**
	 * The default setup, which is basically not to do any logging apart from
	 * showing errors.
	 */
	private static void staticSetup() {
		// Static setup.
		LogManager.getLogManager().reset();
		java.util.logging.Logger l = java.util.logging.Logger.getLogger("");

		ConsoleHandler handler = new ConsoleHandler();
		UsefulFormatter f = new UsefulFormatter();

		f.setShowTime(false);

		handler.setFormatter(f);
		handler.setLevel(Level.FINE);

		l.addHandler(handler);
		l.setLevel(Level.SEVERE);
	}

	public boolean isLoggable(Level level) {
		return log.isLoggable(level);
	}

	public boolean isDebugEnabled() {
		return log.isLoggable(Level.FINE);
	}

	public boolean isInfoEnabled() {
		return log.isLoggable(Level.INFO);
	}

	/**
	 * Debug message.  We are using the j.u.l FINE level for this.  As it is
	 * possible that the toString method on the logged object is expensive
	 * we check that the message should be logged first.  Though this is
	 * perhaps overkill.
	 *
	 * This comment applies to all the corresponding methods below.
	 *
	 * @param o The object to be logged.
	 */
	public void debug(Object o) {
		if (log.isLoggable(Level.FINE))
			log.fine(tagMessage(o == null? "null" : o.toString()));
	}

	/**
	 * Log a message that consists of a variable number of arguments.  The
	 * arguments are simply concatenated with a space between them.
	 *
	 * The arrayFormat call is very expensive and checking the log level first
	 * is important.  The same applies to all similar routines below.
	 *
	 * @param olist The list of objects to log as one message.
	 */
	public void debug(Object ... olist) {
		if (log.isLoggable(Level.FINE))
			arrayFormat(Level.FINE, olist);
	}

	public void info(Object o) {
		if (log.isLoggable(Level.INFO))
			log.info(tagMessage(o == null? "null" : o.toString()));
	}

	public void info(Object ... olist) {
		if (log.isLoggable(Level.INFO))
			arrayFormat(Level.INFO, olist);
	}

	public void infof(String fmt, Object... args) {
		if (log.isLoggable(Level.INFO))
			printf(Level.INFO, fmt, args);
	}

	public void warn(Object o) {
		if (log.isLoggable(Level.WARNING))
			log.warning(tagMessage(o == null? "null" : o.toString()));
	}

	public void warn(Object ... olist) {
		if (log.isLoggable(Level.WARNING))
			arrayFormat(Level.WARNING, olist);
	}

	public void warnf(String fmt, Object... args) {
		if (log.isLoggable(Level.WARNING))
			printf(Level.WARNING, fmt, args);
	}

	public void error(Object o) {
		log.severe(tagMessage(o == null? "null" : o.toString()));
	}

	public void error(Object ... olist) {
		arrayFormat(Level.SEVERE, olist);
	}

	public void errorf(String fmt, Object... args) {
		printf(Level.SEVERE, fmt, args);
	}
	
	public void error(Object o, Throwable e) {
		log.log(Level.SEVERE, tagMessage(o == null? "null" : o.toString()), e);
	}
	
	// output a requested diagnostic message
	public void diagnostic(String msg) {
		log.log(LogLevel.DIAGNOSTIC, tagMessage(msg));
	}

	// output an echo or echotags message
	public void echo(String msg) {
		log.log(LogLevel.ECHO, tagMessage(msg));
	}

	// an information message that is always output
	public void write(String msg) {
		log.log(LogLevel.OVERRIDE, tagMessage(msg));
	}

	public void log(Level level, Object o) {
		if (log.isLoggable(level))
			log.log(level, tagMessage(o == null? "null" : o.toString()));
	}

	public void log(Level level, Object ... olist) {
		if (log.isLoggable(level))
			arrayFormat(level, olist);
	}
	
	/**
	 * Format the list of arguments by appending them to one string, keeping a
	 * space between them.
	 *
	 * Only call this if you've checked that the message needs to be printed,
	 * otherwise it will all go to waste.
	 *
	 * @param type The Level type FINE, INFO etc.
	 * @param olist The argument list as objects.
	 */
	private void arrayFormat(Level type, Object... olist) {
		if (log.isLoggable(type)) {
			StringBuilder sb = new StringBuilder();
			for (Object o : olist) {
				sb.append(o);
				sb.append(' ');
			}
			sb.setLength(sb.length()-1);
			log.log(type, tagMessage(sb.toString()));
		}
	}

	private void printf(Level type, String fmt, Object... args) {
		if (log.isLoggable(type)) {
			String msg = String.format(fmt, args);
			log.log(type, tagMessage(msg));
		}
	}

	private String tagMessage(String message) {
		if (!addPrefix)
			return message;
		
		String threadTag = threadTags.get();
		return (threadTag != null) ? threadTag + ": " + message : message;
	}

	public void threadTag(String tag) {
		threadTags.set(tag);
	}

	public String threadTag() {
		return threadTags.get();
	}
}
