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

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicInteger;

import jp.sourceforge.mergedoc.pleiades.log.Logger;
import jp.sourceforge.mergedoc.pleiades.resource.PropertySet;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.JointPoint;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.PleiadesConfig;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.PointCut;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.TraceConfig;
import jp.sourceforge.mergedoc.pleiades.runtime.advice.TraceJointPoint;

/**
 * 呼び出し階層エクスプローラーです。
 * <p>
 * @author cypher256
 */
public class CallHierarchyExplorer {

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

	/** 呼び出しトレースを検査する最大階層 (変更すると不具合、可変にする場合は XML 定義追加実装が必要) */
	public static final int TRACE_MAX = 20;

	/** このクラスのシングルトン・インスタンス */
	private static final CallHierarchyExplorer singleton = new CallHierarchyExplorer();

	/**
	 * 呼び出し階層探査オブジェクトを取得します。
	 * <p>
	 * @return 呼び出し階層探査オブジェクト
	 */
	public static CallHierarchyExplorer getInstance() {
		return singleton;
	}

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

	/** 除外パッケージ・プロパティー */
	private final ExcludePackageProperties excludePackageProperties = ExcludePackageProperties.getInstance();

	/** トレース構成 (通常実行時は null) */
	private final TraceConfig traceConfig = PleiadesConfig.getInstance().traceConfig;

	/**
	 * 翻訳対象外か判定します。
	 * 呼び出し元のパッケージやクラスを元に判定されます。
	 * <p>
	 * @param en 英語リソース文字列（ニーモニック無し）
	 * @param whereJP 呼び出し元ジョイント・ポイント (null 可、*cludeTrace を使用する場合は XML に ?{JOINT_POINT} が必要)
	 * @return 翻訳対象外の場合は true
	 */
	public boolean isExcludeTranslation(String en, JointPoint whereJP) {

		StackTraceElement[] stes = null;

		// properties [%EXCULUDE%] 呼び出し元による除外（訳語とパッケージ単位）
		// xml 定義のアドバイスに ?{JOINT_POINT} 指定は不要。
		List<String> noTransPathEntries = excludePackageProperties.getPathEntries(en);

		if (noTransPathEntries != null) {
			debugCalls(whereJP, stes);
			stes = getStackTrace();

			for (int i = 0; i < TRACE_MAX && i < stes.length; i++) {

				StackTraceElement ste = stes[i];
				String className = ste.getClassName();

				for (String noTransPath : noTransPathEntries) {
					if (className.startsWith(noTransPath)) {
						return true;
					}
				}
			}
		}

		// トレース時は除外・限定しない
		if (traceConfig != null && traceConfig.disabled) {
			return false;
		}

		// xml [*cludeTrace] 呼び出し元トレースによる除外と限定（トレースを逆上る）。
		// xml 定義のアドバイスに ?{JOINT_POINT} 指定が必要。
		if (whereJP != null) {

			PointCut pointCut = PleiadesConfig.getInstance().getPointCut(whereJP);

			if (pointCut != null) {

				List<TraceJointPoint> excludeTrace = pointCut.getExcludeTrace();
				List<TraceJointPoint> includeTrace = pointCut.getIncludeTrace();

				// include 指定に一致する場合は除外しない (exclude より優先)
				if (includeTrace.size() > 0) {
					debugCalls(whereJP, stes);
					if (containsTrace(includeTrace, stes, en)) {
						return false;
					}
					// include に一致しない かつ exclude が 0 の場合は除外する
					if (excludeTrace.size() == 0) {
						return true;
					}
				}

				// exclude 指定に一致する場合は除外する
				if (excludeTrace.size() > 0) {
					debugCalls(whereJP, stes);
					if (containsTrace(excludeTrace, stes, en)) {
						return true;
					}
				}
			}
		}

		return false;
	}

