/*
 * Copyright (c) 2005- Shinji Kashihara.
 * All rights reserved. This program are made available under
 * the terms of the Eclipse Public License v1.0 which accompanies
 * this distribution, and is available at epl-v10.html.
 */
package jp.sourceforge.mergedoc.pleiades.log;

import java.io.File;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import jp.sourceforge.mergedoc.org.apache.commons.lang.ArrayUtils;

/**
 * Log4J ライクなインタフェースを持つ抽象ロガーです。
 * メッセージのフォーマットのための可変長引数によるメッセージ埋め込み機能を持ちます。
 * <p>
 * @author cypher256
 */
public abstract class Logger {

	/** ロガー・マップ */
	private static Map<String, Logger> loggerMap = new HashMap<String, Logger>();

	/** ロガー・クラス名を示すシステム・プロパティーのキー */
	public static final String LOGGER_CLASS_NAME = "pleiades.logger.class.name";

	/** ログ・レベルを示すシステム・プロパティーのキー */
	public static final String LOG_LEVEL = "pleiades.log.level";

	/** ログ・レベル列挙型 */
	public enum Level {
		/** デバッグ */
		DEBUG,
		/** 情報 */
		INFO,
		/** 警告 */
		WARN,
		/** エラー */
		ERROR,
		/** 致命的 */
		FATAL
	}

	/**
	 * ロガーを取得します。
	 * <p>
	 * @param clazz カテゴリー・クラス (通常はログを使用するクラス)
	 * @return ロガー
	 */
	public static Logger getLogger(Class<?> clazz) {
		return getLoggerInternal(getShortClassName(clazz));

	}

	/**
	 * ロガーを取得します。
	 * <p>
	 * @param category カテゴリー
	 * @return ロガー
	 */
	public static Logger getLogger(String category) {
		return getLoggerInternal(category + "-Category");
	}

	/**
	 * ロガーを取得します。
	 * 第 2 引数以降のオプションは init での設定を無視し、
	 * このメソッドでのみ適用されます。
	 * <p>
	 * @param category カテゴリー
	 * @param loggerClass ロガー・クラス
	 * @param logLevel ログ・レベル
	 * @param logFile ログ・ファイル
	 * @return ロガー
	 */
	public static Logger getLogger(String category,
		Class<? extends Logger> loggerClass, Level logLevel) {

		return getLogger(category, loggerClass, logLevel, null);
	}

	/**
	 * ロガーを取得します。
	 * 第 2 引数以降のオプションは init での設定を無視し、
	 * このメソッドでのみ適用されます。
	 * <p>
	 * @param category カテゴリー
	 * @param loggerClass ロガー・クラス
	 * @param logLevel ログ・レベル
	 * @param logFile ログ・ファイル
	 * @return ロガー
	 */
	public static Logger getLogger(String category,
		Class<? extends Logger> loggerClass, Level logLevel, File logFile) {

		synchronized (Logger.class) {

			String lcn = System.getProperty(Logger.LOGGER_CLASS_NAME);
			String ll = System.getProperty(Logger.LOG_LEVEL);
			String lfn = System.getProperty(FileLogger.LOG_FILE_NAME);

			try {
				init(loggerClass, logLevel, logFile);
				return getLogger(category);

			} finally {

				setSystemProperty(LOGGER_CLASS_NAME, lcn);
				setSystemProperty(LOG_LEVEL, ll);
				setSystemProperty(FileLogger.LOG_FILE_NAME, lfn);
			}
		}
	}

	/**
	 * ログ環境を初期化します。
	 * カテゴリーごとにキャッシュされているロガーもクリアします。
	 * このメソッドが呼び出されない場合のデフォルトは標準出力、INFO です。
	 * <p>
	 * @param loggerClass ロガー・クラス
	 * @param logLevel ログ・レベル
	 */
	public static void init(Class<? extends Logger> loggerClass, Level logLevel) {
		init(loggerClass, logLevel, null);
	}

	/**
	 * ログ環境を初期化します。
	 * カテゴリーごとにキャッシュされているロガーもクリアします。
	 * このメソッドが呼び出されない場合のデフォルトは標準出力、INFO です。
	 * <p>
	 * @param loggerClass ロガー・クラス
	 * @param logLevel ログ・レベル
	 * @param logFile ログ・ファイル
	 */
	public static void init(Class<? extends Logger> loggerClass, Level logLevel, File logFile) {

		synchronized (Logger.class) {

			setSystemProperty(LOGGER_CLASS_NAME,
				loggerClass == null ? null : loggerClass.getName());
			setSystemProperty(LOG_LEVEL,
				logLevel == null ? null : logLevel.toString());
			setSystemProperty(FileLogger.LOG_FILE_NAME,
				logFile == null ? null : logFile.getAbsolutePath());

			loggerMap.clear();
		}
	}

