package jp.ehobby.logging;

import java.io.IOException;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;

import jp.ehobby.info.SystemInfo;
import jp.ehobby.util.number.NumberUtilities;
import jp.ehobby.util.syslog.Syslog;
import jp.ehobby.util.syslog.SyslogFacility;
import jp.ehobby.util.syslog.SyslogSeverity;


/**
 * Syslog に書き出す Handler.
 * SyslogHandler として以下の設定が可能です.
 * <pre>
 * jp.ehobby.logging.SyslogHandler.server   = Syslogサーバのアドレス (デフォルト:localhost)
 * jp.ehobby.logging.SyslogHandler.port     = Syslogサーバのポート   (デフォルト:514)
 * jp.ehobby.logging.SyslogHandler.tag      = Syslogのタグ           (デフォルト:指定なし)
 * jp.ehobby.logging.SyslogHandler.facility = Syslog分類             (デフォルト:USER_LEVEL)
 * jp.ehobby.logging.SyslogHandler.FINEST   = Syslog重要度           (デフォルト:DEBUG)
 * jp.ehobby.logging.SyslogHandler.FINER    = Syslog重要度           (デフォルト:DEBUG)
 * jp.ehobby.logging.SyslogHandler.FINE     = Syslog重要度           (デフォルト:DEBUG)
 * jp.ehobby.logging.SyslogHandler.CONFIG   = Syslog重要度           (デフォルト:INFORMATIONAL)
 * jp.ehobby.logging.SyslogHandler.INFO     = Syslog重要度           (デフォルト:INFORMATIONAL)
 * jp.ehobby.logging.SyslogHandler.WARNING  = Syslog重要度           (デフォルト:WARNING)
 * jp.ehobby.logging.SyslogHandler.SEVERE   = Syslog重要度           (デフォルト:CRITICAL)
 *
 * Syslogのタグのキーが存在しない場合、タグが出力されません.
 * Syslogのタグのキーが存在する場合、「[PID]」がログに書き出されます.
 * Syslogのタグが指定されている場合、「指定されたタグ[PID]」がログに書き出されます.
 * Syslog分類には {@link jp.ehobby.util.syslog.SyslogFacility} の定数名を指定可能です.
 * Syslog重要度には {@link jp.ehobby.util.syslog.SyslogSeverity} の定数名を指定可能です.
 * </pre>
 *
 * @author kei-n
 */
public class SyslogHandler extends Handler {


	/** Level リスト.			*/
	private static final Level[] LEVEL_LIST = {
		Level.FINEST,
		Level.FINER,
		Level.FINE,
		Level.CONFIG,
		Level.INFO,
		Level.WARNING,
		Level.SEVERE
	};

	/** デフォルトの Severity.	*/
	private static final SyslogSeverity[] DEFAULT_SEVERITYS = {
		SyslogSeverity.DEBUG,
		SyslogSeverity.DEBUG,
		SyslogSeverity.DEBUG,
		SyslogSeverity.INFORMATIONAL,
		SyslogSeverity.INFORMATIONAL,
		SyslogSeverity.WARNING,
		SyslogSeverity.CRITICAL
	};

	/** Syslog 出力用.						*/
	private final Syslog syslog;

	/** Syslog レベル.						*/
	private final HashMap<Level, SyslogSeverity> levelMap;

	/** Syslog 分類.						*/
	private final SyslogFacility facility;

	/** ローカルホスト名.					*/
	private final String hostName;

	/** Tag名.								*/
	private final String tag;

	/** syslog が close されたか否かを表す.	*/
	private boolean closed;


	/**
	 * SyslogHandler を構築します.
	 *
	 * @throws SocketException Socketエラー
	 * @throws UnknownHostException 不明なホスト
	 */
	public SyslogHandler() throws SocketException, UnknownHostException {
		LogManager logMgr = LogManager.getLogManager();
		String     cName  = getClass().getName();

		this.syslog   = createSyslog(logMgr, cName);
		this.facility = createFacility(logMgr, cName);
		this.levelMap = createLevelMap(logMgr, cName);
		this.hostName = SystemInfo.getLocalHostName();
		this.tag      = logMgr.getProperty(cName + ".tag");		//$NON-NLS-1$
		this.closed   = false;

		// その他設定
		configure(logMgr, cName);
	}


