/*
 * 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.aspect;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import jp.sourceforge.mergedoc.pleiades.Pleiades;
import jp.sourceforge.mergedoc.pleiades.log.Logger;

/**
 * 計測ユーティリティーです。
 * <p>
 * クラスが状態を持っていますが、同期化されていないため、計測結果が壊れている場合があるかもしれませ
 * ん。厳密性よりパフォーマンスが優先されています。
 * <p>
 * 基本的に計測は AOP によりこのクラスのメソッド呼び出しで行われますが、AOP を適用できないクラス向け
 * のメソッドも用意されています。AOP を適用できないクラスとは、最初に起動されるエージェント・クラス
 * や AOP の埋め込み処理自体を行うトランスフォーマー・クラスなどが該当します。
 * <p>
 * @author cypher256
 */
public class Analyses {

	// ロガー初期化チェック
	static {
		if (System.getProperty(Logger.LOGGER_CLASS_NAME) == null) {
			String m = "まだ Logger が明示的に初期化されていません。";
			System.err.println(m);
			throw new IllegalStateException(m);
		}
	}

	/** ロガー */
	private static final Logger log = Logger.getLogger(Analyses.class);

	/** 呼び出し回数時間マップ */
	private static final Map<String, CallsTime> callsTimeMap = new ConcurrentHashMap<String, CallsTime>();

	/** 呼び出し回数時間クラス (time 降順の Comparable) */
	private static class CallsTime implements Comparable<CallsTime> {

		final String jointPoint;
		volatile long calls;
		volatile long cumulativeTime;
		volatile long lastStartNanoTime;

		CallsTime(String jointPoint) {
			this.jointPoint = jointPoint;
		}
		public int compareTo(CallsTime o) {
			return o.cumulativeTime > cumulativeTime ? 1 : -1;
		}
	}

	/**
	 * 生成できません。
	 */
	private Analyses() {
	}

	/**
	 * 計測結果をクリアし、ログに出力します。
	 * <p>
	 * @param message メッセージ
	 * @param args メッセージ埋め込みオブジェクト
	 */
	public static void flashLog(String message, Object... args) {

		log.info(message, args);
		if (!log.isDebugEnabled()) {
			return;
		}

		List<CallsTime> values = new LinkedList<CallsTime>(callsTimeMap.values());
		callsTimeMap.clear();
		Collections.sort(values);
		final String format = "%8d 回呼出計 %6.3f 秒 %s";
		final int MAX_OUT = 30;

		for (int i = 0; i < values.size() && i < MAX_OUT; i++) {

			CallsTime ct = values.get(i);
			if (ct.calls == 0) {
				continue;
			}
			String name = ct.jointPoint;
			if (name.startsWith(Analyses.class.getName())) {
				continue;
			}
			name = name.replace(Pleiades.PACKAGE_NAME + ".", "");

			log.debug(format, ct.calls, ct.cumulativeTime / 1e+9, name);
		}
	}

	//-------------------------------------------------------------------------
	// AOP 用のメソッド

	/**
	 * 計測を開始します。
	 * AOP 構成ファイルで使用されることを想定しています。
	 * <p>
	 * @param classMethodName クラス、メソッド名
	 */
	public static void start(String classMethodName) {

		if (!log.isDebugEnabled()) {
			return;
		}
		CallsTime callsTime = callsTimeMap.get(classMethodName);
		if (callsTime == null) {
			callsTime = new CallsTime(classMethodName);
			callsTimeMap.put(classMethodName, callsTime);
		}
		callsTime.lastStartNanoTime = System.nanoTime();
	}

	/**
	 * 計測を終了します。
	 * AOP 構成ファイルで使用されることを想定しています。
	 * <p>
	 * @param classMethodName クラス、メソッド名
	 */
	public static void end(String classMethodName) {

		if (!log.isDebugEnabled()) {
			return;
		}
		CallsTime callsTime = callsTimeMap.get(classMethodName);
		if (callsTime == null) {
			return;
		}
		callsTime.cumulativeTime += System.nanoTime() - callsTime.lastStartNanoTime;
		callsTime.calls++;
	}

	//-------------------------------------------------------------------------
	// AOP 以外用のメソッド

	/**
	 * 計測を終了します。
	 * このメソッドは AOP 開始前でウィービングができない場合に明示的に呼び出します。
	 * <p>
	 * @param clazz クラス
	 * @param methodName メソッド名
	 * @param startNanoTime 開始時間
	 */
	public static void end(Class<?> clazz, String methodName, long startNanoTime) {

		if (!log.isDebugEnabled()) {
			return;
		}
		String classMethodName = clazz.getName() + "#" + methodName;
		if (callsTimeMap.keySet().contains(classMethodName)) {
			log.debug("AOP ですでに計測処理が埋め込まれています。明示的なコードは不要です。%s", classMethodName);
		}

		String manualName = classMethodName + " [MANUAL]";
		CallsTime callsTime = callsTimeMap.get(manualName);
		if (callsTime == null) {
			callsTime = new CallsTime(manualName);
			callsTimeMap.put(manualName, callsTime);
		}
		callsTime.cumulativeTime += System.nanoTime() - startNanoTime;
		callsTime.calls++;
	}
}