	/**
	 * システム・プロパティーをセットします。
	 * value が null の場合はクリアします。
	 * <p>
	 * @param key キー
	 * @param value 値
	 */
	private static void setSystemProperty(String key, String value) {
		if (value == null) {
			System.clearProperty(key);
		} else {
			System.setProperty(key, value);
		}
	}

	/**
	 * ロガーを取得します。
	 * <p>
	 * @param category カテゴリー
	 * @return ロガー
	 */
	private static Logger getLoggerInternal(String category) {

		synchronized (Logger.class) {

			Logger logger = loggerMap.get(category);
			if (logger != null) {
				return logger;
			}

			String loggerClassName =
				System.getProperty(LOGGER_CLASS_NAME, SystemOutLogger.class.getName());
			try {
				Class<?> loggerClass = Class.forName(loggerClassName);
				Class<? extends Logger> subClass = loggerClass.asSubclass(Logger.class);
				Constructor<? extends Logger> cons = subClass.getDeclaredConstructor(String.class);
				cons.setAccessible(true);
				logger = cons.newInstance(category);

			} catch (Exception e) {
				throw new IllegalStateException(
					"システムプロパティ " + LOGGER_CLASS_NAME + "=" + loggerClassName +
					" に指定されたロガークラスがロードできませんでした。", e);
			}

			String logLevel = System.getProperty(LOG_LEVEL, Level.INFO.toString());
			try {
				logger.minLevel = Level.valueOf(logLevel.toUpperCase());

			} catch (IllegalStateException e) {
				throw new IllegalStateException(
					"システムプロパティ " + LOG_LEVEL + "=" + logLevel + " に指定する値は " +
					Level.class.getName() + "で定義された型である必要があります。");
			}
			loggerMap.put(category, logger);

			// DEBUG
			//System.out.println(logger.toString());
			return logger;
		}
	}

	/**
	 * 単純クラス名を取得します。
	 * <p>
	 * @param clazz クラス
	 * @return 単純クラス名
	 */
	private static String getShortClassName(Class<?> clazz) {
		return clazz.getName().replaceFirst("(\\w+\\.)+", "");
	}

	//-------------------------------------------------------------------------

	/** カテゴリー */
	private final String category;

	/** 最小出力ログ・レベル */
	private Level minLevel;

	/**
	 * ロガーを構築します。
	 * <p>
	 * @param category カテゴリー
	 */
	protected Logger(String category) {
		this.category = category;
	}

	/**
	 * 出力先となる PrintStream を取得します。
	 * <p>
	 * @return 出力先となる PrintStream
	 */
	abstract protected PrintStream getOut();

	/**
	 * メッセージを出力します。
	 * <p>
	 * @param level ログ・レベル
	 * @param t 例外。null の場合は message のみを出力。
	 * @param message メッセージ
	 * @param args メッセージ埋め込みオブジェクト (String#format)
	 */
	protected void println(Level level, Throwable t, String message, Object... args) {

		if (args.length == 1 && args[0] instanceof Exception) {
			throw new IllegalArgumentException("args に例外オブジェクトが指定されています。");
		}
		message = format(message, args);

		Date date = new Date();
		String threadName = Thread.currentThread().getName();
		String s = format(
			"%-5s %tT.%tL [%.20s] (%s) %s",
			level, date, date, threadName, category, message);

		PrintStream out = getOut();
		out.print(s + "\n");
		if (t != null) {
			t.printStackTrace(out);
		}
	}

	/**
	 * メッセージをフォーマットします。
	 * <p>
	 * @param message メッセージ
	 * @param args メッセージ埋め込みオブジェクト
	 * @return フォーマット後のメッセージ (String#format)
	 */
	protected String format(String message, Object... args) {

		if (message == null) {
			return "null";
		}
		if (args == null || args.length == 0) {
			return message;
		}
		try {
			return String.format(message, args);
		} catch (Exception e) {
			return message + ArrayUtils.toString(args) +
				" ログ・フォーマット・エラー: " + e;
		}
	}

	/**
	 * デバッグ・ログを出力します。
	 * <p>
	 * @param message メッセージ
	 * @param args メッセージ埋め込みオブジェクト (String#format)
	 */
	public void debug(String message, Object... args) {
		debug(null, message, args);
	}

	/**
	 * デバッグ・ログを出力します。
	 * <p>
	 * @param e 例外
	 * @param message メッセージ
	 * @param args メッセージ埋め込みオブジェクト (String#format)
	 */
	public void debug(Throwable e, String message, Object... args) {
		if (isDebugEnabled()) println(Level.DEBUG, e, message, args);
	}