	@Override
	public void publish(final LogRecord record) {
		if (this.closed)         { return; }
		if (!isLoggable(record)) { return; }

		Level          level    = record.getLevel();
		SyslogSeverity severity = this.levelMap.get(level);
		Formatter      fmt      = getFormatter();
		String         message  = record.getMessage();
		if (fmt != null) {
			message  = fmt.format(record);
		}

		try {
			this.syslog.logger(
					this.facility,
					severity,
					this.hostName,
					this.tag,
					message);
		} catch (IOException e) {
			// Nothing to do.
		}

	}


	@Override
	public void flush() {
		// Nothing to do.
	}


	@Override
	public synchronized void close() throws SecurityException {
		if (this.closed) { return; }
		this.closed = true;
		try {
			this.syslog.close();
		} catch (IOException e) {
			// NOP
		}
	}


	////////////////////////////////////////////////////////////////////////////
	//
	// Syslog の設定
	//

	/**
	 * Syslogの設定を実施します.
	 *
	 * @param mgr LogManager
	 * @param cName クラス名
	 */
	private void configure(final LogManager mgr, final String cName) {
	    Formatter newFormatter = createFormatter(mgr, cName);
	    setFormatter(newFormatter);
	}


	/**
	 * 設定ファイルに指定されたサーバ、ポートにログを書き込む Syslog を構築します.
	 *
	 * @param mgr   LogManager
	 * @param cName クラス名
	 * @return Syslog
	 * @throws SocketException ソケットエラー
	 * @throws UnknownHostException 不明なホスト
	 */
	private static Syslog createSyslog(final LogManager mgr, final String cName) throws SocketException, UnknownHostException {
		// Syslog サーバ
		String server  = mgr.getProperty(cName + ".server");			//$NON-NLS-1$
		if (server == null) {
			server = "localhost";										//$NON-NLS-1$
		}

		// Syslog サーバポート
		String portStr = mgr.getProperty(cName + ".port");				//$NON-NLS-1$
		int port = Syslog.SERVER_PORT;
		if (portStr != null) {
			port = (int) NumberUtilities.longValue(portStr);
		}

		return new Syslog(server, port);
	}


	/**
	 * Syslog に出力する分類を生成します.
	 *
	 * @param mgr   LogManager
	 * @param cName クラス名
	 * @return SyslogFacility
	 */
	private static SyslogFacility createFacility(final LogManager mgr, final String cName) {
		// Syslog 分類設定
		String facilityName = mgr.getProperty(cName + ".facility");							//$NON-NLS-1$
		if (facilityName == null) {
			facilityName = SyslogFacility.USER_LEVEL.name();
		}
		return SyslogFacility.valueOf(facilityName);
	}


	/**
	 * LevelとSyslogの重要度対応MAPを生成します.
	 *
	 * @param mgr   LogManager
	 * @param cName クラス名
	 * @return LevelとSyslogの重要度対応MAP
	 */
	private static HashMap<Level, SyslogSeverity> createLevelMap(final LogManager mgr, final String cName) {
		HashMap<Level, SyslogSeverity> map = new HashMap<>();

		for (int i = 0; i < LEVEL_LIST.length; i++) {
			String         key      = LEVEL_LIST[i].getName();
			String         val      = mgr.getProperty(cName + '.' + key);
			SyslogSeverity severity = DEFAULT_SEVERITYS[i];
			if (val != null) {
				try {
					severity = SyslogSeverity.valueOf(val);
				} catch (IllegalArgumentException e) {
					// NOP
				}
			}
			map.put(LEVEL_LIST[i], severity);
		}
		return map;
	}


	/**
	 * Formatter を返します.
	 * Formatter が指定されていない場合、そのままのメッセージを出力する Formatter を返します.
	 *
	 * @param mgr LogManager
	 * @param cName クラス名
	 * @return Formatter
	 */
	private static Formatter createFormatter(final LogManager mgr, final String cName) {
		String val = mgr.getProperty(cName + ".formatter");		//$NON-NLS-1$
		if (val != null) {
			try {
				Class<Formatter> clz = (Class<Formatter>) ClassLoader.getSystemClassLoader().loadClass(val);
				return clz.newInstance();
			} catch (Exception e) {
				// Nothing to do.
			}
		}
		return new SyslogDefaultFormatter();
	}

}


/**
 * SyslogのデフォルトFormatter.
 * 特にフォーマットを実施せず、そのままメッセージを出力する.
 */
class SyslogDefaultFormatter extends Formatter {

	@Override
	public String format(LogRecord record) {
		return record.getMessage();
	}
}