	/**
	 * スタックトレースにトレース・リストのジョイント・ポイントが含まれるか判定します。
	 * <p>
	 * @param traceDefList トレース定義リスト
	 * @param stacks スタックトレース配列
	 * @param en 英語リソース文字列（ニーモニック無し）
	 * @return 含まれる場合は true
	 */
	protected boolean containsTrace(
			List<TraceJointPoint> traceDefList, StackTraceElement[] stacks, String en) {

		// スタックトレースから判定
		if (stacks == null) {
			stacks = getStackTrace();
		}

		for (TraceJointPoint def : traceDefList) {

			int traceMax = def.getLimit();

			for (int i = 0; i < traceMax && i < stacks.length; i++) {

				StackTraceElement stack = stacks[i];

				// クラス名は前方一致
				if (stack.getClassName().startsWith(def.getClassName())) {

					// メソッド名は完全一致 (先頭が大文字の場合はコンストラクタと判断し <init>)
					String methodName = def.getMethodName();
					if (methodName == null) {
						return true;
					}
					String steMethodName = stack.getMethodName();
					if (Character.isUpperCase(methodName.charAt(0)) && steMethodName.equals("<init>")) {
						return true;
					}
					if (steMethodName.equals(methodName)) {
						return true;
					}
				}
			}
		}
		return false;
	}

	/**
	 * 現在のスレッドのスタックトレースを取得します。
	 * Analysis 計測のためメソッド化。
	 * <p>
	 * @return スタックトレース
	 */
	protected StackTraceElement[] getStackTrace() {

		// 高速化 (Log4j などの実装でも同様の変更が行われている)
		// return Thread.currentThread().getStackTrace();
		return new Throwable().getStackTrace();
	}

	/**
	 * 引数に指定したパッケージがスタックトレースに含まれている場合は、対応する値を返します。
	 * @param packageMap パッケージマップ
	 * @return パッケージに対応する値
	 */
	public String findTrace(PropertySet packageMap) {

		StackTraceElement[] stes = getStackTrace();

		for (String pkg : packageMap.keySet()) {
			if (pkg == null) {
				continue;
			}
			for (int i = 0; i < TRACE_MAX && i < stes.length; i++) {
				StackTraceElement ste = stes[i];

				if (ste.getClassName().startsWith(pkg)) {
					return packageMap.get(pkg);
				}
			}
		}
		return packageMap.get(null);
	}

	//-------------------------------------------------------------------------
	// デバッグモード時の getStackTrace() 調査

	/**  */
	private final Map<JointPoint, AtomicInteger> callCountMap = new HashMap<JointPoint, AtomicInteger>();

	/**
	 * 呼び出し回数を加算します。
	 * @param jointPoint ジョイントポイント (null も有効な値)
	 * @param stes スタックトレース
	 */
	private void debugCalls(JointPoint jointPoint, StackTraceElement[] stes) {

		if (stes == null && log.isDebugEnabled()) {
			AtomicInteger i = callCountMap.get(jointPoint);
			if (i == null) {
				i = new AtomicInteger();
				callCountMap.put(jointPoint, i);
			}
			i.incrementAndGet();
		}
	}

	/**
	 * getStackTrace() 呼び出しトップ n をログに出力します。
	 */
	public void flushStackTraceCount() {

		if (!log.isDebugEnabled()) {
			return;
		}
		ArrayList<Entry<JointPoint, AtomicInteger>> list =
				new ArrayList<Entry<JointPoint, AtomicInteger>>(callCountMap.entrySet());

		Collections.sort(list, new Comparator<Entry<JointPoint, AtomicInteger>>() {
			@Override
			public int compare(Entry<JointPoint, AtomicInteger> e1, Entry<JointPoint, AtomicInteger> e2) {
				return e2.getValue().get() - e1.getValue().get();
			}
		});

		for (int i = 0; i < list.size() && i < 5; i++) {

			Entry<JointPoint, AtomicInteger> entry = list.get(i);
			JointPoint p = entry.getKey();
			int count = entry.getValue().get();
			if (p == null) {
				log.debug("getStackTrace() %4d 回呼出 %%EXCLUDE%% によるパッケージ除外", count);
			} else {
				log.debug("getStackTrace() %4d 回呼出 %s %s %s", count,
						p.getClassName(), p.getMethodName(), p.getDescriptor());
			}
		}
	}
}