	/**
	 * 情報ログを出力します。
	 * <p>
	 * @param message メッセージ
	 * @param args メッセージ埋め込みオブジェクト (String#format)
	 */
	public void info(String message, Object... args) {
		info(null, message, args);
	}

	/**
	 * 情報ログを出力します。
	 * <p>
	 * @param e 例外
	 * @param message メッセージ
	 * @param args メッセージ埋め込みオブジェクト (String#format)
	 */
	public void info(Throwable e, String message, Object... args) {
		if (isInfoEnabled()) println(Level.INFO, e, message, args);
	}

	/**
	 * 警告ログを出力します。
	 * <p>
	 * @param message メッセージ
	 * @param args メッセージ埋め込みオブジェクト (String#format)
	 */
	public void warn(String message, Object... args) {
		warn(null, message, args);
	}

	/**
	 * 警告ログを出力します。
	 * <p>
	 * @param e 例外
	 * @param message メッセージ
	 * @param args メッセージ埋め込みオブジェクト (String#format)
	 */
	public void warn(Throwable e, String message, Object... args) {
		if (isWarnEnabled()) println(Level.WARN, e, message, args);
	}

	/**
	 * エラー・ログを出力します。
	 * <p>
	 * @param message メッセージ
	 * @param args メッセージ埋め込みオブジェクト (String#format)
	 */
	public void error(String message, Object... args) {
		error(null, message, args);
	}

	/**
	 * エラー・ログを出力します。
	 * <p>
	 * @param e 例外
	 * @param message メッセージ
	 * @param args メッセージ埋め込みオブジェクト (String#format)
	 */
	public void error(Throwable e, String message, Object... args) {
		if (isErrorEnabled()) println(Level.ERROR, e, message, args);
	}

	/**
	 * 致命的エラー・ログを出力します。
	 * <p>
	 * @param message メッセージ
	 * @param args メッセージ埋め込みオブジェクト (String#format)
	 */
	public void fatal(String message, Object... args) {
		fatal(null, message, args);
	}

	/**
	 * 致命的エラー・ログを出力します。
	 * <p>
	 * @param e 例外
	 * @param message メッセージ
	 * @param args メッセージ埋め込みオブジェクト (String#format)
	 */
	public void fatal(Throwable e, String message, Object... args) {
		if (isFatalEnabled()) println(Level.FATAL, e, message, args);
	}

	/**
	 * デバッグ・ログが有効か判定します。
	 * <p>
	 * @return 有効な場合は true
	 */
	public boolean isDebugEnabled() {
		return	minLevel == Level.DEBUG;
	}

	/**
	 * 情報ログが有効か判定します。
	 * <p>
	 * @return 有効な場合は true
	 */
	public boolean isInfoEnabled() {
		return	minLevel == Level.DEBUG || minLevel == Level.INFO ;
	}

	/**
	 * 警告ログが有効か判定します。
	 * <p>
	 * @return 有効な場合は true
	 */
	public boolean isWarnEnabled() {
		return	minLevel == Level.DEBUG || minLevel == Level.INFO  ||
				minLevel == Level.WARN ;
	}

	/**
	 * エラー・ログが有効か判定します。
	 * <p>
	 * @return 有効な場合は true
	 */
	public boolean isErrorEnabled() {
		return	minLevel == Level.DEBUG || minLevel == Level.INFO  ||
				minLevel == Level.WARN  || minLevel == Level.ERROR;
	}

	/**
	 * 致命的エラー・ログが有効か判定します。
	 * <p>
	 * @return 有効な場合は true
	 */
	public boolean isFatalEnabled() {
		return true;
	}

	/**
	 * 指定したレベルのログが有効か判定します。
	 * <p>
	 * @param level レベル
	 * @return 有効な場合は true
	 */
	public boolean isEnabled(Level level) {

		if (level == Level.DEBUG) {
			return isDebugEnabled();
		} else if (level == Level.INFO) {
			return isInfoEnabled();
		} else if (level == Level.WARN) {
			return isWarnEnabled();
		} else if (level == Level.ERROR) {
			return isErrorEnabled();
		} else if (level == Level.FATAL) {
			return isFatalEnabled();
		}
		throw new IllegalArgumentException("不正なレベル: " + level);
	}

	/**
	 * このクラスの文字列表現を取得します。
	 */
	@Override
	public String toString() {
		String format = "%-5s %-15s (%s)";
		return String.format(format, minLevel, getShortClassName(getClass()), category);
	}
}
